mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-28 07:37:49 +09:00
Every unit bats file under hack/ must load test_helper so it runs with nounset, and hack/bats-strict-setup.bats fails the suite for a file that does not. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <f@lex.la>
243 lines
9.4 KiB
Bash
243 lines
9.4 KiB
Bash
#!/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 4 ]
|
|
|
|
# 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 ]
|
|
}
|