diff --git a/campaigns/TEMPLATE-DRIFT-CAMPAIGN.adoc b/campaigns/TEMPLATE-DRIFT-CAMPAIGN.adoc index 60db62c2..e82d0017 100644 --- a/campaigns/TEMPLATE-DRIFT-CAMPAIGN.adoc +++ b/campaigns/TEMPLATE-DRIFT-CAMPAIGN.adoc @@ -34,8 +34,11 @@ No rule in `AGENTIC.a2ml` assigns these recipes; that grant is the owner's. | `workflow-linter.yml` with the rule `head -1 "$file" \| grep -q "^# SPDX-License-Identifier:"`. The tool `gh actions-lock` writes its comment on line 1, so each lock run makes each managed workflow fail the lint. + Also a `workflow-linter.yml` with the first form of the template rule, + which stops only at a line in column 1 (`/^[^#[:space:]]/`). A workflow + with an indented body then passes with a licence line after the body. | The rule of the template: the licence line can be at any position in the - leading comment block. + leading comment block, and the block ends at the first line of the body. | `recipe-sonarqube-token-guard` | `fix-sonarqube-token-guard.sh` @@ -73,6 +76,9 @@ Each recipe was also run on the files of `system-et` at `main` (`04a8b6e`): * SPDX rule: the step is identical to the step of the template branch (`rsr-template-repo` pull request 323, `915acb2`). That pull request makes the rule stop at an indented workflow body also. +* SPDX rule, first form of the template rule: run on the linter of + `system-et` at `main` (`bbe1d13`), which has that form. 1 line changes, and + the step is identical to the step of the template at `main` (`0208563`). * Token guard: the job is identical to the job in pull request 92 (`9f529ef`). The guard step is identical to the guard step of the template branch (`rsr-template-repo` pull request 321, `806dac0`). @@ -102,10 +108,10 @@ and `fix-lock-self-sha-refs.sh` already cover these. inactive file (`e2e.yml.template`). * The token guard recipe edits only the shape of the old template. It does not rewrite a workflow that a person changed. -* The SPDX recipe has 1 target: the `head -1` rule. A linter that has the - first form of the template rule (the form before `rsr-template-repo` pull - request 323, for example in `system-et`) is not a target, and keeps that - form. +* The SPDX recipe has 2 targets: the `head -1` rule, and the first form of + the template rule (the form before `rsr-template-repo` pull request 323, + for example in `system-et`). A linter with any other rule is not a target, + and keeps its rule. * The number of repositories with each target is not known. No estate scan was made. diff --git a/scripts/fix-workflow-linter-spdx-rule.sh b/scripts/fix-workflow-linter-spdx-rule.sh index 7ec4113a..c479eff5 100755 --- a/scripts/fix-workflow-linter-spdx-rule.sh +++ b/scripts/fix-workflow-linter-spdx-rule.sh @@ -3,18 +3,30 @@ # # fix-workflow-linter-spdx-rule.sh: accept the SPDX line in the leading comment block # -# Older copies of the estate template have this rule in -# .github/workflows/workflow-linter.yml: +# Older copies of the estate template have 1 of 2 rules in +# .github/workflows/workflow-linter.yml. +# +# Form 1 reads line 1 only: # # if ! head -1 "$file" | grep -q "^# SPDX-License-Identifier:"; then # # The tool gh actions-lock writes its own comment on line 1 of each workflow # that it manages. Each lock run thus makes every managed workflow fail the -# lint ("missing SPDX header"). The current template (rsr-template-repo) -# accepts the licence line at any position in the leading comment block. -# This recipe replaces the old rule with the rule of the template. +# lint ("missing SPDX header"). +# +# Form 2 is the first template rule that reads the leading comment block: +# +# if ! awk '... /^[^#[:space:]]/ { exit } ...' "$file"; then +# +# It stops only at a line that starts in column 1. A workflow with an +# indented body thus passes with a licence line after the body +# (rsr-template-repo pull request 323). +# +# The current template (rsr-template-repo) accepts the licence line at any +# position in the leading comment block, and stops at the first line of the +# body. This recipe replaces each form with the rule of the template. # -# Only the exact old rule is a target. A linter with a different rule (for +# Only the 2 exact forms are targets. A linter with a different rule (for # example `head -5`) is not changed. # # Idempotent: a file that has the new rule is not changed. @@ -27,7 +39,23 @@ set -euo pipefail REL=".github/workflows/workflow-linter.yml" OLD_RULE='^[[:space:]]*if ! head -1 "\$[A-Za-z_][A-Za-z0-9_]*" \| grep -q "\^# SPDX-License-Identifier:"; then[[:space:]]*$' +OLD_AWK_RULE='/^# SPDX-License-Identifier:/ { found=1 } /^[^#[:space:]]/ { exit } END { exit !found }' NEW_RULE='/^# SPDX-License-Identifier:/ { found=1 } /^[[:space:]]*[^#[:space:]]/ { exit } END { exit !found }' +export OLD_AWK_RULE NEW_RULE + +# awk function: the position of form 2 in a line, or 0. A line is a target +# only when the whole line is `if ! awk '
' "$name"; then`. +OLD_AWK_AT=$(cat <<'AWK' +function old_awk_at(s, lit, p) { + lit = "awk '" ENVIRON["OLD_AWK_RULE"] "' \"$" + p = index(s, lit) + if (p == 0) return 0 + if (substr(s, 1, p - 1) !~ /^[[:space:]]*if ! $/) return 0 + if (substr(s, p + length(lit)) !~ /^[A-Za-z_][A-Za-z0-9_]*"; then[[:space:]]*$/) return 0 + return p +} +AWK +) # Print the path of the linter when it is a regular file inside the repository. # A missing file, a symlink, or a path that leaves the repository is no target. @@ -41,9 +69,11 @@ target_file() { # Print the number of old-rule lines in the repository (0 when no target file). count_targets() { - local file + local file form1 form2 file=$(target_file "$1") || { echo 0; return 0; } - grep -cE -- "$OLD_RULE" "$file" || true + form1=$(grep -cE -- "$OLD_RULE" "$file" || true) + form2=$(awk "$OLD_AWK_AT"' old_awk_at($0) { n++ } END { print n + 0 }' "$file") + echo $((form1 + form2)) } if [[ "${1:-}" == "--check" ]]; then @@ -76,11 +106,19 @@ PROGRAM=$(cat <<'AWK' /echo "Add '# SPDX-License-Identifier: [A-Za-z0-9.+-]+' as first line"/ { sub(/ as first line"/, " to the leading comment block\"") } +{ + # Form 2: only the rule text changes. The 2 comment lines are there. + p = old_awk_at($0) + if (p) { + rest = substr($0, p + length("awk '" ENVIRON["OLD_AWK_RULE"] "' \"$")) + $0 = substr($0, 1, p - 1) "awk '" ENVIRON["NEW_RULE"] "' \"$" rest + } +} { print } AWK ) -NEW_RULE="$NEW_RULE" awk "$PROGRAM" "$FILE" > "$TMP" +awk "$OLD_AWK_AT"$'\n'"$PROGRAM" "$FILE" > "$TMP" [[ -s "$TMP" ]] || { echo "fix-workflow-linter-spdx-rule: the edit gave no text; $REL is not changed" >&2; exit 1; } cat "$TMP" > "$FILE" diff --git a/tests/template-drift-recipes-test.sh b/tests/template-drift-recipes-test.sh index 7a82140f..d0077f24 100644 --- a/tests/template-drift-recipes-test.sh +++ b/tests/template-drift-recipes-test.sh @@ -107,6 +107,23 @@ printf '# SPDX-License-Identifier: MPL-2.0\n name: a\n jobs: {}\n' > "$TMP/s5" if awk "$RULE" "$TMP/s4"; then fail "$R: the new rule accepts a licence after an indented body"; fi awk "$RULE" "$TMP/s5" || fail "$R: the new rule refuses a licence before an indented body" +# Form 2, the first template rule that reads the leading comment block, is a +# target too. Only the rule text changes: the result is the same file. +REPO=$(new_repo spdx-form2); F="$REPO/.github/workflows/workflow-linter.yml" +OLD_STOP='/^[^#[:space:]]/ { exit }' +NEW_STOP='/^[[:space:]]*[^#[:space:]]/ { exit }' +awk -v old="$OLD_STOP" -v new="$NEW_STOP" '{ p = index($0, new); if (p) $0 = substr($0, 1, p - 1) old substr($0, p + length(new)); print }' "$TMP/expected" > "$F" +grep -qF -- "$OLD_STOP" "$F" || fail "$R: the test file does not have form 2" +# Form 2 accepts a licence after an indented body. That is its defect. +FORM2=$(sed -n "s/^ *if ! awk '\(.*\)' \"\$file\"; then\$/\1/p" "$F") +awk "$FORM2" "$TMP/s4" || fail "$R: the test file does not have the defect of form 2" +repair "$R" "$REPO" "$F" 1 "$TMP/expected" + +# The rule text of form 2 in a line of a different shape is not a target. +REPO=$(new_repo spdx-form2-other); F="$REPO/.github/workflows/workflow-linter.yml" +printf '%s\n' " awk '/^# SPDX-License-Identifier:/ { found=1 } $OLD_STOP END { exit !found }' \"\$file\" || failed=1" > "$F" +expect_untouched "$R" "$REPO" "$F" + # A linter with a different rule is not a target. REPO=$(new_repo spdx-other); F="$REPO/.github/workflows/workflow-linter.yml" printf '%s\n' ' if ! head -5 "$f" | grep -q "SPDX-License-Identifier"; then' > "$F"