Skip to content

Commit 717ea02

Browse files
noreplyclaude
andcommitted
fork-follow: changed and non-ASCII upstream workflows hold the move too
new_workflows read `git diff --name-only` with git's quoting, so a new upstream workflow with a non-ASCII name came out quoted, failed the .yml filter and slipped past the branch-move refusal. It now reads the diff NUL-separated with core.quotepath=off, and refuses a name with ',' or a newline (the lists are ','-joined). It also takes changed files (--diff-filter=AM): an unlisted upstream workflow that now runs on push holds the move like a new one. The lists are built in the plan's own shell, so a refusal stops the plan. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 2e10b37 commit 717ea02

5 files changed

Lines changed: 68 additions & 22 deletions

File tree

‎.github/scripts/fork-follow-report.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ fi
114114
[[ -z "${CANCELLED:-}" ]] || row "Their runs cancelled" "${CANCELLED//,/, }"
115115
[[ -z "${FINISHED:-}" ]] || row "**Upstream runs that finished before a sweep could stop them**" "${FINISHED//,/, }"
116116
[[ -z "${UNREGISTERED:-}" ]] \
117-
|| row "**Upstream workflow files new on this release that GitHub does not list yet, so not disabled** (the sweep after an event reaches them disables them; the branch move waits for that)" "${UNREGISTERED//,/, }"
117+
|| row "**Upstream workflow files new or changed on this release that GitHub does not list yet, so not disabled** (the sweep after an event reaches them disables them; the branch move waits for that)" "${UNREGISTERED//,/, }"
118118
[[ -z "${DROPPED:-}" ]] || row "Patches dropped (empty on $BASE_TAG: upstream has the change)" "${DROPPED//;/, }"
119119

120120
reasons=""

‎.github/scripts/fork-follow.sh‎

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
# dropped subjects of patches that became empty on the new base (';'-separated)
2525
# own_workflows workflow files the series itself adds (','-separated); every other one is
2626
# upstream's and gets disabled
27-
# new_workflows upstream workflow files added between the series' old base and the new one
28-
# (','-separated; empty without a new base). The branch move refuses while one of
29-
# them is not listed by GitHub yet, so it cannot be disabled before main has it.
27+
# new_workflows upstream workflow files added or changed between the series' old base and the
28+
# new one (','-separated; empty without a new base). The branch move refuses while
29+
# one of them is not listed by GitHub yet, so it cannot be disabled before main has
30+
# it (a changed file may now run on push).
3031
set -Eeuo pipefail
3132

3233
: "${UPSTREAM:?owner/name of the upstream repository}"
@@ -152,12 +153,35 @@ latest_release() {
152153
[[ -z "$last" ]] || echo "$last $(git rev-parse "refs/fork-tags/$last^{commit}")"
153154
}
154155

155-
own_workflows() {
156-
git diff --name-only --diff-filter=A "$1" "$2" -- .github/workflows | paste -sd, -
156+
# Workflow paths, read NUL-separated with core.quotepath=off, so a name with non-ASCII bytes, a
157+
# quote or a backslash comes out as GitHub lists it. The lists are ','-joined, so a name with a ','
158+
# or a newline cannot be named and the plan refuses it. These run in the plan's own shell (no
159+
# command substitution), so a refusal stops the plan.
160+
workflow_paths() { # workflow_paths FILTER FROM TO: sets paths, the ','-joined files FROM..TO shows as FILTER
161+
local path
162+
paths=""
163+
git -c core.quotepath=off diff -z --no-renames --name-only --diff-filter="$1" "$2" "$3" -- .github/workflows \
164+
> "$logs/paths" || fail "Could not list the workflow files that differ between ${2:0:12} and ${3:0:12}."
165+
while IFS= read -r -d '' path; do
166+
if [[ "$path" == *[,$'\n']* ]]; then
167+
fail "The workflow file \`$path\` (between ${2:0:12} and ${3:0:12}) has a ',' or a newline in its name, so it cannot be named to the workflow sweep." \
168+
"The series was left as it is. Rename or delete it with a fork patch."
169+
fi
170+
paths+="${paths:+,}$path"
171+
done < "$logs/paths"
172+
}
173+
own_workflows() { # own_workflows UPSTREAM_COMMIT TIP: sets own_list, the workflow files the series adds
174+
workflow_paths A "$1" "$2"
175+
own_list="$paths"
157176
}
158-
new_workflows() { # upstream workflow files added from upstream commit $1 to $2 (a rename counts as new)
159-
git diff --no-renames --name-only --diff-filter=A "$1" "$2" -- .github/workflows \
160-
| { grep -E '\.ya?ml$' || true; } | paste -sd, -
177+
new_workflows() { # new_workflows FROM TO: sets new_list, the upstream workflow files added or changed
178+
local path list=()
179+
workflow_paths AM "$1" "$2" # a rename counts as new
180+
IFS=, read -r -a list <<< "$paths"
181+
new_list=""
182+
for path in "${list[@]}"; do
183+
[[ "$path" != *.yml && "$path" != *.yaml ]] || new_list+="${new_list:+,}$path"
184+
done
161185
}
162186

163187
# The sweep (fork-workflows.sh) takes every .github/workflows/fork-*.y[a]ml as the fork's own, even
@@ -231,13 +255,14 @@ if [[ "$target_tag" == "$base_tag" ]]; then
231255
stage_report "$head" "$base_tag" "$changed"
232256
fi
233257
fi
258+
own_workflows "$base" HEAD
234259
{
235260
echo "action=$action"
236261
echo "base_tag=$base_tag"
237262
echo "base_commit=$base"
238263
echo "candidate=$head"
239264
echo "released_tag=${released_tag:-}"
240-
echo "own_workflows=$(own_workflows "$base" HEAD)"
265+
echo "own_workflows=$own_list"
241266
echo "new_workflows="
242267
} >> "$OUTPUT"
243268
exit 0
@@ -280,6 +305,8 @@ else
280305
stage_report "$candidate" "$target_tag" "$changed"
281306
fi
282307
fi
308+
own_workflows "$target" "$candidate"
309+
new_workflows "$base" "$target"
283310
{
284311
echo "action=$action"
285312
echo "base_tag=$target_tag"
@@ -289,6 +316,6 @@ fi
289316
echo "candidate=$candidate"
290317
echo "released_tag=${released_tag:-}"
291318
echo "dropped=$dropped"
292-
echo "own_workflows=$(own_workflows "$target" "$candidate")"
293-
echo "new_workflows=$(new_workflows "$base" "$target")"
319+
echo "own_workflows=$own_list"
320+
echo "new_workflows=$new_list"
294321
} >> "$OUTPUT"

‎.github/scripts/fork-follow.test.sh‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,24 @@ check "plan: same base with an upstream fork-*.yml is refused too" \
313313
plan
314314
check "plan: a fork patch that deletes the upstream fork-*.yml lets the plan go on" action_is release
315315

316+
# Round 4: a changed upstream workflow counts like a new one (an unlisted file that now runs on push
317+
# must hold the move too), and a non-ASCII name comes out as GitHub lists it, not quoted by git.
318+
new_upstream
319+
(cd "$root/upwork" && git checkout -q main && echo "on: [push] # changed" > .github/workflows/ci.yml \
320+
&& echo "on: push" > .github/workflows/dé.yml && git add -A && git commit -q -m u5 \
321+
&& git tag v0.1.11 && git push -q https://github.com/up/t3code.git main --tags) > /dev/null 2>&1
322+
new_fork v0.1.10
323+
plan
324+
check "plan: a changed upstream workflow and a non-ASCII new one are both named, unquoted" \
325+
eval 'action_is rebase && [[ "$(out new_workflows)" == ".github/workflows/ci.yml,.github/workflows/dé.yml" ]]'
326+
new_upstream
327+
(cd "$root/upwork" && git checkout -q main && echo "on: push" > ".github/workflows/a,b.yml" && git add -A \
328+
&& git commit -q -m u5 && git tag v0.1.11 && git push -q https://github.com/up/t3code.git main --tags) > /dev/null 2>&1
329+
new_fork v0.1.10
330+
plan
331+
check "plan: a new upstream workflow with ',' in its name (cannot be listed) is refused, nothing planned" \
332+
eval '[[ -z "$(out action)" ]] && has "a,b.yml" "$root/report.md" && has "cannot be named" "$root/report.md" && hasnt "plan stopped at" "$root/report.md"'
333+
316334
# Minor: upstream changed a reusable workflow the release calls; the run's own copy is older.
317335
new_upstream
318336
(cd "$root/upwork" && git checkout -q main && echo "on: workflow_call # v2" > .github/workflows/release-desktop.yml \
@@ -503,7 +521,7 @@ check "workflows: an unlisted upstream file from an earlier release is named apa
503521
rm -f "$root/api/writes"
504522
OWN_WORKFLOWS=.github/workflows/fork-follow.yml,.github/workflows/fork-tests.yml HEAD_SHA=abc NEW_WORKFLOWS=.github/workflows/release.yml sweep
505523
check "workflows: an unlisted upstream file new on this release is named with a warning, not called disabled" \
506-
eval '[[ "$(sed -n "s/^unregistered=//p" "$root/sweep.out")" == ".github/workflows/release.yml" && -z "$(sed -n "s/^unregistered_old=//p" "$root/sweep.out")" ]] && has "::warning::.*new on this release.*does not list yet" "$root/out"'
524+
eval '[[ "$(sed -n "s/^unregistered=//p" "$root/sweep.out")" == ".github/workflows/release.yml" && -z "$(sed -n "s/^unregistered_old=//p" "$root/sweep.out")" ]] && has "::warning::.*new or changed on this release.*does not list yet" "$root/out"'
507525
rm -f "$root/api/writes" "$root/report.md"
508526
check "workflows: before the move, a new unlisted upstream file refuses it, after disabling the listed ones" \
509527
eval '! OWN_WORKFLOWS=.github/workflows/fork-follow.yml HEAD_SHA=abc NEW_WORKFLOWS=.github/workflows/release.yml REQUIRE_NEW_OFF=true sweep && has "main was not moved" "$root/report.md" && has "release.yml" "$root/report.md" && has "A person decides" "$root/report.md" && has "workflows/2/disable" "$root/api/writes"'

‎.github/scripts/fork-workflows.sh‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
# finished runs of upstream workflows that completed since SINCE (they ran before a sweep
77
# could stop them), with their conclusion
88
# unregistered upstream workflow files in the tree at TREE_REF that GitHub does not list yet
9-
# and that are new on this upstream release (NEW_WORKFLOWS). GitHub lists a
10-
# workflow only once an event has reached its file, so these can be neither
9+
# and that are new or changed on this upstream release (NEW_WORKFLOWS). GitHub
10+
# lists a workflow only once an event has reached its file, so these can be neither
1111
# disabled nor cancelled until then; the next sweep after that event does.
1212
# unregistered_old the other unlisted upstream files, from earlier releases. Most of them run
1313
# only on events the fork never sends (workflow_call, workflow_dispatch,
@@ -18,7 +18,7 @@
1818
#
1919
# Environment: REPOSITORY (owner/name; GITHUB_REPOSITORY in a run), OWN_WORKFLOWS (','-separated
2020
# paths the fork's own commits add; required, nothing is guessed), NEW_WORKFLOWS (','-separated
21-
# upstream files added since the series' old base; optional), REQUIRE_NEW_OFF (true: fail when one
21+
# upstream files added or changed since the series' old base; optional), REQUIRE_NEW_OFF (true: fail when one
2222
# of NEW_WORKFLOWS is in the tree and not listed, because then it cannot be disabled before the
2323
# series branch has it), TREE_REF (commit or branch whose workflow files are checked; default
2424
# HEAD_SHA, else SERIES_BRANCH, else main), HEAD_SHA, SERIES_BRANCH, PREVIOUS_HEAD (the series head
@@ -119,15 +119,15 @@ join() { local IFS=,; echo "$*"; }
119119
((${#finished[@]} == 0)) \
120120
|| echo "::warning::Upstream workflow runs finished before they could be stopped: $(join "${finished[@]}")" >&2
121121
((${#unregistered[@]} == 0)) \
122-
|| echo "::warning::Upstream workflow files new on this release that GitHub does not list yet, so not disabled: $(join "${unregistered[@]}")" >&2
122+
|| echo "::warning::Upstream workflow files new or changed on this release that GitHub does not list yet, so not disabled: $(join "${unregistered[@]}")" >&2
123123
echo "Upstream workflows disabled: ${#disabled[@]}; runs cancelled: ${#cancelled[@]}; finished before a sweep: ${#finished[@]}; new and not registered yet: ${#unregistered[@]}; older and not registered: ${#unregistered_old[@]}." >&2
124124

125125
# Before the series branch moves: any workflow file on it may ask for the Environment that holds
126126
# FORK_PUSH_TOKEN, and the push starts its push workflows before a later sweep can stop them. A
127127
# new upstream file that GitHub does not list cannot be disabled first, so a person decides.
128128
if [[ "${REQUIRE_NEW_OFF:-}" == true ]] && ((${#unregistered[@]})); then
129129
branch="${SERIES_BRANCH:-main}"
130-
die "$branch was not moved: upstream workflow files that are new on this release are not listed by GitHub yet, so they could not be disabled before $branch gets them:" \
130+
die "$branch was not moved: upstream workflow files that are new or changed on this release are not listed by GitHub yet, so they could not be disabled before $branch gets them:" \
131131
"$(printf -- '- `%s`\n' "${unregistered[@]}")" "" \
132132
"On $branch, a push starts their push workflows before a sweep can stop them, and any workflow file there may ask for the Environment that holds FORK_PUSH_TOKEN." \
133133
"A person decides. Read them in the candidate (\`git show ${TREE_REF:0:12}:<file>\`). If they are harmless, move $branch to $TREE_REF yourself" \

‎.github/workflows/fork-follow.yml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ name: Fork follow upstream
2222
# fork-workflows.sh disables every active workflow the series did not add (a fork-*.yml is always the
2323
# fork's own, also before main has it; the plan refuses an upstream release that has one), cancels
2424
# every unfinished run of one (any event), and names the runs that finished before it could stop
25-
# them and the files GitHub has not listed yet. The branch move is refused while an upstream workflow file that is new
26-
# on this release is not listed (it could not be disabled, and on the branch it could ask for
27-
# fork-push); a person decides then.
25+
# them and the files GitHub has not listed yet. The branch move is refused while an upstream
26+
# workflow file that is new or changed on this release is not listed (it could not be disabled, and
27+
# on the branch it could ask for fork-push); a person decides then.
2828
#
2929
# A re-run of failed jobs (or of one job) keeps the Guard job's earlier result, so the jobs with
3030
# the token are skipped unless the guard passed in this attempt: use "Re-run all jobs".
@@ -290,7 +290,8 @@ jobs:
290290
291291
# Any workflow file on the series branch may ask for fork-push, and the move fires its push
292292
# workflows. So upstream workflows are disabled first, and the move is refused while an
293-
# upstream file new on this release is not listed by GitHub (it cannot be disabled yet).
293+
# upstream file new or changed on this release is not listed by GitHub (it cannot be
294+
# disabled yet).
294295
- id: sweep_before
295296
name: Disable upstream workflows before the move, refuse while a new one cannot be
296297
env:

0 commit comments

Comments
 (0)