forked from heavy-duty/rig
fix(changelog-monotonic): check uniqueness before anything base-side (#98)
Uniqueness is a property of HEAD alone — no base ref, no merge base, no base blob. It sat downstream of all three, so every degradation path returned success on a tree carrying a duplicate. The base-blob path was the worst: a branch that introduces CHANGELOG.md hit a bare `exit 0` on a message that was true about deletion and silent about the duplicate in front of it. STRICT could not reach it — STRICT guards the two skip() calls, and that is not one of them. That inverted the two halves. Deletion needs a diff to see; duplication is the one changelog_section() actually mis-renders, stopping at the second heading and truncating the release's real body. The half with the live extraction bug behind it had the most ways to silently not run. Moved, not rewritten. The skip messages now say containment skipped and that uniqueness already passed. The CI step is no longer pull_request-only, with a `github.ref_name` fallback because base_ref is empty on a push and a bare `origin/` under STRICT would redden every push to main. The script is also now 100755, matching cast's copy of the same file. Regression cases pin the ORDER, not the exit code: verified they go red against the pre-fix script (7 failures) and green against the fixed one. Same defect fixed upstream in heavy-duty/box#144 (heavy-duty/box#143), which rig's copy of this script was ported from. Found by claude-bot-andresmgsl and codex-bot-andresmgsl reviewing #99. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b2e7febf08
commit
7ffc30bacc
4 changed files with 174 additions and 36 deletions
53
.github/scripts/changelog-monotonic.sh
vendored
Normal file → Executable file
53
.github/scripts/changelog-monotonic.sh
vendored
Normal file → Executable file
|
|
@ -69,26 +69,18 @@ skip() {
|
||||||
if [ "$strict" = "1" ]; then
|
if [ "$strict" = "1" ]; then
|
||||||
echo "changelog-monotonic: $* — and CHANGELOG_MONOTONIC_STRICT=1, so this is a FAILURE, not a skip." >&2
|
echo "changelog-monotonic: $* — and CHANGELOG_MONOTONIC_STRICT=1, so this is a FAILURE, not a skip." >&2
|
||||||
echo " CI sets STRICT because a guard that quietly stops guarding is worse than no guard." >&2
|
echo " CI sets STRICT because a guard that quietly stops guarding is worse than no guard." >&2
|
||||||
|
echo " (Uniqueness on HEAD already passed; it is containment that cannot run.)" >&2
|
||||||
echo " Fix the checkout, not this script: the base ref must be fetched (fetch-depth: 0)." >&2
|
echo " Fix the checkout, not this script: the base ref must be fetched (fetch-depth: 0)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "changelog-monotonic: SKIPPED — $*"
|
echo "changelog-monotonic: containment SKIPPED — $*"
|
||||||
echo " (Nothing was checked. In CI this same condition is a hard failure.)"
|
echo " (Uniqueness on HEAD already ran and passed — only the deleted-heading"
|
||||||
|
echo " half needs the history. In CI this same condition is a hard failure.)"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
[ -f "$changelog" ] || { echo "changelog-monotonic: no such file: $changelog" >&2; exit 1; }
|
[ -f "$changelog" ] || { echo "changelog-monotonic: no such file: $changelog" >&2; exit 1; }
|
||||||
|
|
||||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
||||||
|| skip "not inside a git work tree, so there is no history to compare against"
|
|
||||||
|
|
||||||
git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null \
|
|
||||||
|| skip "base ref '$base_ref' does not resolve here (a shallow clone, or a fork checkout without the upstream remote)"
|
|
||||||
|
|
||||||
merge_base="$(git merge-base "$base_ref" HEAD 2>/dev/null || true)"
|
|
||||||
[ -n "$merge_base" ] \
|
|
||||||
|| skip "no merge base between '$base_ref' and HEAD (unrelated histories, or a clone too shallow to reach one)"
|
|
||||||
|
|
||||||
# The set of RELEASE headings: '## <token> ...' where <token> looks like a
|
# The set of RELEASE headings: '## <token> ...' where <token> looks like a
|
||||||
# version. Field $2, the same split changelog_section() uses, so the two
|
# version. Field $2, the same split changelog_section() uses, so the two
|
||||||
# cannot disagree about what a section header is. 'Unreleased' fails the
|
# cannot disagree about what a section header is. 'Unreleased' fails the
|
||||||
|
|
@ -100,14 +92,6 @@ headings_raw() {
|
||||||
}
|
}
|
||||||
headings() { headings_raw | sort -u; }
|
headings() { headings_raw | sort -u; }
|
||||||
|
|
||||||
# The changelog may not exist at the merge base at all (the commit that adds
|
|
||||||
# it). Nothing to have deleted, so nothing to assert.
|
|
||||||
base_file="$(git show "$merge_base:$changelog" 2>/dev/null || true)"
|
|
||||||
[ -n "$base_file" ] || {
|
|
||||||
echo "changelog-monotonic: $changelog does not exist at the merge base ($(git rev-parse --short "$merge_base")) — nothing could have been deleted."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- uniqueness on HEAD (the box#118 class) ----------------------------------
|
# --- uniqueness on HEAD (the box#118 class) ----------------------------------
|
||||||
# Containment catches a DELETED heading. It cannot catch a DUPLICATED one: the
|
# Containment catches a DELETED heading. It cannot catch a DUPLICATED one: the
|
||||||
# duplicate is head-side SURPLUS, and `comm -23` (base minus head) is blind to
|
# duplicate is head-side SURPLUS, and `comm -23` (base minus head) is blind to
|
||||||
|
|
@ -167,6 +151,35 @@ EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- everything below needs the HISTORY --------------------------------------
|
||||||
|
# Uniqueness is settled. What follows is containment, which compares HEAD
|
||||||
|
# against the merge base and therefore genuinely depends on the base ref, the
|
||||||
|
# merge base, and the base blob. Each of those can be unavailable for reasons
|
||||||
|
# that are not the author's fault (a shallow clone, a fork checkout without the
|
||||||
|
# upstream remote, the commit that first adds the changelog), so each degrades
|
||||||
|
# rather than failing — which is exactly why the uniqueness half must NOT live
|
||||||
|
# down here (#98; fixed upstream in heavy-duty/box#143, where rig's copy of
|
||||||
|
# this script came from). It asks nothing of the history, and gating it behind
|
||||||
|
# these conditions let a duplicate exit 0 on a message about deletion.
|
||||||
|
|
||||||
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||||
|
|| skip "not inside a git work tree, so there is no history to compare against"
|
||||||
|
|
||||||
|
git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null \
|
||||||
|
|| skip "base ref '$base_ref' does not resolve here (a shallow clone, or a fork checkout without the upstream remote)"
|
||||||
|
|
||||||
|
merge_base="$(git merge-base "$base_ref" HEAD 2>/dev/null || true)"
|
||||||
|
[ -n "$merge_base" ] \
|
||||||
|
|| skip "no merge base between '$base_ref' and HEAD (unrelated histories, or a clone too shallow to reach one)"
|
||||||
|
|
||||||
|
# The changelog may not exist at the merge base at all (the commit that adds
|
||||||
|
# it). Nothing to have deleted, so nothing to assert.
|
||||||
|
base_file="$(git show "$merge_base:$changelog" 2>/dev/null || true)"
|
||||||
|
[ -n "$base_file" ] || {
|
||||||
|
echo "changelog-monotonic: $changelog does not exist at the merge base ($(git rev-parse --short "$merge_base")) — nothing could have been deleted (uniqueness on HEAD already passed)."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
base_headings="$(printf '%s\n' "$base_file" | headings)"
|
base_headings="$(printf '%s\n' "$base_file" | headings)"
|
||||||
head_headings="$(headings < "$changelog")"
|
head_headings="$(headings < "$changelog")"
|
||||||
|
|
||||||
|
|
|
||||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
|
|
@ -51,21 +51,32 @@ jobs:
|
||||||
run: bash test/labels-reconcile.sh
|
run: bash test/labels-reconcile.sh
|
||||||
- name: release-flow tests
|
- name: release-flow tests
|
||||||
run: bash test/release.sh
|
run: bash test/release.sh
|
||||||
# No SHIPPED release heading was deleted (#98). Its own step rather than
|
# No SHIPPED release heading was deleted or DUPLICATED (#98). Its own step
|
||||||
# a line inside test/release.sh: that suite drives the arming rule
|
# rather than a line inside test/release.sh: that suite drives the arming
|
||||||
# against constructed VERSION + CHANGELOG.md trees that are not git
|
# rule against constructed VERSION + CHANGELOG.md trees that are not git
|
||||||
# repos, and this assert needs a git history — folding it in would make
|
# repos, and this assert needs a git history — folding it in would make
|
||||||
# those cases skip or lie. It is also a DIFFERENT invariant: arming is a
|
# those cases skip or lie. It is also a DIFFERENT invariant: arming is a
|
||||||
# fact about this tree, monotonicity is a fact about this tree versus
|
# fact about this tree, monotonicity is a fact about this tree versus its
|
||||||
# its merge base. Pull requests only: on a push to main the merge base
|
# merge base. STRICT=1 so a checkout that cannot reach the base ref fails
|
||||||
# IS HEAD, so the assert is vacuous and would only add a green step that
|
# here instead of skipping quietly forever.
|
||||||
# proves nothing. STRICT=1 so a checkout that cannot reach the base ref
|
#
|
||||||
# fails here instead of skipping quietly forever.
|
# NOT pull-request-only, and that is the #98 fix at the workflow level.
|
||||||
- name: no shipped changelog heading was deleted
|
# The two halves have different vacuity: DELETION is vacuous on a push to
|
||||||
if: github.event_name == 'pull_request'
|
# main (the merge base IS HEAD), but DUPLICATION is vacuous on no tree at
|
||||||
|
# all, so gating the whole script on `pull_request` left a duplicate that
|
||||||
|
# reached main by any other route unasserted forever.
|
||||||
|
#
|
||||||
|
# The `|| github.ref_name` fallback is load-bearing, not defensive. On a
|
||||||
|
# push event `github.base_ref` is EMPTY, so the argument would collapse to
|
||||||
|
# a bare `origin/`, which does not resolve — and STRICT=1 correctly
|
||||||
|
# promotes that to a hard failure, turning every push to main red. With
|
||||||
|
# the fallback it resolves to the pushed branch, whose merge base with
|
||||||
|
# HEAD is HEAD or its parent: containment passes vacuously, exactly as the
|
||||||
|
# old `if` intended, while uniqueness now runs on every push.
|
||||||
|
- name: no shipped changelog heading was deleted or duplicated
|
||||||
env:
|
env:
|
||||||
CHANGELOG_MONOTONIC_STRICT: '1'
|
CHANGELOG_MONOTONIC_STRICT: '1'
|
||||||
run: bash .github/scripts/changelog-monotonic.sh "origin/${{ github.base_ref }}"
|
run: bash .github/scripts/changelog-monotonic.sh "origin/${{ github.base_ref || github.ref_name }}"
|
||||||
|
|
||||||
# Kept SEPARATE from `check` on purpose: this job pulls a Postgres image and
|
# Kept SEPARATE from `check` on purpose: this job pulls a Postgres image and
|
||||||
# stands up throwaway containers, and a slow image pull must never delay the
|
# stands up throwaway containers, and a slow image pull must never delay the
|
||||||
|
|
|
||||||
35
CHANGELOG.md
35
CHANGELOG.md
|
|
@ -50,6 +50,41 @@ on the way to cutting its first release, and this file starts there.
|
||||||
same class: no conflict, no red run, found only by a human reading the
|
same class: no conflict, no red run, found only by a human reading the
|
||||||
published notes.
|
published notes.
|
||||||
|
|
||||||
|
- **...and the uniqueness half no longer sits behind conditions it does not
|
||||||
|
depend on** (#98, heavy-duty/box#143) — containment is a property of a DIFF
|
||||||
|
and genuinely needs the base ref, the merge base and the base blob.
|
||||||
|
Uniqueness is a property of **HEAD alone** and needs none of the three. It
|
||||||
|
sat downstream of all of them anyway, so every degradation path returned
|
||||||
|
success on a tree carrying a duplicate in plain sight.
|
||||||
|
|
||||||
|
The base-blob case was the worst of the three, because it was not a skip at
|
||||||
|
all: a branch that *introduces* `CHANGELOG.md` hit a bare `exit 0` on a
|
||||||
|
message that was true about deletion and silent about the duplicate in front
|
||||||
|
of it. `STRICT=1` could not reach it — STRICT guards the two `skip()` calls,
|
||||||
|
and that path is not one of them. Off CI the two skips had the same shape, so
|
||||||
|
a shallow clone or an unpacked tarball would not look at a duplicate the
|
||||||
|
author was about to push.
|
||||||
|
|
||||||
|
That inverted the value of the two halves. Deletion is the failure that needs
|
||||||
|
a diff to see; duplication is the one `changelog_section()` actually
|
||||||
|
mis-renders, stopping dead at the second copy and truncating the release's
|
||||||
|
real body. The half with the live extraction bug behind it was the half with
|
||||||
|
the most ways to silently not run.
|
||||||
|
|
||||||
|
Fixed by moving, not rewriting: uniqueness now runs directly after the file
|
||||||
|
exists, before any git access, under a boundary comment saying so. The skip
|
||||||
|
messages say *containment* skipped and that uniqueness already passed, so a
|
||||||
|
skip no longer claims nothing was checked. The CI step is also no longer
|
||||||
|
gated to `pull_request` — deletion is vacuous on a push to main, but
|
||||||
|
duplication is vacuous on no tree, so a duplicate reaching main by any other
|
||||||
|
route went unasserted. That gate could not simply be dropped:
|
||||||
|
`github.base_ref` is empty on a push, and a bare `origin/` under `STRICT=1`
|
||||||
|
is a hard failure on *every* push to main, so the base ref falls back to
|
||||||
|
`github.ref_name`. The regression cases pin the ORDER rather than the exit
|
||||||
|
code, since the clean base-absent tree is green either way — asserting only
|
||||||
|
the code is what let the original ship. `changelog-monotonic.sh` is also now
|
||||||
|
`100755`, matching cast's copy of the same file.
|
||||||
|
|
||||||
- **An unreadable check rollup no longer reads as "nothing is failing"** (#90)
|
- **An unreadable check rollup no longer reads as "nothing is failing"** (#90)
|
||||||
— when `gh pr view` failed, the fallback left the `statusCheckRollup` key
|
— when `gh pr view` failed, the fallback left the `statusCheckRollup` key
|
||||||
absent entirely, and `(.statusCheckRollup // [])` collapsed that into the
|
absent entirely, and `(.statusCheckRollup // [])` collapsed that into the
|
||||||
|
|
|
||||||
|
|
@ -306,8 +306,9 @@ check "monotonic: the re-armed ceremony tree passes too" 0 "" mono "$T"
|
||||||
# of checks exists to refuse. So STRICT flips exactly that case red.
|
# of checks exists to refuse. So STRICT flips exactly that case red.
|
||||||
mono_noref() { local d="$1"; shift; ( cd "$d" && env "$@" bash "$MONO" no/such/ref 2>&1 ); }
|
mono_noref() { local d="$1"; shift; ( cd "$d" && env "$@" bash "$MONO" no/such/ref 2>&1 ); }
|
||||||
T="$(monorepo noref)"
|
T="$(monorepo noref)"
|
||||||
check "monotonic: an unresolvable base ref SKIPS locally" 0 "SKIPPED" mono_noref "$T"
|
check "monotonic: an unresolvable base ref SKIPS containment locally" 0 "containment SKIPPED" mono_noref "$T"
|
||||||
check "monotonic: ...and the skip says nothing was checked" 0 "Nothing was checked" mono_noref "$T"
|
check "monotonic: ...and the skip says uniqueness already ran, not that nothing did" 0 \
|
||||||
|
"already ran and passed" mono_noref "$T"
|
||||||
check "monotonic: ...but is a FAILURE under STRICT=1 (what CI sets)" 1 "STRICT=1" \
|
check "monotonic: ...but is a FAILURE under STRICT=1 (what CI sets)" 1 "STRICT=1" \
|
||||||
mono_noref "$T" CHANGELOG_MONOTONIC_STRICT=1
|
mono_noref "$T" CHANGELOG_MONOTONIC_STRICT=1
|
||||||
check "monotonic: ...and the STRICT failure blames the checkout, not the script" 1 \
|
check "monotonic: ...and the STRICT failure blames the checkout, not the script" 1 \
|
||||||
|
|
@ -320,19 +321,97 @@ T="$(monorepo nofile)"
|
||||||
check "monotonic: a missing changelog file is an error, never a skip" 1 "no such file" \
|
check "monotonic: a missing changelog file is an error, never a skip" 1 "no such file" \
|
||||||
bash -c 'cd "$1" && bash "$2" base nope.md 2>&1' _ "$T" "$MONO"
|
bash -c 'cd "$1" && bash "$2" base nope.md 2>&1' _ "$T" "$MONO"
|
||||||
|
|
||||||
|
# --- #98: uniqueness is a property of HEAD, so nothing base-side may gate it --
|
||||||
|
# Containment needs the merge base. Uniqueness needs only the file in front of
|
||||||
|
# it. As first written (and as inherited from heavy-duty/box, fixed there in
|
||||||
|
# box#144 for box#143) the duplicate check sat DOWNSTREAM of the base-ref,
|
||||||
|
# merge-base and base-blob conditions, so each of the degradation paths below
|
||||||
|
# exited 0 on a tree carrying a duplicate in plain sight — the base-blob one
|
||||||
|
# not even through skip(), but a bare `exit 0` that STRICT could not reach.
|
||||||
|
#
|
||||||
|
# These cases pin the ORDER, which is the actual invariant. Every monorepo
|
||||||
|
# fixture above commits MONO_BASE on 'base', so no case up there ever reaches
|
||||||
|
# the base-absent branch at all; and asserting the exit code alone is what let
|
||||||
|
# the original ship, since the clean base-absent case is green either way.
|
||||||
|
mononocl() { # mononocl <name> -> a repo whose 'base' has NO changelog, on 'work'
|
||||||
|
local d="$WORK/mono-$1"; mkdir -p "$d"
|
||||||
|
git -C "$d" init -q -b base
|
||||||
|
git -C "$d" config user.email harness@example.invalid
|
||||||
|
git -C "$d" config user.name harness
|
||||||
|
printf '%s\n' '# rig' > "$d/README.md"
|
||||||
|
git -C "$d" add README.md
|
||||||
|
git -C "$d" commit -qm 'base: no changelog yet'
|
||||||
|
git -C "$d" checkout -q -b work
|
||||||
|
printf '%s' "$d"
|
||||||
|
}
|
||||||
|
monoadd() { # monoadd <dir> <line...> — the branch INTRODUCES CHANGELOG.md
|
||||||
|
local d="$1"; shift
|
||||||
|
printf '%s\n' "$@" > "$d/CHANGELOG.md"
|
||||||
|
git -C "$d" add CHANGELOG.md
|
||||||
|
git -C "$d" commit -qm 'work: introduce the changelog'
|
||||||
|
}
|
||||||
|
|
||||||
|
# The changelog is absent at the merge base AND the branch introduces a
|
||||||
|
# duplicate. Before the fix this exited 0 on "nothing could have been deleted".
|
||||||
|
T="$(mononocl 98-newdup)"
|
||||||
|
monoadd "$T" '# Changelog' '' '## Unreleased' '' '## 0.2.0 — 2026-07-19' '' \
|
||||||
|
'- **A pending thing** (#2) — prose.' '' '## 0.2.0 — 2026-07-19' '' \
|
||||||
|
'- **A shipped thing** (#1) — prose.'
|
||||||
|
check "monotonic: a duplicate introduced where the base had NO changelog is CAUGHT (#98)" 1 \
|
||||||
|
"DUPLICATE release heading" mono "$T"
|
||||||
|
check "monotonic: ...and STRICT does not change that (it was never a skip)" 1 \
|
||||||
|
"DUPLICATE release heading" mono "$T" CHANGELOG_MONOTONIC_STRICT=1
|
||||||
|
# ...and the clean counterpart still passes, now SAYING uniqueness ran. Without
|
||||||
|
# this the case above could be satisfied by failing the base-absent path
|
||||||
|
# outright, which would redden every changelog-introducing branch.
|
||||||
|
T="$(mononocl 98-newok)"
|
||||||
|
monoadd "$T" '# Changelog' '' '## Unreleased' '' '## 0.2.0 — 2026-07-19' '' \
|
||||||
|
'- **A shipped thing** (#1) — prose.'
|
||||||
|
check "monotonic: ...while a CLEAN introduced changelog still passes" 0 \
|
||||||
|
"nothing could have been deleted" mono "$T"
|
||||||
|
check "monotonic: ...saying uniqueness was checked, not that nothing was" 0 \
|
||||||
|
"uniqueness on HEAD already passed" mono "$T"
|
||||||
|
|
||||||
|
# No git at all (a tarball, an unpacked release): uniqueness still has
|
||||||
|
# everything it needs, so a duplicate is caught rather than skipped past.
|
||||||
|
mkdir -p "$WORK/mono-98-nogit"
|
||||||
|
printf '%s\n' '# Changelog' '' '## 0.2.0 — 2026-07-19' '' \
|
||||||
|
'## 0.2.0 — 2026-07-19' > "$WORK/mono-98-nogit/CHANGELOG.md"
|
||||||
|
check "monotonic: a duplicate OUTSIDE a git work tree is caught (#98)" 1 \
|
||||||
|
"DUPLICATE release heading" mono "$WORK/mono-98-nogit"
|
||||||
|
|
||||||
|
# An unresolvable base ref: same — the skip belongs to containment, not to the
|
||||||
|
# script, so uniqueness has already run by the time skip() is reachable.
|
||||||
|
T="$(monorepo 98-nobase)"
|
||||||
|
monowrite "$T" '# Changelog' '' '## Unreleased' '' '## 0.2.0 — 2026-07-19' '' \
|
||||||
|
'- **A pending thing** (#2) — prose.' '' '## 0.2.0 — 2026-07-19' '' \
|
||||||
|
'- **A shipped thing** (#1) — prose.' '' '## 0.1.0 — 2026-07-01' '' \
|
||||||
|
'- **The first thing** (#0) — prose.'
|
||||||
|
check "monotonic: a duplicate is caught even when the base ref will not resolve (#98)" 1 \
|
||||||
|
"DUPLICATE release heading" mono_noref "$T"
|
||||||
|
|
||||||
# --- ci.yml: the monotonic step is actually wired (#98) ----------------------
|
# --- ci.yml: the monotonic step is actually wired (#98) ----------------------
|
||||||
# The guard runs from ci.yml, not from this suite, so pin the wiring the same
|
# The guard runs from ci.yml, not from this suite, so pin the wiring the same
|
||||||
# way release.yml's is pinned — a script nothing invokes is not a check.
|
# way release.yml's is pinned — a script nothing invokes is not a check.
|
||||||
CIY="$ROOT/.github/workflows/ci.yml"
|
CIY="$ROOT/.github/workflows/ci.yml"
|
||||||
check "ci.yml: runs the monotonic guard" 0 "" \
|
check "ci.yml: runs the monotonic guard" 0 "" \
|
||||||
grep -q "changelog-monotonic.sh" "$CIY"
|
grep -q "changelog-monotonic.sh" "$CIY"
|
||||||
check "ci.yml: ...on pull requests only (on a push to main it is vacuous)" 0 "" \
|
|
||||||
grep -qF "github.event_name == 'pull_request'" "$CIY"
|
|
||||||
check "ci.yml: ...with STRICT=1, so a skip is red rather than quietly green" 0 "" \
|
check "ci.yml: ...with STRICT=1, so a skip is red rather than quietly green" 0 "" \
|
||||||
grep -qF "CHANGELOG_MONOTONIC_STRICT: '1'" "$CIY"
|
grep -qF "CHANGELOG_MONOTONIC_STRICT: '1'" "$CIY"
|
||||||
# shellcheck disable=SC2016 # the $-string is a literal in the target file
|
# shellcheck disable=SC2016 # the $-string is a literal in the target file
|
||||||
check "ci.yml: ...against the PR's base branch" 0 "" \
|
check "ci.yml: ...against the PR's base branch" 0 "" \
|
||||||
grep -qF 'origin/${{ github.base_ref }}' "$CIY"
|
grep -qF 'origin/${{ github.base_ref' "$CIY"
|
||||||
|
# The step must NOT be pull-request-only. Deletion is vacuous on a push to main
|
||||||
|
# (the merge base IS HEAD), but DUPLICATION is vacuous on no tree at all, so
|
||||||
|
# gating the whole script left a duplicate reaching main by any other route
|
||||||
|
# unasserted. Dropping the gate is only safe with the ref_name fallback:
|
||||||
|
# `github.base_ref` is EMPTY on a push, a bare `origin/` does not resolve, and
|
||||||
|
# STRICT=1 promotes that to a hard failure on every push to main.
|
||||||
|
check "ci.yml: the monotonic step is NOT gated to pull_request (#98)" 1 "" \
|
||||||
|
grep -qF "if: github.event_name == 'pull_request'" "$CIY"
|
||||||
|
# shellcheck disable=SC2016 # the $-string is a literal in the target file
|
||||||
|
check "ci.yml: ...and falls back to ref_name, so a push has a base to resolve" 0 "" \
|
||||||
|
grep -qF 'github.base_ref || github.ref_name' "$CIY"
|
||||||
# Without full history the base ref does not resolve, and STRICT turns that
|
# Without full history the base ref does not resolve, and STRICT turns that
|
||||||
# into a red run — so the fetch depth is load-bearing, not incidental.
|
# into a red run — so the fetch depth is load-bearing, not incidental.
|
||||||
check "ci.yml: the checkout has full history (the base ref must resolve)" 0 "" \
|
check "ci.yml: the checkout has full history (the base ref must resolve)" 0 "" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue