Ctrl K
ComunidadGuías…

hack/pr-labeler-contract.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# Contract for the PR title labeler in .github/workflows/pr-labeler.yaml.
#
# Every failure this guards is silent: the job either dies on a title it cannot
# handle, applies a label that does not exist, or lands a PR in the wrong area
# with no warning, and nobody reads the log of a labeler that did not label.
#
# The behavioural tests run the workflow's own github-script block under node,
# extracted from the file rather than mirrored, with `context`, `core` and
# `github` stubbed, so an edit to the script is what gets tested. node is
# required, not optional: a suite that skips when the runtime is missing goes
# green having run nothing.

load test_helper

REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
WF="$REPO_ROOT/.github/workflows/pr-labeler.yaml"
LABELS="$REPO_ROOT/.github/labels.yml"

need_node() {
  command -v node >/dev/null 2>&1 && return 0
  echo "node is required to run the pr-labeler script" >&2
  return 1
}

# The `script: |` block, de-indented to plain JavaScript. The block ends at the
# first non-blank line indented less than its body, where the next job starts.
extract_script() {
  awk 'f && length($0) && substr($0, 1, 12) != "            " { exit }
       f { print substr($0, 13) }
       /^ +script: \|$/ { f = 1 }' "$WF" > "$1"
  [ -s "$1" ]
}

# Runs the extracted script once per title read from stdin, all in one node
# process, and prints one line per title: the title, a tab, then the applied
# labels sorted and comma-joined, or THROW: and the error message.
#
# JavaScript closing braces below are indented on purpose: hack/cozytest.sh
# rewrites any line that is exactly `}` into a shell `return 0`.
run_labeler() {
  tmp="$(mktemp -d)"
  extract_script "$tmp/script.js"
  cat > "$tmp/run.js" <<'JS'
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
const AsyncFunction = (async () => {}).constructor;
const fn = new AsyncFunction('context', 'core', 'github', src);
(async () => {
  const out = [];
  const titles = fs.readFileSync(0, 'utf8').split('\n').filter(Boolean);
  for (const title of titles) {
    let applied = [];
    const context = {
      repo: { owner: 'o', repo: 'r' },
      payload: { pull_request: { number: 1, title, body: '', labels: [] } },
    };
    const core = { warning() {}, info() {} };
    const github = { rest: { issues: { addLabels: async (a) => { applied = a.labels; } } } };
    try {
      await fn(context, core, github);
      out.push(title + '\t' + [...applied].sort().join(','));
    } catch (e) {
      out.push(title + '\tTHROW: ' + e.message);
    }
  }
  fs.writeFileSync(1, out.join('\n') + '\n');
})();
JS
  node "$tmp/run.js" "$tmp/script.js"
  rm -rf "$tmp"
}

# Asserts the labeler's output for each `title<TAB>labels` line on stdin.
expect_labels() {
  want="$(cat)"
  got="$(printf '%s\n' "$want" | cut -f1 | run_labeler)"
  [ "$got" = "$want" ] || {
    echo "labeler output mismatch." >&2
    echo "want:" >&2; printf '%s\n' "$want" | sed 's/^/  /' >&2
    echo "got:" >&2; printf '%s\n' "$got" | sed 's/^/  /' >&2
    return 1
  }
}

# The scopeToArea literal as JSON pairs, plus a count of the key lines in the
# source, quoted either way or bare. A duplicate key collapses when the literal
# is evaluated, so the two disagree when a key is written twice, as long as each
# key sits on its own line.
scope_map() { # json|keylines
  tmp="$(mktemp -d)"
  extract_script "$tmp/script.js"
  cat > "$tmp/map.js" <<'JS'
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
const m = src.match(/const scopeToArea = (\{[\s\S]*?\n\};)/);
if (!m) { throw new Error('scopeToArea literal not found'); }
const lines = m[1].split('\n').filter((l) => /^\s*(['"]?)[^'"\s:]+\1\s*:/.test(l));
const map = new Function('return ' + m[1].replace(/;$/, ''))();
if (process.argv[3] === 'keylines') {
  fs.writeFileSync(1, lines.length + ' ' + Object.keys(map).length + '\n');
} else {
  fs.writeFileSync(1, Object.entries(map).map(([k, v]) => k + '\t' + v).join('\n') + '\n');
  }
JS
  node "$tmp/map.js" "$tmp/script.js" "$1"
  rm -rf "$tmp"
}

area_labels() {
  sed -n 's/^- name: \(area\/[^[:space:]]*\)[[:space:]]*$/\1/p' "$LABELS"
}

@test "the workflow under contract exists" {
  [ -f "$WF" ]
  [ -f "$LABELS" ]
}

# A key inherited from Object.prototype resolves to a function, which is truthy,
# lands in the label set, and then kills the job on `l.startsWith`.
@test "a scope or type named after an Object.prototype member does not kill the job" {
  need_node
  expect_labels <<'EOF'
fix(constructor): x	area/uncategorized,kind/bug
fix(toString): x	area/uncategorized,kind/bug
fix(hasOwnProperty): x	area/uncategorized,kind/bug
fix(__proto__): x	area/uncategorized,kind/bug
[constructor] x	area/uncategorized
constructor: x	area/uncategorized
EOF
}

# One stripped prefix left the second one to be parsed as a bracket scope, which
# dropped the real scope and the kind, and the area/release from the first strip
# suppressed the uncategorized signal.
@test "every leading backport prefix is stripped before the title is parsed" {
  need_node
  expect_labels <<'EOF'
[Backport release-1.3] fix(linstor): x	area/release,area/storage,kind/bug
[Backport release-1.2] [Backport release-1.3] fix(linstor): x	area/release,area/storage,kind/bug
[Backport release-1.2] [Backport release-1.3] [Backport release-1.4] feat(linstor): x	area/release,area/storage,kind/feature
EOF
}

@test "an unmapped scope still falls back to area/uncategorized" {
  need_node
  expect_labels <<'EOF'
fix(no-such-scope): x	area/uncategorized,kind/bug
fix: x	area/uncategorized,kind/bug
just a sentence	area/uncategorized
EOF
}

@test "recurring scopes resolve to their area" {
  need_node
  expect_labels <<'EOF'
fix(etcd-operator): x	area/database,kind/bug
fix(opensearch): x	area/database,kind/bug
fix(multus): x	area/networking,kind/bug
fix(kubeovn): x	area/networking,kind/bug
fix(kubevirt-operator): x	area/virtualization,kind/bug
fix(keycloak-configure): x	area/keycloak,kind/bug
docs(changelog): x	area/release,kind/documentation
fix(lineage): x	area/platform,kind/bug
fix(piraeus-operator): x	area/storage,kind/bug
fix(monitoring-agents): x	area/monitoring,kind/bug
ci(workflows): x	area/ci
fix(hack): x	area/dx,kind/bug
EOF
}

# Contributors reach for the subsystem name as often as for a package name, so
# every area is reachable by its own name.
@test "every area label is accepted as a scope that maps to itself" {
  need_node
  map="$(scope_map json)"
  missing=""
  for a in $(area_labels); do
    [ "$a" = "area/uncategorized" ] && continue
    printf '%s\n' "$map" | grep -qxF "${a#area/}	$a" || missing="$missing ${a#area/}"
  done
  [ -z "$missing" ] || { echo "scopes missing for their own area:$missing" >&2; return 1; }
}

# A target missing from labels.yml is a label the sync workflow never creates,
# and nothing in a run of the labeler reports it.
@test "every scopeToArea target is an area label defined in labels.yml" {
  need_node
  defined="$(area_labels)"
  [ -n "$defined" ]
  bad="$(scope_map json | cut -f2 | sort -u | grep -vxF "$defined" || true)"
  [ -z "$bad" ] || { echo "targets not in labels.yml: $bad" >&2; return 1; }
}

# A repeated key silently overrides the earlier value and reads as an ordinary
# added line in review.
@test "no scope key appears twice in scopeToArea" {
  need_node
  read -r lines keys <<EOF
$(scope_map keylines)
EOF
  [ "$lines" -gt 0 ]
  [ "$lines" = "$keys" ] || { echo "$lines key lines but $keys distinct keys" >&2; return 1; }
}