fix: resume a stranded merge-door release #274
9 changed files with 354 additions and 64 deletions
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
|
|
@ -46,6 +46,7 @@ scope:release-flow:
|
||||||
- CHANGELOG.md
|
- CHANGELOG.md
|
||||||
- drills/**
|
- drills/**
|
||||||
- test/decide.test.sh
|
- test/decide.test.sh
|
||||||
|
- test/preflight.test.sh
|
||||||
- test/facts.test.sh
|
- test/facts.test.sh
|
||||||
- test/release-chain.test.sh
|
- test/release-chain.test.sh
|
||||||
- test/version.test.sh
|
- test/version.test.sh
|
||||||
|
|
|
||||||
1
.github/scripts/release-path.sh
vendored
1
.github/scripts/release-path.sh
vendored
|
|
@ -19,6 +19,7 @@ printf '%s\n' \
|
||||||
bin/ \
|
bin/ \
|
||||||
lib/version.sh \
|
lib/version.sh \
|
||||||
lib/decide.sh \
|
lib/decide.sh \
|
||||||
|
lib/preflight.sh \
|
||||||
lib/facts.sh \
|
lib/facts.sh \
|
||||||
lib/changelog.sh \
|
lib/changelog.sh \
|
||||||
lib/forge.sh
|
lib/forge.sh
|
||||||
|
|
|
||||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
|
|
@ -205,31 +205,40 @@ jobs:
|
||||||
fi
|
fi
|
||||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||||
cat "$RUNNER_TEMP/notes.md"
|
cat "$RUNNER_TEMP/notes.md"
|
||||||
- name: nothing may exist yet — no tag, no release (re-runs refuse loudly)
|
- name: preflight — resume this merge, refuse every other collision
|
||||||
|
id: preflight
|
||||||
if: steps.decide.outputs.ceremony == 'yes'
|
if: steps.decide.outputs.ceremony == 'yes'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
VER: ${{ steps.facts.outputs.ver }}
|
VER: ${{ steps.facts.outputs.ver }}
|
||||||
# What makes a re-run of a completed ceremony refuse instead of
|
MERGE_SHA: ${{ github.sha }}
|
||||||
# clobber, and what catches a manual tag racing the merge.
|
# The pure table in lib/preflight.sh distinguishes a stranded run of
|
||||||
|
# this door from a completed release or a tag at another commit (#273).
|
||||||
run: |
|
run: |
|
||||||
if git ls-remote --exit-code origin "refs/tags/$VER" >/dev/null 2>&1; then
|
tag_read_rc=0
|
||||||
echo "tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing." >&2
|
tag_refs="$(git ls-remote --exit-code origin "refs/tags/$VER" "refs/tags/$VER^{}")" || tag_read_rc=$?
|
||||||
|
case "$tag_read_rc" in
|
||||||
|
0) tag_shas="$(printf '%s\n' "$tag_refs" | awk 'NF { print $1 }')" ;;
|
||||||
|
2) tag_shas="" ;;
|
||||||
|
*)
|
||||||
|
echo "could not read tag '$VER' from origin (git ls-remote exit $tag_read_rc) — refusing rather than assuming it does not exist." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
;;
|
||||||
|
esac
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
. "$CEREMONY_DIR/lib/forge.sh"
|
. "$CEREMONY_DIR/lib/forge.sh"
|
||||||
forge_select ""
|
forge_select ""
|
||||||
if ! exists="$(forge_release_exists "$VER")"; then
|
if ! released="$(forge_release_exists "$VER")"; then
|
||||||
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$exists" = yes ]; then
|
# shellcheck source=/dev/null
|
||||||
echo "release '$VER' already exists — refusing to re-release, creating nothing." >&2
|
. "$CEREMONY_DIR/lib/preflight.sh"
|
||||||
exit 1
|
out="$(TAG_SHAS="$tag_shas" RELEASED="$released" release_preflight)"
|
||||||
fi
|
printf '%s\n' "$out"
|
||||||
|
printf '%s\n' "$out" | grep '^resume=' >> "$GITHUB_OUTPUT"
|
||||||
- name: tag the merge commit — same job as the publish, on purpose
|
- name: tag the merge commit — same job as the publish, on purpose
|
||||||
if: steps.decide.outputs.ceremony == 'yes'
|
if: steps.decide.outputs.ceremony == 'yes' && steps.preflight.outputs.resume != 'yes'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
VER: ${{ steps.facts.outputs.ver }}
|
VER: ${{ steps.facts.outputs.ver }}
|
||||||
|
|
|
||||||
64
README.md
64
README.md
|
|
@ -108,7 +108,7 @@ workflow that carries it.
|
||||||
consumer's release, when it is stale.)
|
consumer's release, when it is stale.)
|
||||||
|
|
||||||
**The merge is the ship decision; the tag is transcription.** After the
|
**The merge is the ship decision; the tag is transcription.** After the
|
||||||
merge, [release.yml](.github/workflows/release.yml#L136-L301) asserts its
|
merge, [release.yml](.github/workflows/release.yml#L136-L310) asserts its
|
||||||
way to certainty, tags the merge commit, publishes the forge release with
|
way to certainty, tags the merge commit, publishes the forge release with
|
||||||
the version's own changelog section as the body — the curated prose, never
|
the version's own changelog section as the body — the curated prose, never
|
||||||
the generated PR list ([lib/changelog.sh](lib/changelog.sh) is the one
|
the generated PR list ([lib/changelog.sh](lib/changelog.sh) is the one
|
||||||
|
|
@ -127,12 +127,13 @@ steps run past the tag, and what a failure at each leaves behind is what
|
||||||
sorts them. Two fail before the release exists: the consumer's
|
sorts them. Two fail before the release exists: the consumer's
|
||||||
[artifact hook](docs/CONSUMERS.md#the-artifact-hook) sits between the tag and
|
[artifact hook](docs/CONSUMERS.md#the-artifact-hook) sits between the tag and
|
||||||
the publish, so its non-zero exit aborts, and the publish itself
|
the publish, so its non-zero exit aborts, and the publish itself
|
||||||
([`forge_release_create`](.github/workflows/release.yml#L255-L268))
|
([`forge_release_create`](.github/workflows/release.yml#L264-L277))
|
||||||
can fail on the API call or the assets. Either leaves the same state — a tag
|
can fail on the API call or the assets. Either leaves the same state — a tag
|
||||||
standing and no release — which the
|
standing and no release — which the merge-door preflight recognizes and a
|
||||||
[nothing-exists assert](#the-merge-door-refused-releaseyml) names and the tag
|
re-run resumes. The tag door remains the fallback when the original run is no
|
||||||
door recovers. The third is the re-arm, which runs after the publish, and its
|
longer reachable or the release must come from a fixed tree. The third is the
|
||||||
refusal is the single failure in this file that leaves a real release behind.
|
re-arm, which runs after the publish, and its refusal is the single failure in
|
||||||
|
this file that leaves a real release behind.
|
||||||
|
|
||||||
## The two doors
|
## The two doors
|
||||||
|
|
||||||
|
|
@ -141,20 +142,20 @@ refusal is the single failure in this file that leaves a real release behind.
|
||||||
`release`-labeled PR whose version transitioned to bare is the ceremony,
|
`release`-labeled PR whose version transitioned to bare is the ceremony,
|
||||||
everything legitimate that isn't one is a green no-op, and every
|
everything legitimate that isn't one is a green no-op, and every
|
||||||
half-ceremony dies loudly
|
half-ceremony dies loudly
|
||||||
([release.yml](.github/workflows/release.yml#L136-L301)). Use it for every
|
([release.yml](.github/workflows/release.yml#L136-L310)). Use it for every
|
||||||
normal release.
|
normal release.
|
||||||
|
|
||||||
- **The tag door — the fallback and the backfill.** A bare `X.Y.Z` tag push
|
- **The tag door — the fallback and the backfill.** A bare `X.Y.Z` tag push
|
||||||
— **no `v` prefix**, box's 0.6.0 set the scheme
|
— **no `v` prefix**, box's 0.6.0 set the scheme
|
||||||
([release.yml](.github/workflows/release.yml#L316-L401)) — publishes the
|
([release.yml](.github/workflows/release.yml#L325-L410)) — publishes the
|
||||||
same way. The tag is the operator's explicit act, so there is no decide
|
same way. The tag is the operator's explicit act, so there is no decide
|
||||||
and no label check — what is left is three asserts: **the tag names the
|
and no label check — what is left is three asserts: **the tag names the
|
||||||
tree's own version**
|
tree's own version**
|
||||||
([L341–L352](.github/workflows/release.yml#L341-L352)), **the tagged
|
([L350–L361](.github/workflows/release.yml#L350-L361)), **the tagged
|
||||||
tree carries a publishable `## X.Y.Z` section**
|
tree carries a publishable `## X.Y.Z` section**
|
||||||
([L353–L365](.github/workflows/release.yml#L353-L365)), and **no published
|
([L362–L374](.github/workflows/release.yml#L362-L374)), and **no published
|
||||||
release already exists for the tag**
|
release already exists for the tag**
|
||||||
([L366–L381](.github/workflows/release.yml#L366-L381)); any failure
|
([L375–L390](.github/workflows/release.yml#L375-L390)); any failure
|
||||||
refuses, creating nothing. No `-dev` bump either
|
refuses, creating nothing. No `-dev` bump either
|
||||||
— the fallback does not rewrite main (cast's precedent). Use it when the
|
— the fallback does not rewrite main (cast's precedent). Use it when the
|
||||||
merge path is red, for backfills, and for the
|
merge path is red, for backfills, and for the
|
||||||
|
|
@ -472,7 +473,7 @@ so this line can only appear when some *other* caller invokes `version_read`
|
||||||
directly with a backend that is neither `file` nor `package-json`. Fix that
|
directly with a backend that is neither `file` nor `package-json`. Fix that
|
||||||
caller.
|
caller.
|
||||||
|
|
||||||
### The merge door refused ([release.yml](.github/workflows/release.yml#L136-L301))
|
### The merge door refused ([release.yml](.github/workflows/release.yml#L136-L310))
|
||||||
|
|
||||||
> CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release
|
> CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release
|
||||||
|
|
||||||
|
|
@ -482,22 +483,21 @@ without its stamp (a state the
|
||||||
the PR — red main here means it was overridden). Stamp the section on main,
|
the PR — red main here means it was overridden). Stamp the section on main,
|
||||||
then publish by the tag door.
|
then publish by the tag door.
|
||||||
|
|
||||||
> tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing.
|
> release '$VER' already exists — this release already happened; refusing to re-release, creating nothing.
|
||||||
> release '$VER' already exists — refusing to re-release, creating nothing.
|
> tag '$VER' already exists at <tag sha> but this run would tag <MERGE_SHA> — a manual tag won the race, or it names a different commit; refusing to re-release, creating nothing. Delete that tag, or re-tag the merge commit.
|
||||||
|
> NOTICE: tag '$VER' already stands at this merge commit and no release exists — a previous run of this door tagged and then failed to publish. Resuming: the tag is not recreated; the artifact hook and the publish run.
|
||||||
|
|
||||||
[L208–L223](.github/workflows/release.yml#L208-L223), the nothing-exists
|
[L208–L239](.github/workflows/release.yml#L208-L239), the merge-door
|
||||||
assert — what makes a re-run of a completed ceremony refuse instead of
|
preflight — the published-release refusal prevents clobbering, the
|
||||||
clobber, and what catches a manual tag racing the merge. If the release
|
different-commit refusal diagnoses a racing or manual tag with both SHAs, and
|
||||||
truly exists, there is nothing to do: this red is the system declining to do
|
the notice resumes this door after its tag succeeded but the artifact hook or
|
||||||
the thing twice. If the tag exists but the release does not (a manual tag
|
publish failed. Re-run the merge-door job first. If that run is no longer
|
||||||
won the race, or
|
reachable or the tree itself needs repair, use the tag-door fallback: delete
|
||||||
[a failed artifact hook](docs/CONSUMERS.md#the-artifact-hook), or the publish
|
and re-push the tag from the fixed tree, or run `forge_release_create` by hand.
|
||||||
step itself failing after the tag), recover by the tag door: delete and
|
|
||||||
re-push the tag, or run `forge_release_create` by hand from a fixed tree.
|
|
||||||
|
|
||||||
> direct push refused (branch protection?) — opening the bump PR instead
|
> direct push refused (branch protection?) — opening the bump PR instead
|
||||||
|
|
||||||
[L293–L301](.github/workflows/release.yml#L293-L301) — loud, but not a
|
[L302–L310](.github/workflows/release.yml#L302-L310) — loud, but not a
|
||||||
refusal: the post-release `-dev` bump could not push directly, so the run
|
refusal: the post-release `-dev` bump could not push directly, so the run
|
||||||
opened a `release`-labeled bump PR itself. Your move: merge it promptly —
|
opened a `release`-labeled bump PR itself. Your move: merge it promptly —
|
||||||
until it lands, main is sitting bare, where a dev install impersonates the
|
until it lands, main is sitting bare, where a dev install impersonates the
|
||||||
|
|
@ -505,35 +505,35 @@ release and the
|
||||||
[armed guard's window](#changelog-armed--main-never-sits-disarmed) stays
|
[armed guard's window](#changelog-armed--main-never-sits-disarmed) stays
|
||||||
open.
|
open.
|
||||||
|
|
||||||
### The tag door refused ([release.yml](.github/workflows/release.yml#L316-L401))
|
### The tag door refused ([release.yml](.github/workflows/release.yml#L325-L410))
|
||||||
|
|
||||||
> tag '$GITHUB_REF_NAME' does not match the tree's version '$ver' — creating nothing.
|
> tag '$GITHUB_REF_NAME' does not match the tree's version '$ver' — creating nothing.
|
||||||
> A release is a PR, then a tag: the release PR bumps the version and stamps the changelog; the tag goes on its MERGE commit. Delete this tag and re-tag the right commit.
|
> A release is a PR, then a tag: the release PR bumps the version and stamps the changelog; the tag goes on its MERGE commit. Delete this tag and re-tag the right commit.
|
||||||
|
|
||||||
[L347–L350](.github/workflows/release.yml#L347-L350). The message is the
|
[L356–L359](.github/workflows/release.yml#L356-L359). The message is the
|
||||||
remedy.
|
remedy.
|
||||||
|
|
||||||
> CHANGELOG.md has no '## $VER' section — run changelog-assemble in the release PR before tagging; refusing to publish an empty release
|
> CHANGELOG.md has no '## $VER' section — run changelog-assemble in the release PR before tagging; refusing to publish an empty release
|
||||||
|
|
||||||
[L359–L365](.github/workflows/release.yml#L359-L365). The tagged tree was
|
[L368–L374](.github/workflows/release.yml#L368-L374). The tagged tree was
|
||||||
never stamped. Assemble the section
|
never stamped. Assemble the section
|
||||||
([docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)), then
|
([docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)), then
|
||||||
delete and re-push the tag.
|
delete and re-push the tag.
|
||||||
|
|
||||||
> release '$VER' already exists — refusing to re-release, creating nothing.
|
> release '$VER' already exists — refusing to re-release, creating nothing.
|
||||||
|
|
||||||
[L366–L381](.github/workflows/release.yml#L366-L381). A published release is
|
[L375–L390](.github/workflows/release.yml#L375-L390). A published release is
|
||||||
never replaced by the fallback. If it is correct, there is nothing to do; if
|
never replaced by the fallback. If it is correct, there is nothing to do; if
|
||||||
it is wrong, correct that published artifact deliberately before retrying.
|
it is wrong, correct that published artifact deliberately before retrying.
|
||||||
|
|
||||||
### The re-arm refused ([release.yml](.github/workflows/release.yml#L267-L301))
|
### The re-arm refused ([release.yml](.github/workflows/release.yml#L276-L310))
|
||||||
|
|
||||||
The bump belongs to the merge door alone — the tag door deliberately does not
|
The bump belongs to the merge door alone — the tag door deliberately does not
|
||||||
rewrite main ([L316–L320](.github/workflows/release.yml#L316-L320)) — and it
|
rewrite main ([L325–L329](.github/workflows/release.yml#L325-L329)) — and it
|
||||||
runs *after* the tag, the notes and the publish. So a refusal here leaves a
|
runs *after* the tag, the notes and the publish. So a refusal here leaves a
|
||||||
real release standing behind a main that never re-armed — the release exists,
|
real release standing behind a main that never re-armed — the release exists,
|
||||||
and main is left *armed to impersonate* it, still reading the version it just
|
and main is left *armed to impersonate* it, still reading the version it just
|
||||||
shipped ([L266](.github/workflows/release.yml#L266)). That is the one failure
|
shipped ([L275](.github/workflows/release.yml#L275)). That is the one failure
|
||||||
in this catalog whose remedy is a manual bump, not a re-run.
|
in this catalog whose remedy is a manual bump, not a re-run.
|
||||||
|
|
||||||
> version_next_dev: refusing '$ver' — expected bare X.Y.Z
|
> version_next_dev: refusing '$ver' — expected bare X.Y.Z
|
||||||
|
|
@ -581,7 +581,7 @@ In every case the remedy has the same shape — bump `VERSION` (or the
|
||||||
shipped version was bare, and where it was an rc, whatever you have decided
|
shipped version was bare, and where it was an rc, whatever you have decided
|
||||||
comes next. Note that a *push* refusal is not one of these — branch
|
comes next. Note that a *push* refusal is not one of these — branch
|
||||||
protection is expected, and the step opens the bump PR itself rather than
|
protection is expected, and the step opens the bump PR itself rather than
|
||||||
failing ([L293–L301](.github/workflows/release.yml#L293-L301)).
|
failing ([L302–L310](.github/workflows/release.yml#L302-L310)).
|
||||||
|
|
||||||
### Red main that is not the release workflow
|
### Red main that is not the release workflow
|
||||||
|
|
||||||
|
|
|
||||||
3
changelog.d/273.md
Normal file
3
changelog.d/273.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Merge-door release reruns resume after a matching stranded tag while completed or conflicting releases still refuse with precise diagnostics (#273).
|
||||||
81
lib/preflight.sh
Executable file
81
lib/preflight.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/preflight.sh — the merge door's resume decision, pure and exhaustively
|
||||||
|
# tested (issue #273).
|
||||||
|
#
|
||||||
|
# A merge-door run creates the tag before the artifact hook and release. A
|
||||||
|
# failed hook or publish therefore leaves a tag but no release. Re-running the
|
||||||
|
# same merge commit must resume after that irreversible step; a published
|
||||||
|
# release or a tag naming another commit must still refuse.
|
||||||
|
#
|
||||||
|
# Pure: no repository or forge reads. The workflow establishes four facts:
|
||||||
|
#
|
||||||
|
# VER the version being released
|
||||||
|
# MERGE_SHA the commit this door would tag
|
||||||
|
# TAG_SHAS object names returned for the direct and peeled tag refs,
|
||||||
|
# newline-separated; empty means the tag is absent
|
||||||
|
# RELEASED yes|no — whether a published release for VER exists
|
||||||
|
#
|
||||||
|
# Output: resume=yes or resume=no on stdout, notices to stdout, refusals to
|
||||||
|
# stderr, return 1 on refusal.
|
||||||
|
#
|
||||||
|
# The decision table (this IS the spec — issue #273):
|
||||||
|
#
|
||||||
|
# | # | RELEASED | TAG_SHAS contains MERGE_SHA | result |
|
||||||
|
# |---|----------|------------------------------|---------------------------|
|
||||||
|
# | 1 | yes | either | REFUSE: already released |
|
||||||
|
# | 2 | no | empty | resume=no: ordinary run |
|
||||||
|
# | 3 | no | yes | resume=yes + resume NOTICE |
|
||||||
|
# | 4 | no | non-empty, no | REFUSE: tag is elsewhere |
|
||||||
|
|
||||||
|
release_preflight() {
|
||||||
|
local tag_sha sha
|
||||||
|
|
||||||
|
if [ -z "${VER:-}" ]; then
|
||||||
|
printf '%s\n' "VER is empty — the caller failed to establish the release version. Refusing to decide — creating nothing." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -z "${MERGE_SHA:-}" ]; then
|
||||||
|
printf '%s\n' "MERGE_SHA is empty — the caller failed to establish the merge commit. Refusing to decide — creating nothing." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -z "${RELEASED:-}" ]; then
|
||||||
|
printf '%s\n' "RELEASED is empty — the caller failed to establish whether release '$VER' exists. Refusing to decide — creating nothing." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
case "$RELEASED" in
|
||||||
|
yes | no) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "RELEASED='$RELEASED' — expected yes or no. Refusing to decide — creating nothing." >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Row 1 comes first: deleting a tag under a standing release never makes
|
||||||
|
# that release safe to recreate.
|
||||||
|
if [ "$RELEASED" = yes ]; then
|
||||||
|
printf '%s\n' "release '$VER' already exists — this release already happened; refusing to re-release, creating nothing." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Row 2: an absent tag is the ordinary first run.
|
||||||
|
if [ -z "${TAG_SHAS:-}" ]; then
|
||||||
|
printf '%s\n' 'resume=no'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Row 3: compare each object name as a whole line. For an annotated tag the
|
||||||
|
# direct ref names the tag object and the peeled ref names MERGE_SHA.
|
||||||
|
while IFS= read -r sha; do
|
||||||
|
if [ "$sha" = "$MERGE_SHA" ]; then
|
||||||
|
printf '%s\n' "NOTICE: tag '$VER' already stands at this merge commit and no release exists — a previous run of this door tagged and then failed to publish. Resuming: the tag is not recreated; the artifact hook and the publish run."
|
||||||
|
printf '%s\n' 'resume=yes'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done <<<"$TAG_SHAS"
|
||||||
|
|
||||||
|
# Row 4: the first object name is enough to diagnose the conflicting tag;
|
||||||
|
# MERGE_SHA is printed beside it so the operator sees both sides.
|
||||||
|
tag_sha="${TAG_SHAS%%$'\n'*}"
|
||||||
|
printf '%s\n' "tag '$VER' already exists at $tag_sha but this run would tag $MERGE_SHA — a manual tag won the race, or it names a different commit; refusing to re-release, creating nothing. Delete that tag, or re-tag the merge commit." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
@ -1185,6 +1185,66 @@ release_read 200 '{"id":41,"tag_name":"1.2.3","draft":false}'
|
||||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" >/dev/null 2>&1
|
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" >/dev/null 2>&1
|
||||||
check "a published same-tag release is never deleted" 1 "" grep -q '^DELETE ' "$WRITES"
|
check "a published same-tag release is never deleted" 1 "" grep -q '^DELETE ' "$WRITES"
|
||||||
|
|
||||||
|
# The merge door's preflight step is extracted and executed. Its network
|
||||||
|
# edges are stubbed at the boundary, while the real pure decision library
|
||||||
|
# consumes the gathered facts (#273).
|
||||||
|
MERGE_PREFLIGHT="$TMP/merge-preflight.sh"
|
||||||
|
{
|
||||||
|
printf '%s\n' '#!/usr/bin/env bash' 'set -e'
|
||||||
|
yq -r '.jobs.release-on-merge.steps[] | select(.id == "preflight") | .run' \
|
||||||
|
"$ROOT/.github/workflows/release.yml"
|
||||||
|
} >"$MERGE_PREFLIGHT"
|
||||||
|
chmod +x "$MERGE_PREFLIGHT"
|
||||||
|
|
||||||
|
mkdir -p "$TMP/merge-preflight-lib/lib" "$TMP/merge-preflight-bin"
|
||||||
|
ln -s "$ROOT/lib/preflight.sh" "$TMP/merge-preflight-lib/lib/preflight.sh"
|
||||||
|
# shellcheck disable=SC2016 # expanded when the generated helper runs
|
||||||
|
printf '%s\n' \
|
||||||
|
'forge_select() { :; }' \
|
||||||
|
'forge_release_exists() { case "$RELEASE_EXISTS" in error) return 1 ;; *) echo "$RELEASE_EXISTS" ;; esac; }' \
|
||||||
|
>"$TMP/merge-preflight-lib/lib/forge.sh"
|
||||||
|
# shellcheck disable=SC2016 # expanded when the PATH stub is executed
|
||||||
|
printf '%s\n' \
|
||||||
|
'#!/usr/bin/env bash' \
|
||||||
|
'case "$GIT_LS_REMOTE" in' \
|
||||||
|
' absent) exit 2 ;;' \
|
||||||
|
' error) exit 128 ;;' \
|
||||||
|
' *) printf "%s\n" "$GIT_LS_REMOTE" ;;' \
|
||||||
|
'esac' >"$TMP/merge-preflight-bin/git"
|
||||||
|
chmod +x "$TMP/merge-preflight-bin/git"
|
||||||
|
|
||||||
|
merge_preflight_extracted() { [ "$(wc -l <"$MERGE_PREFLIGHT")" -ge 8 ]; }
|
||||||
|
run_merge_preflight() {
|
||||||
|
: >"$TMP/merge-preflight-output"
|
||||||
|
CEREMONY_DIR="$TMP/merge-preflight-lib" \
|
||||||
|
GITHUB_OUTPUT="$TMP/merge-preflight-output" \
|
||||||
|
PATH="$TMP/merge-preflight-bin:$PATH" \
|
||||||
|
VER=1.2.3 MERGE_SHA=1111111111111111111111111111111111111111 \
|
||||||
|
RELEASE_EXISTS="$1" GIT_LS_REMOTE="$2" "$MERGE_PREFLIGHT"
|
||||||
|
}
|
||||||
|
merge_preflight_output_is() {
|
||||||
|
run_merge_preflight "$1" "$2" >/dev/null && \
|
||||||
|
[ "$(cat "$TMP/merge-preflight-output")" = "$3" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
check "the merge door's preflight is extracted" 0 "" merge_preflight_extracted
|
||||||
|
check "the merge door refuses an unreadable release state" 1 \
|
||||||
|
"refusing rather than assuming" run_merge_preflight error absent
|
||||||
|
check "the merge door refuses an unreadable tag state" 1 \
|
||||||
|
"could not read tag '1.2.3'" run_merge_preflight no error
|
||||||
|
check "ls-remote exit 2 is tag absence and proceeds" 0 "" \
|
||||||
|
merge_preflight_output_is no absent resume=no
|
||||||
|
check "a matching peeled ref records a resume" 0 "" \
|
||||||
|
merge_preflight_output_is no \
|
||||||
|
$'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\trefs/tags/1.2.3\n1111111111111111111111111111111111111111\trefs/tags/1.2.3^{}' \
|
||||||
|
resume=yes
|
||||||
|
tag_step_uses_preflight() {
|
||||||
|
yq -r '.jobs.release-on-merge.steps[] | select(.name | test("tag the merge commit")) | .if' \
|
||||||
|
"$ROOT/.github/workflows/release.yml" | grep -q 'steps.preflight.outputs.resume'
|
||||||
|
}
|
||||||
|
check "the merge-door tag step is conditioned on the preflight output" 0 "" \
|
||||||
|
tag_step_uses_preflight
|
||||||
|
|
||||||
# The tag door's published-release guard is extracted and executed, like the
|
# The tag door's published-release guard is extracted and executed, like the
|
||||||
# other workflow-shell contracts in this repository. The tag itself is this
|
# other workflow-shell contracts in this repository. The tag itself is this
|
||||||
# door's premise, so only a published release refuses; adding the merge door's
|
# door's premise, so only a published release refuses; adding the merge door's
|
||||||
|
|
|
||||||
121
test/preflight.test.sh
Executable file
121
test/preflight.test.sh
Executable file
|
|
@ -0,0 +1,121 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Contract tests for lib/preflight.sh (issue #273) — every row of the
|
||||||
|
# merge-door resume table, offline. set -u, not -e: refusals are behavior for
|
||||||
|
# the harness to inspect.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=test/harness.sh
|
||||||
|
. "$ROOT/test/harness.sh"
|
||||||
|
|
||||||
|
PREFLIGHT="$ROOT/lib/preflight.sh"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
VER=1.2.3
|
||||||
|
MERGE_SHA=1111111111111111111111111111111111111111
|
||||||
|
FOREIGN_SHA=2222222222222222222222222222222222222222
|
||||||
|
|
||||||
|
# preflight <VER> <MERGE_SHA> <TAG_SHAS> <RELEASED> — run the pure decision
|
||||||
|
# with exactly the four gathered facts in its environment.
|
||||||
|
preflight() {
|
||||||
|
# shellcheck disable=SC2016 # PREFLIGHT expands inside the isolated child
|
||||||
|
env VER="$1" MERGE_SHA="$2" TAG_SHAS="$3" RELEASED="$4" \
|
||||||
|
PREFLIGHT="$PREFLIGHT" bash -c '. "$PREFLIGHT"; release_preflight'
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight_stdout() {
|
||||||
|
preflight "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight_stderr() {
|
||||||
|
{ preflight "$@" >/dev/null; } 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
refuses_without_output() {
|
||||||
|
local out rc
|
||||||
|
out="$(preflight "$@" 2>/dev/null)"
|
||||||
|
rc=$?
|
||||||
|
[ "$rc" -eq 1 ] && [ -z "$out" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- the four table rows ----------------------------------------------------
|
||||||
|
|
||||||
|
check "row 1: a published release refuses even with no tag" 1 \
|
||||||
|
"release '$VER' already exists — this release already happened" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "" yes
|
||||||
|
check "row 1: a published release refuses with the matching tag" 1 \
|
||||||
|
"release '$VER' already exists — this release already happened" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "$MERGE_SHA" yes
|
||||||
|
check "row 1: a published release refuses with a foreign tag" 1 \
|
||||||
|
"release '$VER' already exists — this release already happened" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "$FOREIGN_SHA" yes
|
||||||
|
check "row 1: refusal emits no workflow output" 0 "" \
|
||||||
|
refuses_without_output "$VER" "$MERGE_SHA" "$MERGE_SHA" yes
|
||||||
|
|
||||||
|
check "row 2: an ordinary first run proceeds" 0 "resume=no" \
|
||||||
|
preflight_stdout "$VER" "$MERGE_SHA" "" no
|
||||||
|
|
||||||
|
check "row 3: the matching tag resumes" 0 "resume=yes" \
|
||||||
|
preflight_stdout "$VER" "$MERGE_SHA" "$MERGE_SHA" no
|
||||||
|
check "row 3: resume notice names the previous failed publish" 0 \
|
||||||
|
"a previous run of this door tagged and then failed to publish" \
|
||||||
|
preflight_stdout "$VER" "$MERGE_SHA" "$MERGE_SHA" no
|
||||||
|
check "row 3: an annotated tag resumes when the peeled ref matches" 0 \
|
||||||
|
"resume=yes" preflight_stdout "$VER" "$MERGE_SHA" \
|
||||||
|
"$FOREIGN_SHA"$'\n'"$MERGE_SHA" no
|
||||||
|
check "row 3: an annotated tag resumes when the direct ref matches" 0 \
|
||||||
|
"resume=yes" preflight_stdout "$VER" "$MERGE_SHA" \
|
||||||
|
"$MERGE_SHA"$'\n'"$FOREIGN_SHA" no
|
||||||
|
|
||||||
|
check "row 4: a foreign tag refuses" 1 "tag '$VER' already exists" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "$FOREIGN_SHA" no
|
||||||
|
check "row 4: the refusal names the foreign tag SHA" 1 "$FOREIGN_SHA" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "$FOREIGN_SHA" no
|
||||||
|
check "row 4: the refusal names the merge SHA" 1 "$MERGE_SHA" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "$FOREIGN_SHA" no
|
||||||
|
check "row 4: refusal emits no workflow output" 0 "" \
|
||||||
|
refuses_without_output "$VER" "$MERGE_SHA" "$FOREIGN_SHA" no
|
||||||
|
|
||||||
|
# A ref object that merely contains MERGE_SHA is not the merge commit. Each
|
||||||
|
# ls-remote object name is compared as a whole line.
|
||||||
|
PREFIX_SHA="${MERGE_SHA%?}"
|
||||||
|
check "a prefix of MERGE_SHA does not resume" 1 "already exists at $PREFIX_SHA" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "$PREFIX_SHA" no
|
||||||
|
check "a line containing MERGE_SHA does not resume" 1 \
|
||||||
|
"already exists at x${MERGE_SHA}y" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "x${MERGE_SHA}y" no
|
||||||
|
|
||||||
|
# --- fact validation --------------------------------------------------------
|
||||||
|
|
||||||
|
check "empty VER refuses" 1 "VER is empty" \
|
||||||
|
preflight_stderr "" "$MERGE_SHA" "" no
|
||||||
|
check "empty MERGE_SHA refuses" 1 "MERGE_SHA is empty" \
|
||||||
|
preflight_stderr "$VER" "" "" no
|
||||||
|
check "empty RELEASED refuses" 1 "RELEASED is empty" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "" ""
|
||||||
|
check "malformed RELEASED refuses" 1 "RELEASED='maybe' — expected yes or no" \
|
||||||
|
preflight_stderr "$VER" "$MERGE_SHA" "" maybe
|
||||||
|
|
||||||
|
# --- stream discipline and purity ------------------------------------------
|
||||||
|
|
||||||
|
notice_stays_on_stdout() {
|
||||||
|
local stdout stderr
|
||||||
|
stdout="$(preflight "$VER" "$MERGE_SHA" "$MERGE_SHA" no 2>"$TMP/preflight.err")"
|
||||||
|
stderr="$(cat "$TMP/preflight.err")"
|
||||||
|
[ -n "$stdout" ] && [ -z "$stderr" ]
|
||||||
|
}
|
||||||
|
refusal_stays_on_stderr() {
|
||||||
|
local stdout stderr rc
|
||||||
|
stdout="$(preflight "$VER" "$MERGE_SHA" "$FOREIGN_SHA" no 2>"$TMP/preflight.err")"
|
||||||
|
rc=$?
|
||||||
|
stderr="$(cat "$TMP/preflight.err")"
|
||||||
|
[ "$rc" -eq 1 ] && [ -z "$stdout" ] && [ -n "$stderr" ]
|
||||||
|
}
|
||||||
|
no_tool_calls() {
|
||||||
|
! grep -v '^[[:space:]]*#' "$PREFLIGHT" | grep -Ewq 'git|gh|curl|wget'
|
||||||
|
}
|
||||||
|
check "resume notice and output stay on stdout" 0 "" notice_stays_on_stdout
|
||||||
|
check "refusal stays on stderr" 0 "" refusal_stays_on_stderr
|
||||||
|
check "preflight calls no git/gh/network tools" 0 "" no_tool_calls
|
||||||
|
|
||||||
|
summary
|
||||||
|
|
@ -97,6 +97,17 @@ path_check() {
|
||||||
[ -z "$missing" ] && [ -z "$extra" ]
|
[ -z "$missing" ] && [ -z "$extra" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path_check_reports_only() {
|
||||||
|
local tree="$1" expected="$2" output rc
|
||||||
|
output="$(path_check "$tree" 2>&1)"
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -ne 1 ] || [ "$output" != "$expected" ]; then
|
||||||
|
printf 'expected only: %s\ngot (exit %s): %s\n' \
|
||||||
|
"$expected" "$rc" "$output" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
readme_has_no_path_enumeration() {
|
readme_has_no_path_enumeration() {
|
||||||
local token found=no
|
local token found=no
|
||||||
for token in \
|
for token in \
|
||||||
|
|
@ -119,6 +130,7 @@ fixture() {
|
||||||
"\$ROOT/lib/changelog.sh" >"$tree/bin/assemble"
|
"\$ROOT/lib/changelog.sh" >"$tree/bin/assemble"
|
||||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/changelog.sh"
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/changelog.sh"
|
||||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/decide.sh"
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/decide.sh"
|
||||||
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/preflight.sh"
|
||||||
# facts.sh sources BOTH on this tree: version.sh, and the forge shim #191
|
# facts.sh sources BOTH on this tree: version.sh, and the forge shim #191
|
||||||
# put on the doors' path so a Forgejo consumer can publish (#198). The
|
# put on the doors' path so a Forgejo consumer can publish (#198). The
|
||||||
# synthetic tree mirrors the real one, or every fixture below reports
|
# synthetic tree mirrors the real one, or every fixture below reports
|
||||||
|
|
@ -134,7 +146,7 @@ fixture() {
|
||||||
|
|
||||||
# Exact output is the record author's copy-paste source.
|
# Exact output is the record author's copy-paste source.
|
||||||
check "manifest prints the specified ordered release path" 0 \
|
check "manifest prints the specified ordered release path" 0 \
|
||||||
$'.github/workflows/release.yml\nbin/\nlib/version.sh\nlib/decide.sh\nlib/facts.sh\nlib/changelog.sh\nlib/forge.sh' \
|
$'.github/workflows/release.yml\nbin/\nlib/version.sh\nlib/decide.sh\nlib/preflight.sh\nlib/facts.sh\nlib/changelog.sh\nlib/forge.sh' \
|
||||||
bash "$PATH_SCRIPT"
|
bash "$PATH_SCRIPT"
|
||||||
check "real workflow and transitive dependencies match the manifest" 0 "" \
|
check "real workflow and transitive dependencies match the manifest" 0 "" \
|
||||||
path_check "$ROOT"
|
path_check "$ROOT"
|
||||||
|
|
@ -143,54 +155,56 @@ check "drill doctrine does not duplicate the executable release path" 0 "" \
|
||||||
|
|
||||||
# A door growing a dependency must name the missing path (#237 D7).
|
# A door growing a dependency must name the missing path (#237 D7).
|
||||||
tree="$(fixture missing)"
|
tree="$(fixture missing)"
|
||||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||||
"\$CEREMONY_DIR/lib/changelog.sh" "\$CEREMONY_DIR/lib/version.sh" \
|
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||||
"\$CEREMONY_DIR/lib/ruling.sh" \
|
"\$CEREMONY_DIR/lib/version.sh" "\$CEREMONY_DIR/lib/ruling.sh" \
|
||||||
>"$tree/.github/workflows/release.yml"
|
>"$tree/.github/workflows/release.yml"
|
||||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
||||||
check "a new workflow library fails with its missing path" 1 \
|
check "a new workflow library fails with only its missing path" 0 "" \
|
||||||
"missing dependency: lib/ruling.sh" path_check "$tree"
|
path_check_reports_only "$tree" \
|
||||||
|
"release-path: missing dependency: lib/ruling.sh"
|
||||||
|
|
||||||
# A library growing a sibling dependency in the production idiom must also
|
# A library growing a sibling dependency in the production idiom must also
|
||||||
# name the missing path; a literal lib/ marker in a comment is not evidence.
|
# name the missing path; a literal lib/ marker in a comment is not evidence.
|
||||||
tree="$(fixture missing-transitive)"
|
tree="$(fixture missing-transitive)"
|
||||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
|
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||||
"\$CEREMONY_DIR/lib/changelog.sh" \
|
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||||
>"$tree/.github/workflows/release.yml"
|
>"$tree/.github/workflows/release.yml"
|
||||||
printf '# shellcheck source=lib/ruling.sh\n. "%s"\n' \
|
printf '# shellcheck source=lib/ruling.sh\n. "%s"\n' \
|
||||||
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/ruling.sh" \
|
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/ruling.sh" \
|
||||||
>>"$tree/lib/facts.sh"
|
>>"$tree/lib/facts.sh"
|
||||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
||||||
check "a new sibling library fails with its missing path" 1 \
|
check "a new sibling library fails with only its missing path" 0 "" \
|
||||||
"missing dependency: lib/ruling.sh" path_check "$tree"
|
path_check_reports_only "$tree" \
|
||||||
|
"release-path: missing dependency: lib/ruling.sh"
|
||||||
|
|
||||||
# A manifest may not rot into a safe-looking superset.
|
# A manifest may not rot into a safe-looking superset.
|
||||||
tree="$(fixture extra)"
|
tree="$(fixture extra)"
|
||||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
|
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||||
"\$CEREMONY_DIR/lib/changelog.sh" \
|
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||||
>"$tree/.github/workflows/release.yml"
|
>"$tree/.github/workflows/release.yml"
|
||||||
sed -i 's| lib/forge.sh$| lib/forge.sh \\|' \
|
sed -i '$ s|$| \\|' \
|
||||||
"$tree/.github/scripts/release-path.sh"
|
"$tree/.github/scripts/release-path.sh"
|
||||||
printf ' lib/ruling.sh\n' >>"$tree/.github/scripts/release-path.sh"
|
printf ' lib/ruling.sh\n' >>"$tree/.github/scripts/release-path.sh"
|
||||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
||||||
check "a path no door reads fails as stale" 1 "stale path: lib/ruling.sh" \
|
check "a path no door reads fails with only its stale path" 0 "" \
|
||||||
path_check "$tree"
|
path_check_reports_only "$tree" "release-path: stale path: lib/ruling.sh"
|
||||||
|
|
||||||
# Transitive sourcing is part of the derivation, not decoration.
|
# Transitive sourcing is part of the derivation, not decoration.
|
||||||
tree="$(fixture transitive)"
|
tree="$(fixture transitive)"
|
||||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
|
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||||
"\$CEREMONY_DIR/lib/changelog.sh" \
|
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||||
>"$tree/.github/workflows/release.yml"
|
>"$tree/.github/workflows/release.yml"
|
||||||
# Only the version source is dropped; the forge source #191 added stays, or
|
# Only the version source is dropped; the forge source #191 added stays, or
|
||||||
# the fixture reports two stale paths and proves neither of them (#198).
|
# the fixture reports two stale paths and proves neither of them (#198).
|
||||||
printf '#!/usr/bin/env bash\n# shellcheck source=lib/forge.sh\n. "%s"\n' \
|
printf '#!/usr/bin/env bash\n# shellcheck source=lib/forge.sh\n. "%s"\n' \
|
||||||
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/forge.sh" \
|
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/forge.sh" \
|
||||||
>"$tree/lib/facts.sh"
|
>"$tree/lib/facts.sh"
|
||||||
check "removing facts' version source fails as a stale path" 1 \
|
check "removing facts' version source fails with only its stale path" 0 "" \
|
||||||
"stale path: lib/version.sh" path_check "$tree"
|
path_check_reports_only "$tree" "release-path: stale path: lib/version.sh"
|
||||||
|
|
||||||
summary
|
summary
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue