Ctrl K
ComunidadGuías…

hack/label-workflows-contract.bats

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats

# Contract for the two label-writing workflows: pr-labeler.yaml (which labels
# it may apply and from what input) and stale.yaml (which labels drive the
# auto-close policy and who is exempt). Every label name either workflow can
# write or match must resolve against .github/labels.yml — a mistyped name is
# not an error at runtime, it is a rule that silently stops matching.
#
# These tests intentionally pin executable/structural lines, not prose: a
# mapping demoted to a YAML `#` or JavaScript `//` comment must never satisfy
# the contract. See hack/promote-gate-contract.bats for the pattern.

load test_helper

REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
PR_LABELER="$REPO_ROOT/.github/workflows/pr-labeler.yaml"
STALE="$REPO_ROOT/.github/workflows/stale.yaml"
LABELS="$REPO_ROOT/.github/labels.yml"
DOCS="$REPO_ROOT/docs/agents/contributing.md"

# Comment-stripping filters, one per language in these files. grep exits 1 on
# an empty selection (legitimate for an empty block) and 2 on a real error, so
# only the latter propagates.
code_lines() {
  local rc=0
  grep -v '^[[:space:]]*#' || rc=$?
  [ "$rc" -le 1 ]
}

script_lines() {
  local rc=0
  grep -v '^[[:space:]]*//' || rc=$?
  [ "$rc" -le 1 ]
}

# Body of a `const <name> = {` ... `};` object literal inside the
# github-script block. Comments are stripped BEFORE block detection so a
# commented-out `// const typeToKind = {` can never open the block.
js_object_block() {
  script_lines < "$2" | awk -v name="const $1 = {" '
    index($0, name) { inside = 1; next }
    inside && /^[[:space:]]*};[[:space:]]*$/ { exit }
    inside'
}

# The type -> kind/* table under "Type → `kind/*`" in docs/agents/contributing.md,
# as "<type> <label>" lines. A row listing several types yields one line per
# type; a "(no kind label)" row yields "-" as the label, so a type documented
# as unmapped is distinguishable from one the table does not mention.
doc_type_table() {
  awk '
    /^\*\*Type .*kind\/\*.*:\*\*/ { inside = 1; next }
    inside && /^\|/ {
      started = 1
      if ($0 ~ /^\|[[:space:]]*type[[:space:]]*\|/ || $0 ~ /^\|[[:space:]-]*\|[[:space:]-]*\|/) next
      line = $0
      sub(/^\|[[:space:]]*/, "", line)
      sub(/[[:space:]]*\|[[:space:]]*$/, "", line)
      split(line, cell, /[[:space:]]*\|[[:space:]]*/)
      label = cell[2]
      if (label == "(no kind label)") label = "-"
      n = split(cell[1], types, /[[:space:]]*,[[:space:]]*/)
      for (i = 1; i <= n; i++) print types[i], label
      next
    }
    inside && started { exit }
  ' "$DOCS"
}

# The executable typeToKind mappings in pr-labeler.yaml, as "<type> <label>"
# lines. Comment-stripped by js_object_block, so a mapping demoted to a
# `//` comment is not listed.
workflow_type_table() {
  js_object_block typeToKind "$PR_LABELER" \
    | sed -nE "s/^[[:space:]]*([A-Za-z]+):[[:space:]]*'([^']*)',?[[:space:]]*$/\1 \2/p"
}

# Every label defined in .github/labels.yml, one per line. Parses the value
# after `- name: ` verbatim (labels may contain spaces, e.g. "help wanted"),
# stripping optional quotes and trailing inline comments.
defined_labels() {
  code_lines < "$LABELS" | awk '/^- name: / {
    sub(/^- name: /, "")
    sub(/[[:space:]]+#.*$/, "")
    sub(/[[:space:]]+$/, "")
    gsub(/^"|"$/, "")
    print
  }'
}

# `key: value` inputs of the stale action; whole-line and trailing inline
# comments are both stripped from the value.
stale_input() {
  code_lines < "$STALE" | awk -v key="          $1: " '
    index($0, key) == 1 {
      sub(key, "")
      sub(/[[:space:]]+#.*$/, "")
      sub(/[[:space:]]+$/, "")
      print
      exit
    }'
}

# A `>-` folded list input of the stale action, flattened to one label per line.
stale_exempt_list() {
  code_lines < "$STALE" | awk -v key="          $1: >-" '
    $0 == key { inside = 1; next }
    inside && !/^            / { exit }
    inside { print }' | tr -d ' ' | tr ',' '\n' | grep -v '^$'
}

assert_labels_defined() {
  # $1: newline-separated label names. Fails naming the missing ones.
  # No process substitution here: hack/cozytest.sh runs these files in plain
  # sh, which lacks it. (`local` is fine — dash/ash/bash all support it, and
  # the other contract files in hack/ already use it.)
  local all missing
  all="$(defined_labels)"
  missing="$(printf '%s\n' "$1" | sort -u | while read -r l; do
    [ -n "$l" ] || continue
    printf '%s\n' "$all" | grep -qxF "$l" || printf '%s\n' "$l"
  done)"
  if [ -n "$missing" ]; then
    echo "labels not defined in .github/labels.yml: $missing" >&2
    return 1
  fi
}

@test "every label pr-labeler can apply resolves against labels.yml" {
  kind_labels="$(js_object_block typeToKind "$PR_LABELER" | grep -o "'kind/[^']*'" | tr -d "'")"
  [ -n "$kind_labels" ]
  assert_labels_defined "$kind_labels"

  area_labels="$(js_object_block scopeToArea "$PR_LABELER" | grep -o "'area/[^']*'" | tr -d "'")"
  [ -n "$area_labels" ]
  assert_labels_defined "$area_labels"

  # Labels added outside the two tables: backport prefix, breaking-change
  # footer, and the no-area fallback.
  literal_labels="$(code_lines < "$PR_LABELER" | script_lines \
    | grep -o "toAdd.add('[^']*')" | sed "s/toAdd.add('//; s/')//" || true)"
  for l in area/release kind/breaking-change area/uncategorized; do
    printf '%s\n' "$literal_labels" | grep -qxF "$l"
  done
  assert_labels_defined "$literal_labels"
}

@test "pr-labeler's type mappings are the table in docs/agents/contributing.md" {
  documented="$(doc_type_table)"
  [ -n "$documented" ]
  mapped="$(workflow_type_table)"
  [ -n "$mapped" ]

  # Mapped rows: the same set in both files, in both directions, so a row
  # added, dropped, retyped or demoted to a `//` comment on either side fails.
  expected="$(printf '%s\n' "$documented" | grep -v ' -$' | sort)"
  actual="$(printf '%s\n' "$mapped" | sort)"
  if [ "$expected" != "$actual" ]; then
    printf 'docs table:\n%s\nworkflow typeToKind:\n%s\n' "$expected" "$actual" >&2
    return 1
  fi

  # Rows the table documents as unmapped must have no key in typeToKind.
  unmapped="$(printf '%s\n' "$documented" | awk '$2 == "-" { print $1 }')"
  [ -n "$unmapped" ]
  for t in $unmapped; do
    count="$(printf '%s\n' "$actual" | grep -c "^$t " || true)"
    [ "${count:-0}" -eq 0 ]
  done
}

@test "pr-labeler subscribes to opened/reopened/synchronize and not edited" {
  count="$(code_lines < "$PR_LABELER" | grep -cF '    types: [opened, reopened, synchronize]' || true)"
  [ "${count:-0}" -eq 1 ]
  # 'edited' is deliberately omitted (see the comment above the trigger); it
  # must not creep back in as an executable line — in either the flow-sequence
  # or block-sequence spelling. Scan the whole trigger section (before jobs:).
  count="$(code_lines < "$PR_LABELER" | awk '/^jobs:/ { exit } { print }' | grep -cw 'edited' || true)"
  [ "${count:-0}" -eq 0 ]
}

@test "pr-labeler is additive only and falls back to area/uncategorized" {
  lines="$(code_lines < "$PR_LABELER" | script_lines)"

  count="$(printf '%s\n' "$lines" | grep -cF "toAdd.add('area/uncategorized')" || true)"
  [ "${count:-0}" -eq 1 ]

  # Only labels not already present are sent, and only via addLabels: a
  # labeler that starts removing or replacing labels is a different tool.
  count="$(printf '%s\n' "$lines" | grep -cF '!existing.has(l)' || true)"
  [ "${count:-0}" -eq 1 ]
  count="$(printf '%s\n' "$lines" | grep -cF 'addLabels' || true)"
  [ "${count:-0}" -eq 1 ]
  count="$(printf '%s\n' "$lines" | grep -cE 'removeLabel|setLabels' || true)"
  [ "${count:-0}" -eq 0 ]
}

@test "every label stale.yaml writes or matches resolves against labels.yml" {
  for key in stale-issue-label stale-pr-label close-issue-label close-pr-label; do
    value="$(stale_input "$key")"
    [ -n "$value" ]
    assert_labels_defined "$value"
  done
  assert_labels_defined "$(stale_exempt_list exempt-issue-labels)"
  assert_labels_defined "$(stale_exempt_list exempt-pr-labels)"
}

@test "stale policy pins: label choices, exempt list sizes, day counts, write cap" {
  [ "$(stale_input stale-issue-label)" = "lifecycle/stale" ]
  [ "$(stale_input stale-pr-label)" = "lifecycle/stale" ]
  [ "$(stale_input close-issue-label)" = "lifecycle/rotten" ]
  [ "$(stale_input close-pr-label)" = "lifecycle/rotten" ]

  count="$(stale_exempt_list exempt-issue-labels | wc -l | tr -d ' ')"
  [ "$count" -eq 12 ]
  count="$(stale_exempt_list exempt-pr-labels | wc -l | tr -d ' ')"
  [ "$count" -eq 2 ]

  # The renewal path: frozen exempts both; an accepted issue or a held PR is
  # deliberate, not stale.
  stale_exempt_list exempt-issue-labels | grep -qxF 'lifecycle/frozen'
  stale_exempt_list exempt-issue-labels | grep -qxF 'triage/accepted'
  stale_exempt_list exempt-pr-labels | grep -qxF 'lifecycle/frozen'
  stale_exempt_list exempt-pr-labels | grep -qxF 'do-not-merge/hold'

  [ "$(stale_input days-before-stale)" = "60" ]
  [ "$(stale_input days-before-close)" = "14" ]
  [ "$(stale_input operations-per-run)" = "100" ]
  [ "$(stale_input remove-stale-when-updated)" = "true" ]
}

@test "stale runs only on a daily schedule and manual dispatch" {
  triggers="$(code_lines < "$STALE" | awk '/^on:/ { inside = 1; next }
    inside && /^[^[:space:]]/ { exit }
    inside')"
  [ -n "$triggers" ]
  printf '%s\n' "$triggers" | grep -qF "    - cron: '37 4 * * *'"
  printf '%s\n' "$triggers" | grep -qE '^  workflow_dispatch:'
  # No event trigger may run the write policy on user activity.
  count="$(printf '%s\n' "$triggers" | grep -cE '^  (push|pull_request|pull_request_target|issues|issue_comment):' || true)"
  [ "${count:-0}" -eq 0 ]
}