Compare commits
No commits in common. "main" and "0.6.3" have entirely different histories.
17 changed files with 109 additions and 761 deletions
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
|
|
@ -46,7 +46,6 @@ scope:release-flow:
|
|||
- CHANGELOG.md
|
||||
- drills/**
|
||||
- test/decide.test.sh
|
||||
- test/preflight.test.sh
|
||||
- test/facts.test.sh
|
||||
- test/release-chain.test.sh
|
||||
- test/version.test.sh
|
||||
|
|
|
|||
1
.github/scripts/release-path.sh
vendored
1
.github/scripts/release-path.sh
vendored
|
|
@ -19,7 +19,6 @@ printf '%s\n' \
|
|||
bin/ \
|
||||
lib/version.sh \
|
||||
lib/decide.sh \
|
||||
lib/preflight.sh \
|
||||
lib/facts.sh \
|
||||
lib/changelog.sh \
|
||||
lib/forge.sh
|
||||
|
|
|
|||
57
.github/workflows/release.yml
vendored
57
.github/workflows/release.yml
vendored
|
|
@ -205,40 +205,31 @@ jobs:
|
|||
fi
|
||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
- name: preflight — resume this merge, refuse every other collision
|
||||
id: preflight
|
||||
- name: nothing may exist yet — no tag, no release (re-runs refuse loudly)
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
MERGE_SHA: ${{ github.sha }}
|
||||
# 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).
|
||||
# What makes a re-run of a completed ceremony refuse instead of
|
||||
# clobber, and what catches a manual tag racing the merge.
|
||||
run: |
|
||||
tag_read_rc=0
|
||||
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
|
||||
;;
|
||||
esac
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/forge.sh"
|
||||
forge_select ""
|
||||
if ! released="$(forge_release_exists "$VER")"; then
|
||||
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
||||
if git ls-remote --exit-code origin "refs/tags/$VER" >/dev/null 2>&1; then
|
||||
echo "tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/preflight.sh"
|
||||
out="$(TAG_SHAS="$tag_shas" RELEASED="$released" release_preflight)"
|
||||
printf '%s\n' "$out"
|
||||
printf '%s\n' "$out" | grep '^resume=' >> "$GITHUB_OUTPUT"
|
||||
. "$CEREMONY_DIR/lib/forge.sh"
|
||||
forge_select ""
|
||||
if ! exists="$(forge_release_exists "$VER")"; then
|
||||
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$exists" = yes ]; then
|
||||
echo "release '$VER' already exists — refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: tag the merge commit — same job as the publish, on purpose
|
||||
if: steps.decide.outputs.ceremony == 'yes' && steps.preflight.outputs.resume != 'yes'
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
|
|
@ -372,22 +363,6 @@ jobs:
|
|||
fi
|
||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
- name: no published release may exist
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.assert.outputs.ver }}
|
||||
run: |
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/forge.sh"
|
||||
forge_select ""
|
||||
if ! exists="$(forge_release_exists "$VER")"; then
|
||||
echo "could not read whether release '$VER' exists — refusing rather than assuming it does not (#191)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$exists" = yes ]; then
|
||||
echo "release '$VER' already exists — refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: artifact hook — the consumer's own release-artifact action
|
||||
# After the tag exists (it fired this door), before the publish —
|
||||
# the same contract as the merge door's twin step.
|
||||
|
|
|
|||
72
README.md
72
README.md
|
|
@ -108,8 +108,8 @@ workflow that carries it.
|
|||
consumer's release, when it is stale.)
|
||||
|
||||
**The merge is the ship decision; the tag is transcription.** After the
|
||||
merge, [release.yml](.github/workflows/release.yml#L136-L310) asserts its
|
||||
way to certainty, tags the merge commit, publishes the forge release with
|
||||
merge, [release.yml](.github/workflows/release.yml#L136-L301) asserts its
|
||||
way to certainty, tags the merge commit, publishes the GitHub release with
|
||||
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
|
||||
canonical extractor, and [bin/changelog-section](bin/changelog-section) is
|
||||
|
|
@ -127,13 +127,12 @@ 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
|
||||
[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
|
||||
([`forge_release_create`](.github/workflows/release.yml#L264-L277))
|
||||
([`gh release create --verify-tag`](.github/workflows/release.yml#L246-L258))
|
||||
can fail on the API call or the assets. Either leaves the same state — a tag
|
||||
standing and no release — which the merge-door preflight recognizes and a
|
||||
re-run resumes. The tag door remains the fallback when the original run is no
|
||||
longer reachable or the release must come from a fixed tree. The third is the
|
||||
re-arm, which runs after the publish, and its refusal is the single failure in
|
||||
this file that leaves a real release behind.
|
||||
standing and no release — which the
|
||||
[nothing-exists assert](#the-merge-door-refused-releaseyml) names and the tag
|
||||
door recovers. The third is the 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
|
||||
|
||||
|
|
@ -142,20 +141,18 @@ this file that leaves a real release behind.
|
|||
`release`-labeled PR whose version transitioned to bare is the ceremony,
|
||||
everything legitimate that isn't one is a green no-op, and every
|
||||
half-ceremony dies loudly
|
||||
([release.yml](.github/workflows/release.yml#L136-L310)). Use it for every
|
||||
([release.yml](.github/workflows/release.yml#L136-L301)). Use it for every
|
||||
normal release.
|
||||
|
||||
- **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
|
||||
([release.yml](.github/workflows/release.yml#L325-L410)) — publishes the
|
||||
([release.yml](.github/workflows/release.yml#L303-L371)) — publishes the
|
||||
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 two asserts: **the tag names the
|
||||
tree's own version**
|
||||
([L350–L361](.github/workflows/release.yml#L350-L361)), **the tagged
|
||||
([L328–L339](.github/workflows/release.yml#L328-L339)) and **the tagged
|
||||
tree carries a publishable `## X.Y.Z` section**
|
||||
([L362–L374](.github/workflows/release.yml#L362-L374)), and **no published
|
||||
release already exists for the tag**
|
||||
([L375–L390](.github/workflows/release.yml#L375-L390)); any failure
|
||||
([L340–L352](.github/workflows/release.yml#L340-L352)); either failing
|
||||
refuses, creating nothing. No `-dev` bump either
|
||||
— the fallback does not rewrite main (cast's precedent). Use it when the
|
||||
merge path is red, for backfills, and for the
|
||||
|
|
@ -473,7 +470,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
|
||||
caller.
|
||||
|
||||
### The merge door refused ([release.yml](.github/workflows/release.yml#L136-L310))
|
||||
### The merge door refused ([release.yml](.github/workflows/release.yml#L136-L301))
|
||||
|
||||
> CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release
|
||||
|
||||
|
|
@ -483,21 +480,22 @@ without its stamp (a state the
|
|||
the PR — red main here means it was overridden). Stamp the section on main,
|
||||
then publish by the tag door.
|
||||
|
||||
> release '$VER' already exists — this release already happened; 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.
|
||||
> 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 — refusing to re-release, creating nothing.
|
||||
|
||||
[L208–L239](.github/workflows/release.yml#L208-L239), the merge-door
|
||||
preflight — the published-release refusal prevents clobbering, the
|
||||
different-commit refusal diagnoses a racing or manual tag with both SHAs, and
|
||||
the notice resumes this door after its tag succeeded but the artifact hook or
|
||||
publish failed. Re-run the merge-door job first. If that run is no longer
|
||||
reachable or the tree itself needs repair, use the tag-door fallback: delete
|
||||
and re-push the tag from the fixed tree, or run `forge_release_create` by hand.
|
||||
[L208–L223](.github/workflows/release.yml#L208-L223), the nothing-exists
|
||||
assert — what makes a re-run of a completed ceremony refuse instead of
|
||||
clobber, and what catches a manual tag racing the merge. If the release
|
||||
truly exists, there is nothing to do: this red is the system declining to do
|
||||
the thing twice. If the tag exists but the release does not (a manual tag
|
||||
won the race, or
|
||||
[a failed artifact hook](docs/CONSUMERS.md#the-artifact-hook), or the publish
|
||||
step itself failing after the tag), recover by the tag door: delete and
|
||||
re-push the tag, or `gh release create` by hand from a fixed tree.
|
||||
|
||||
> direct push refused (branch protection?) — opening the bump PR instead
|
||||
|
||||
[L302–L310](.github/workflows/release.yml#L302-L310) — loud, but not a
|
||||
[L293–L301](.github/workflows/release.yml#L293-L301) — loud, but not a
|
||||
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 —
|
||||
until it lands, main is sitting bare, where a dev install impersonates the
|
||||
|
|
@ -505,35 +503,29 @@ release and the
|
|||
[armed guard's window](#changelog-armed--main-never-sits-disarmed) stays
|
||||
open.
|
||||
|
||||
### The tag door refused ([release.yml](.github/workflows/release.yml#L325-L410))
|
||||
### The tag door refused ([release.yml](.github/workflows/release.yml#L303-L371))
|
||||
|
||||
> 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.
|
||||
|
||||
[L356–L359](.github/workflows/release.yml#L356-L359). The message is the
|
||||
[L333–L337](.github/workflows/release.yml#L333-L337). The message is the
|
||||
remedy.
|
||||
|
||||
> CHANGELOG.md has no '## $VER' section — run changelog-assemble in the release PR before tagging; refusing to publish an empty release
|
||||
|
||||
[L368–L374](.github/workflows/release.yml#L368-L374). The tagged tree was
|
||||
[L346–L349](.github/workflows/release.yml#L346-L349). The tagged tree was
|
||||
never stamped. Assemble the section
|
||||
([docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)), then
|
||||
delete and re-push the tag.
|
||||
|
||||
> release '$VER' already exists — refusing to re-release, creating nothing.
|
||||
|
||||
[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
|
||||
it is wrong, correct that published artifact deliberately before retrying.
|
||||
|
||||
### The re-arm refused ([release.yml](.github/workflows/release.yml#L276-L310))
|
||||
### The re-arm refused ([release.yml](.github/workflows/release.yml#L267-L301))
|
||||
|
||||
The bump belongs to the merge door alone — the tag door deliberately does not
|
||||
rewrite main ([L325–L329](.github/workflows/release.yml#L325-L329)) — and it
|
||||
rewrite main ([L303–L307](.github/workflows/release.yml#L303-L307)) — and it
|
||||
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,
|
||||
and main is left *armed to impersonate* it, still reading the version it just
|
||||
shipped ([L275](.github/workflows/release.yml#L275)). That is the one failure
|
||||
shipped ([L266](.github/workflows/release.yml#L266)). That is the one failure
|
||||
in this catalog whose remedy is a manual bump, not a re-run.
|
||||
|
||||
> version_next_dev: refusing '$ver' — expected bare X.Y.Z
|
||||
|
|
@ -581,7 +573,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
|
||||
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
|
||||
failing ([L302–L310](.github/workflows/release.yml#L302-L310)).
|
||||
failing ([L293–L301](.github/workflows/release.yml#L293-L301)).
|
||||
|
||||
### Red main that is not the release workflow
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.6.4-dev
|
||||
0.6.3
|
||||
|
|
|
|||
|
|
@ -270,8 +270,7 @@ set_required_bots() { # the PR author is recused by construction
|
|||
# The state machine. Pure functions over these globals, set per PR:
|
||||
# DRAFT true|false
|
||||
# HEAD_SHA the PR's current head commit
|
||||
# BASE_SHA the PR's base branch head
|
||||
# MERGE_BASE_SHA the PR's merge base (the release-shape guard's ref)
|
||||
# BASE_SHA the PR's base branch head (the release-shape guard's ref)
|
||||
# REQUESTED newline-separated logins with a review currently requested
|
||||
# REVIEWS_JSON JSON array of submitted, gradeable reviews
|
||||
# MERGEABLE MERGEABLE | CONFLICTING | UNKNOWN (GitHub's own verdict)
|
||||
|
|
@ -919,8 +918,7 @@ reconcile_pr() { # $1 = PR number; relies on the globals set from its fetch
|
|||
# Drafts are exempt (the build phase is the builder's); the version
|
||||
# reads cost two API calls and only on PRs missing the label.
|
||||
if [ "$DRAFT" != true ] && ! has_label release; then
|
||||
release_shape_warning "$n" "$(tree_version "$HEAD_SHA")" \
|
||||
"$(tree_version "${MERGE_BASE_SHA:-$BASE_SHA}")"
|
||||
release_shape_warning "$n" "$(tree_version "$HEAD_SHA")" "$(tree_version "$BASE_SHA")"
|
||||
fi
|
||||
|
||||
# ---- merge-next: cleared, never set ----------------------------------
|
||||
|
|
@ -1033,7 +1031,6 @@ main() {
|
|||
set_required_bots "$AUTHOR"
|
||||
HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")"
|
||||
BASE_SHA="$(jq -r '.base.sha' <<<"$PR_JSON")"
|
||||
MERGE_BASE_SHA="$(jq -r '.merge_base // empty' <<<"$PR_JSON")"
|
||||
LABELS="$(jq -r '.labels[].name' <<<"$PR_JSON")"
|
||||
# This allow-list answers whether a row is a submitted, gradeable review;
|
||||
# bot_verdict separately answers what that submitted verdict says (#235).
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
### Fixed
|
||||
|
||||
- The upstream deferral record now names its floor and a dated measurement instead of a frozen `0.7.4` ceiling that expired before it was written (#269).
|
||||
|
||||
### Changed
|
||||
|
||||
- `docs/UPSTREAM-SYNC.md` now records unconditionally that the next sync campaign merges rather than ports, and so advances the ancestry baseline (#269).
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
### Fixed
|
||||
|
||||
- Forgejo release publishing now stages drafts until assets upload, rolls back failures, and recovers stranded same-tag drafts before retrying (#271).
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
### Fixed
|
||||
|
||||
- Merge-door release reruns resume after a matching stranded tag while completed or conflicting releases still refuse with precise diagnostics (#273).
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
### Fixed
|
||||
|
||||
- Compare release-shaped pull requests with their merge base so later base-branch releases do not create phantom version-change warnings (#275).
|
||||
|
|
@ -30,33 +30,10 @@ The ancestry baseline therefore remains the full `.upstream-ref` value
|
|||
`upstream-0.6.x` for upstream's line and bare `0.6.x` for releases cut on this
|
||||
forge.
|
||||
|
||||
Upstream's drill-record fixes and the upstream `0.7.x` line remain deferred to
|
||||
the next sync campaign. That line has no ceiling this file can hold still —
|
||||
upstream tags roughly one release a week — so what is recorded here is its
|
||||
floor and the date it was last measured: `0.7.0` onward, `0.7.6` newest as
|
||||
measured 2026-08-27. Run `git ls-remote --tags` for today's ceiling. The
|
||||
sentence this replaced froze `0.7.4` and was already a release behind on the
|
||||
day it was written.
|
||||
|
||||
## The next campaign merges
|
||||
|
||||
The next sync campaign is a merge, not a port, and it therefore advances
|
||||
`.upstream-ref` to the commit it merges. The operator ruled this on #268 on
|
||||
2026-08-27, when release-init found no forge-local work to fill an `0.6.4` and
|
||||
opened no window.
|
||||
|
||||
The reason is the ancestry baseline. `.upstream-ref` has been pinned at
|
||||
`8c3a4d1` — upstream `0.6.0`, merged by #198 — because 0.6.2 came across as a
|
||||
port and a port moves no ancestry. Another port would leave it pinned and
|
||||
guarantee a third, larger campaign against a delta that is still growing.
|
||||
|
||||
Size it before starting. With upstream's tags fetched read-only per step 1,
|
||||
`git diff --shortstat 8c3a4d1dee2bdb5ac06a632a285bb65ab2615214 0.7.6`
|
||||
was 92 files and +25,121/-971 over 488 commits when measured 2026-08-27, and
|
||||
the three action scripts the forge delta is heaviest in all move:
|
||||
`runner-isolated` +1619, `issueflow-reconcile` +913, `labels-reconcile` +889.
|
||||
Step 4 of the procedure below — the audit of what merged *without* conflicting
|
||||
— is where that size is actually paid for.
|
||||
Upstream's drill-record fixes and the upstream `0.7.0` through `0.7.4` line
|
||||
remain deferred to the next sync campaign. If that campaign merges rather
|
||||
than ports, it is the campaign that advances `.upstream-ref`; another port
|
||||
leaves the ancestry baseline unchanged.
|
||||
|
||||
## The standing resolutions
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ forgejo_api_base() {
|
|||
# THE TRAP, measured on this instance with REPO unset (#191, caught by
|
||||
# @kimi on !193 before it shipped):
|
||||
#
|
||||
# forge_release_exists 0.4.1 -> "no", rc 0 (repos//releases/tags/0.4.1
|
||||
# 404s; a repo-less path read
|
||||
# as "the published release
|
||||
# does not exist" — and the
|
||||
# nothing-exists assert would
|
||||
# then proceed to CREATE)
|
||||
# forge_release_exists 0.4.1 -> "no", rc 0 (repos//releases/tags/0.4.1
|
||||
# 404s; a repo-less path read
|
||||
# as "the release does not
|
||||
# exist" — and the
|
||||
# nothing-exists assert would
|
||||
# then proceed to CREATE)
|
||||
# forge_commit_pulls <sha> -> "[]", rc 0 (a commit that HAS a merged
|
||||
# PR behind it, read as none)
|
||||
#
|
||||
|
|
@ -672,31 +672,22 @@ forgejo_read_code() {
|
|||
rm -f "$hdr"
|
||||
}
|
||||
|
||||
# forge_release_exists <tag> — prints `yes` for a published release and `no`
|
||||
# for a draft or a 404. A non-zero exit means the read did not complete and
|
||||
# the answer is UNKNOWN; the caller must not treat that as `no` (#191, #271).
|
||||
# forge_release_exists <tag> — prints `yes` or `no`. A non-zero exit means
|
||||
# the read did not complete and the answer is UNKNOWN; the caller must not
|
||||
# treat that as `no` (#191).
|
||||
forge_release_exists() {
|
||||
local tag="${1:?forge_release_exists: tag required}" body code draft
|
||||
local tag="${1:?forge_release_exists: tag required}" body code
|
||||
body="$(mktemp)"
|
||||
code="$(forgejo_read_code "repos/$REPO/releases/tags/$tag" "$body")" || { rm -f "$body"; return 1; }
|
||||
rm -f "$body"
|
||||
case "$code" in
|
||||
2*)
|
||||
if ! draft="$(jq -r 'if has("draft") then .draft else false end' "$body" 2>/dev/null)" \
|
||||
|| [[ "$draft" != true && "$draft" != false ]]; then
|
||||
rm -f "$body"
|
||||
echo "forge_release_exists: unreadable draft state for release '$tag' — the answer is unknown, not 'no'" >&2
|
||||
return 1
|
||||
fi
|
||||
[ "$draft" = true ] && echo no || echo yes
|
||||
;;
|
||||
2*) echo yes ;;
|
||||
404) echo no ;;
|
||||
*)
|
||||
rm -f "$body"
|
||||
echo "forge_release_exists: HTTP $code reading release '$tag' — the answer is unknown, not 'no'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
rm -f "$body"
|
||||
}
|
||||
|
||||
# forge_commit_pulls <sha> — the pull requests whose merge produced <sha>, as
|
||||
|
|
@ -774,74 +765,20 @@ forgejo_urlencode() {
|
|||
jq -rn --arg s "${1-}" '$s|@uri'
|
||||
}
|
||||
|
||||
# forgejo_release_cleanup_draft <id> <tag> — best-effort rollback after a
|
||||
# post-create failure. The caller has already printed the original failure;
|
||||
# cleanup can add evidence but must never replace that diagnosis (#271).
|
||||
forgejo_release_cleanup_draft() {
|
||||
local id="${1:?forgejo_release_cleanup_draft: id required}"
|
||||
local tag="${2:?forgejo_release_cleanup_draft: tag required}" cleanup
|
||||
if ! cleanup="$(forgejo_write DELETE "repos/$REPO/releases/$id" '' 2>&1)"; then
|
||||
[ -z "$cleanup" ] || printf '%s\n' "$cleanup" >&2
|
||||
echo "forge_release_create: cleanup failed; stranded draft $id for tag '$tag'" >&2
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# forge_release_create <tag> <title> <notes-file> [asset…] — creates a draft,
|
||||
# uploads every asset, then publishes it. Any failure after create removes the
|
||||
# draft, so the function leaves either a complete published release or nothing.
|
||||
# forge_release_create <tag> <title> <notes-file> [asset…] — publishes, then
|
||||
# uploads each asset to the created release. The release id comes back from
|
||||
# the create, so no second lookup is needed.
|
||||
forge_release_create() {
|
||||
local tag="${1:?forge_release_create: tag required}" title="${2:?forge_release_create: title required}"
|
||||
local notes="${3:?forge_release_create: notes file required}" out id base token
|
||||
local existing code draft existing_id
|
||||
shift 3
|
||||
|
||||
# A previous rollback whose DELETE failed must not wedge the next attempt.
|
||||
# Remove only a draft for this exact tag; a published release is never
|
||||
# touched and its create will retain Forgejo's ordinary conflict refusal.
|
||||
existing="$(mktemp)"
|
||||
code="$(forgejo_read_code "repos/$REPO/releases/tags/$tag" "$existing")" || { rm -f "$existing"; return 1; }
|
||||
case "$code" in
|
||||
2*)
|
||||
if ! draft="$(jq -r 'if has("draft") then .draft else false end' "$existing" 2>/dev/null)" \
|
||||
|| [[ "$draft" != true && "$draft" != false ]]; then
|
||||
rm -f "$existing"
|
||||
echo "forge_release_create: unreadable draft state for release '$tag' — refusing to replace it" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ "$draft" = true ]; then
|
||||
existing_id="$(jq -r '.id // empty' "$existing")"
|
||||
if [ -z "$existing_id" ]; then
|
||||
rm -f "$existing"
|
||||
echo "forge_release_create: the stranded draft for tag '$tag' has no release id — refusing to replace it" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "forge_release_create: removing stranded draft $existing_id for tag '$tag' before publish" >&2
|
||||
if ! forgejo_write DELETE "repos/$REPO/releases/$existing_id" '' >/dev/null; then
|
||||
rm -f "$existing"
|
||||
echo "forge_release_create: could not remove stranded draft $existing_id for tag '$tag'" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
404) ;;
|
||||
*)
|
||||
rm -f "$existing"
|
||||
echo "forge_release_create: HTTP $code checking for a stranded draft for tag '$tag' — refusing to publish" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
rm -f "$existing"
|
||||
|
||||
out="$(forgejo_write POST "repos/$REPO/releases" \
|
||||
"$(jq -nc --arg t "$tag" --arg n "$title" --rawfile b "$notes" \
|
||||
'{tag_name:$t,name:$n,body:$b,draft:true,prerelease:false}')")" || return 1
|
||||
'{tag_name:$t,name:$n,body:$b,draft:false,prerelease:false}')")" || return 1
|
||||
id="$(printf '%s' "$out" | jq -r '.id // empty')"
|
||||
[ -n "$id" ] || { echo "forge_release_create: the create returned no release id" >&2; return 1; }
|
||||
if ! base="$(forgejo_api_base)"; then
|
||||
forgejo_release_cleanup_draft "$id" "$tag"
|
||||
return 1
|
||||
fi
|
||||
[ "$#" -gt 0 ] || return 0
|
||||
base="$(forgejo_api_base)" || return 1
|
||||
token="${GH_TOKEN:-${GITHUB_TOKEN:-${FORGEJO_TOKEN:-}}}"
|
||||
local f name
|
||||
for f in "$@"; do
|
||||
|
|
@ -855,16 +792,8 @@ forge_release_create() {
|
|||
curl -sS -f -X POST -H "Authorization: token $token" \
|
||||
-F "attachment=@$f" \
|
||||
"$base/repos/$REPO/releases/$id/assets?name=$name" >/dev/null \
|
||||
|| {
|
||||
echo "forge_release_create: asset upload failed for '$f'" >&2
|
||||
forgejo_release_cleanup_draft "$id" "$tag"
|
||||
return 1
|
||||
}
|
||||
|| { echo "forge_release_create: asset upload failed for '$f'" >&2; return 1; }
|
||||
done
|
||||
if ! forgejo_write PATCH "repos/$REPO/releases/$id" '{"draft":false}' >/dev/null; then
|
||||
forgejo_release_cleanup_draft "$id" "$tag"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# forge_pr_create <head> <base> <title> <body> <label…> — POST /pulls takes
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -952,25 +952,16 @@ check "...and never hits the flat /pulls/{n}/comments endpoint" 1 "" \
|
|||
# tag creation GitHub POST /git/refs
|
||||
# Forgejo POST /tags (/git/refs is GET-only)
|
||||
|
||||
# release_stub <code> <body> [<code> <body>…] — a curl stub answering a
|
||||
# sequence of canned responses and recording the method+path it was asked for.
|
||||
# The cursor lives in a file because forge_release_create captures
|
||||
# forgejo_write in a command substitution; an in-memory counter would advance
|
||||
# only in that subshell and every later request would replay response one.
|
||||
# release_stub <code> <body> — a curl stub answering one canned response and
|
||||
# recording the method+path it was asked for.
|
||||
release_stub() {
|
||||
# Globals, not locals: the curl closure below runs long after this
|
||||
# function returns, exactly as stub_writes does above.
|
||||
STUB_CODES=() STUB_BODIES=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
STUB_CODES+=("$1") STUB_BODIES+=("$2")
|
||||
shift 2
|
||||
done
|
||||
STUB_READ_CODE="${STUB_CODES[0]}" STUB_READ_BODY="${STUB_BODIES[0]}"
|
||||
printf '0\n' >"$TMP/release-stub-cursor"
|
||||
STUB_CODE="$1" STUB_BODY="$2"
|
||||
: >"$WRITES"
|
||||
# shellcheck disable=SC2317 # invoked indirectly, by the forge verbs
|
||||
curl() {
|
||||
local hdr="" out="" method=GET url="" payload="" fail_on_http=false
|
||||
local hdr="" out="" method=GET url="" payload=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-D) hdr="$2"; shift ;;
|
||||
|
|
@ -978,32 +969,18 @@ release_stub() {
|
|||
-X) method="$2"; shift ;;
|
||||
-d) payload="$2"; shift ;;
|
||||
-F) payload="$payload -F $2"; shift ;;
|
||||
-f) fail_on_http=true ;;
|
||||
-H) shift ;;
|
||||
-*) ;;
|
||||
*) url="$1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
local index code body
|
||||
if [ "$method" = GET ]; then
|
||||
code="$STUB_READ_CODE" body="$STUB_READ_BODY"
|
||||
else
|
||||
index="$(cat "$TMP/release-stub-cursor")"
|
||||
code="${STUB_CODES[$index]:-${STUB_CODES[${#STUB_CODES[@]} - 1]}}"
|
||||
body="${STUB_BODIES[$index]:-${STUB_BODIES[${#STUB_BODIES[@]} - 1]}}"
|
||||
printf '%s\n' "$((index + 1))" >"$TMP/release-stub-cursor"
|
||||
fi
|
||||
[ -n "$hdr" ] && printf 'HTTP/1.1 %s x\r\n\r\n' "$code" >"$hdr"
|
||||
[ -n "$out" ] && printf '%s' "$body" >"$out"
|
||||
[ -n "$hdr" ] && printf 'HTTP/1.1 %s x\r\n\r\n' "$STUB_CODE" >"$hdr"
|
||||
[ -n "$out" ] && printf '%s' "$STUB_BODY" >"$out"
|
||||
printf '%s %s %s\n' "$method" "${url##*/api/v1/}" "$payload" >>"$WRITES"
|
||||
if [ "$fail_on_http" = true ]; then
|
||||
case "$code" in 2*) ;; *) return 22 ;; esac
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
}
|
||||
release_read() { STUB_READ_CODE="$1" STUB_READ_BODY="$2"; }
|
||||
|
||||
GITHUB_API_URL=https://forge.example/api/v1
|
||||
export GITHUB_API_URL
|
||||
|
|
@ -1046,10 +1023,6 @@ check "forgejo: a 500 refuses rather than saying 'none'" 1 "the answer is unknow
|
|||
|
||||
release_stub 200 '{"tag_name":"1.2.3"}'
|
||||
check "forgejo: a present release is yes" 0 "yes" forge_release_exists 1.2.3
|
||||
release_stub 200 '{"tag_name":"1.2.3","draft":true}'
|
||||
check "forgejo: a draft is not a present release" 0 "no" forge_release_exists 1.2.3
|
||||
release_stub 200 '{"tag_name":"1.2.3","draft":false}'
|
||||
check "forgejo: a published release is present" 0 "yes" forge_release_exists 1.2.3
|
||||
release_stub 404 '{}'
|
||||
check "forgejo: an absent release is no" 0 "no" forge_release_exists 1.2.3
|
||||
release_stub 503 '{}'
|
||||
|
|
@ -1073,22 +1046,9 @@ check "forgejo: the tag body names the target sha" 0 '"target":"cafebabe"' \
|
|||
writes_after forge_tag_create 1.2.3 cafebabe
|
||||
|
||||
printf 'notes body\n' >"$TMP/notes.md"
|
||||
release_writes_after() { "$@" >/dev/null 2>&1; grep -v '^GET ' "$WRITES"; }
|
||||
|
||||
release_stub 201 '{"id":42}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
release_stub 201 '{"id":42}'
|
||||
check "forgejo: the publish POSTs to /releases with the notes as body" 0 '"body":"notes body' \
|
||||
release_writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md"
|
||||
check "the create stages a draft" 0 '"draft":true' cat "$WRITES"
|
||||
zero_asset_writes() {
|
||||
release_stub 201 '{"id":42}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" >/dev/null 2>&1
|
||||
local got
|
||||
got="$(grep -v '^GET ' "$WRITES" | sed -E 's/ (\{.*|$)//')"
|
||||
[ "$got" = $'POST repos/o/r/releases\nPATCH repos/o/r/releases/42' ] || { printf '%s\n' "$got"; return 1; }
|
||||
}
|
||||
check "the zero-asset path creates then publishes" 0 "" zero_asset_writes
|
||||
writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md"
|
||||
|
||||
# Assets: the hook contract permits any filename the consumer drops in
|
||||
# RELEASE_ASSETS_DIR, and the asset name travels as a QUERY VALUE. Raw
|
||||
|
|
@ -1102,179 +1062,18 @@ check "the encoder escapes the query delimiters" 0 "a%26b%23c%2Bd%25e.tgz" \
|
|||
|
||||
printf 'x\n' >"$TMP/release asset.tgz"
|
||||
printf 'y\n' >"$TMP/a&b.tgz"
|
||||
release_stub 201 '{"id":42}' 201 '{}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
release_stub 201 '{"id":42}'
|
||||
check "an asset with a space uploads under the encoded name" 0 "assets?name=release%20asset.tgz" \
|
||||
release_writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/release asset.tgz"
|
||||
release_stub 201 '{"id":42}' 201 '{}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/release asset.tgz"
|
||||
release_stub 201 '{"id":42}'
|
||||
check "an asset with '&' does not become two parameters" 0 "assets?name=a%26b.tgz" \
|
||||
release_writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
release_stub 201 '{"id":42}' 201 '{}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
release_stub 201 '{"id":42}'
|
||||
check "the upload targets the created release id" 0 "releases/42/assets" \
|
||||
release_writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
release_stub 201 '{"id":42}' 201 '{}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
release_stub 201 '{"id":42}'
|
||||
check "the asset rides as a multipart attachment" 0 "attachment=@" \
|
||||
release_writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
|
||||
happy_asset_writes() {
|
||||
release_stub 201 '{"id":42}' 201 '{}' 201 '{}' 200 '{}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/release asset.tgz" "$TMP/a&b.tgz" >/dev/null 2>&1
|
||||
local got want
|
||||
got="$(grep -v '^GET ' "$WRITES" | sed -E 's/ (\{.*| -F.*|$)//')"
|
||||
want=$'POST repos/o/r/releases\nPOST repos/o/r/releases/42/assets?name=release%20asset.tgz\nPOST repos/o/r/releases/42/assets?name=a%26b.tgz\nPATCH repos/o/r/releases/42'
|
||||
[ "$got" = "$want" ] || { printf '%s\n' "$got"; return 1; }
|
||||
}
|
||||
check "assets upload in argument order before the release is published" 0 "" happy_asset_writes
|
||||
|
||||
upload_failure() {
|
||||
release_stub 201 '{"id":42}' 500 '{}' 204 '{}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
}
|
||||
check "an upload failure returns non-zero" 1 "asset upload failed" upload_failure
|
||||
check "an upload failure deletes the draft" 0 "DELETE repos/o/r/releases/42" cat "$WRITES"
|
||||
|
||||
flip_failure() {
|
||||
release_stub 201 '{"id":42}' 500 '{}' 204 '{}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md"
|
||||
}
|
||||
check "a publish flip failure returns non-zero" 1 "HTTP 500" flip_failure
|
||||
check "a publish flip failure deletes the draft" 0 "DELETE repos/o/r/releases/42" cat "$WRITES"
|
||||
|
||||
create_failure() {
|
||||
release_stub 500 '{}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md"
|
||||
}
|
||||
check "a create failure returns non-zero" 1 "HTTP 500" create_failure
|
||||
check "a create failure has nothing to delete" 1 "" grep -q '^DELETE ' "$WRITES"
|
||||
|
||||
missing_release_id() {
|
||||
release_stub 201 '{}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md"
|
||||
}
|
||||
check "a create without an id keeps its refusal" 1 "returned no release id" missing_release_id
|
||||
check "a create without an id deletes nothing unknown" 1 "" grep -q '^DELETE ' "$WRITES"
|
||||
|
||||
stranded_cleanup_failure() {
|
||||
release_stub 201 '{"id":42}' 500 '{"message":"upload refused"}' 500 '{"message":"delete refused"}'
|
||||
release_read 404 '{}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
}
|
||||
check "cleanup failure preserves the original failure" 1 "asset upload failed" stranded_cleanup_failure
|
||||
check "cleanup failure also names the stranded draft" 1 "stranded draft 42 for tag '1.2.3'" stranded_cleanup_failure
|
||||
|
||||
release_stub 204 '{}' 201 '{"id":42}' 200 '{}'
|
||||
release_read 200 '{"id":41,"tag_name":"1.2.3","draft":true}'
|
||||
forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" >/dev/null 2>&1
|
||||
stranded_draft_order() {
|
||||
local got
|
||||
got="$(grep -v '^GET ' "$WRITES" | head -2 | sed -E 's/ (\{.*|$)//')"
|
||||
[ "$got" = $'DELETE repos/o/r/releases/41\nPOST repos/o/r/releases' ] || { printf '%s\n' "$got"; return 1; }
|
||||
}
|
||||
check "a stranded same-tag draft is deleted before create" 0 "" stranded_draft_order
|
||||
|
||||
release_stub 409 '{}'
|
||||
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
|
||||
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
|
||||
# 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
|
||||
# ls-remote half here would destroy the documented delete-and-re-push recovery.
|
||||
TAG_GUARD="$TMP/tag-release-guard.sh"
|
||||
{
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'set -e'
|
||||
yq -r '.jobs.release-on-tag.steps[] | select(.name == "no published release may exist") | .run' \
|
||||
"$ROOT/.github/workflows/release.yml"
|
||||
} >"$TAG_GUARD"
|
||||
chmod +x "$TAG_GUARD"
|
||||
mkdir -p "$TMP/tag-guard-lib/lib"
|
||||
# shellcheck disable=SC2016 # expanded when the generated helper runs, not while this test writes it
|
||||
printf '%s\n' \
|
||||
'forge_select() { :; }' \
|
||||
'forge_release_exists() { case "$RELEASE_EXISTS" in error) return 1 ;; *) echo "$RELEASE_EXISTS" ;; esac; }' \
|
||||
>"$TMP/tag-guard-lib/lib/forge.sh"
|
||||
|
||||
tag_guard_extracted() { [ "$(wc -l <"$TAG_GUARD")" -ge 8 ]; }
|
||||
run_tag_guard() {
|
||||
CEREMONY_DIR="$TMP/tag-guard-lib" VER=1.2.3 RELEASE_EXISTS="$1" "$TAG_GUARD"
|
||||
}
|
||||
tag_door_has_no_tag_exists_guard() {
|
||||
! yq -r '.jobs.release-on-tag.steps[].run // ""' "$ROOT/.github/workflows/release.yml" | grep -q 'ls-remote'
|
||||
}
|
||||
check "the tag door's published-release guard is extracted" 0 "" tag_guard_extracted
|
||||
check "the tag door refuses an existing published release" 1 "release '1.2.3' already exists" run_tag_guard yes
|
||||
check "the tag door proceeds when no published release exists" 0 "" run_tag_guard no
|
||||
check "the tag door refuses when release state is unreadable" 1 "refusing rather than assuming" run_tag_guard error
|
||||
check "the tag door carries no tag-exists guard" 0 "" tag_door_has_no_tag_exists_guard
|
||||
writes_after forge_release_create 1.2.3 1.2.3 "$TMP/notes.md" "$TMP/a&b.tgz"
|
||||
|
||||
# --- the github twins address their own paths ----------------------------
|
||||
. "$ROOT/lib/forge-github.sh"
|
||||
|
|
|
|||
|
|
@ -175,93 +175,6 @@ expect "an unreadable head version is silent — never nag on a guess" "" \
|
|||
expect "a bare head over an unreadable base still warns" yes \
|
||||
"$(release_shape_warning 41 2.0.0 "" | grep -qF '::warning::' && echo yes || echo no)"
|
||||
|
||||
# The pure matrix above proves the warning predicate. These two fixtures drive
|
||||
# the sweep boundary that chooses WHICH base tree feeds it (#275): the
|
||||
# reporting Forgejo payload exposes the PR's moving base tip beside its fixed
|
||||
# merge base, and using the former manufactures a downgrade after an
|
||||
# intervening release. A missing merge-base field deliberately keeps the
|
||||
# specified base-tip fallback for backends that do not supply that fact.
|
||||
release_shape_ref_probe() { # $1 = phantom | bump | fallback
|
||||
(
|
||||
# shellcheck disable=SC2030 # this probe intentionally isolates its repository fixture
|
||||
REPO=owner/repo
|
||||
LABELS_CONF="$FIXTURE_CONF"
|
||||
CEREMONY_FORGE=github
|
||||
mode="$1"
|
||||
refs="$RTMP/release-shape-$mode-refs"
|
||||
: >"$refs"
|
||||
case "$mode" in
|
||||
phantom)
|
||||
head_ver=1.3.0 base_tip_ver=1.4.0 merge_base_ver=1.3.0
|
||||
merge_base_json='"merge-base"' ;;
|
||||
bump)
|
||||
head_ver=1.4.0 base_tip_ver=1.3.0 merge_base_ver=1.3.0
|
||||
merge_base_json='"merge-base"' ;;
|
||||
fallback)
|
||||
head_ver=1.4.0 base_tip_ver=1.4.0 merge_base_ver=unused
|
||||
merge_base_json=null ;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
# shellcheck disable=SC2317 # reached through the GitHub backend selected above
|
||||
gh() {
|
||||
if [ "$1" = label ] && [ "$2" = list ]; then
|
||||
core_label_rows | cut -d'|' -f1
|
||||
return 0
|
||||
fi
|
||||
if [ "$1" = pr ] && [ "$2" = list ]; then
|
||||
printf '701\n'
|
||||
return 0
|
||||
fi
|
||||
if [ "$1" = pr ] && [ "$2" = view ]; then
|
||||
jq -n '{mergeable:"MERGEABLE",statusCheckRollup:[]}'
|
||||
return 0
|
||||
fi
|
||||
if [ "$1" = issue ] && [ "$2" = edit ]; then return 0; fi
|
||||
case "$(forge_stub_path "$*")" in
|
||||
*'repos/owner/repo/pulls/701 --jq .requested_reviewers'*) return 0 ;;
|
||||
*repos/owner/repo/pulls/701/reviews*) return 0 ;;
|
||||
*repos/owner/repo/pulls/701)
|
||||
jq -n --argjson merge_base "$merge_base_json" \
|
||||
'{draft:false,user:{login:"fixture-builder"},
|
||||
head:{sha:"head"},base:{sha:"base-tip"},merge_base:$merge_base,
|
||||
labels:[{name:"state:addressing"},{name:"blocked"}],
|
||||
requested_reviewers:[],created_at:"2026-08-31T20:00:00Z"}' ;;
|
||||
*repos/owner/repo/commits/head*) printf '2026-08-31T20:00:00Z\n' ;;
|
||||
*repos/owner/repo/contents/VERSION\?ref=head*)
|
||||
printf 'head\n' >>"$refs"
|
||||
printf '%s' "$head_ver" | base64 ;;
|
||||
*repos/owner/repo/contents/VERSION\?ref=base-tip*)
|
||||
printf 'base-tip\n' >>"$refs"
|
||||
printf '%s' "$base_tip_ver" | base64 ;;
|
||||
*repos/owner/repo/contents/VERSION\?ref=merge-base*)
|
||||
printf 'merge-base\n' >>"$refs"
|
||||
printf '%s' "$merge_base_ver" | base64 ;;
|
||||
*) printf '[]\n' ;;
|
||||
esac
|
||||
}
|
||||
main
|
||||
)
|
||||
}
|
||||
|
||||
phantom_shape="$(release_shape_ref_probe phantom)"
|
||||
expect "an unchanged branch cut before a later base release emits no release-shape warning" \
|
||||
no "$(grep -q 'release-shaped' <<<"$phantom_shape" && echo yes || echo no)"
|
||||
expect "the unchanged-branch guard compares head with the PR merge base" \
|
||||
$'head\nmerge-base' "$(cat "$RTMP/release-shape-phantom-refs")"
|
||||
|
||||
bump_shape="$(release_shape_ref_probe bump)"
|
||||
expect "a genuine version bump relative to the merge base keeps the warning text" \
|
||||
yes "$(grep -qF '#701 is release-shaped (version 1.3.0 -> 1.4.0 at its head)' \
|
||||
<<<"$bump_shape" && echo yes || echo no)"
|
||||
expect "the genuine-bump guard also compares head with the PR merge base" \
|
||||
$'head\nmerge-base' "$(cat "$RTMP/release-shape-bump-refs")"
|
||||
|
||||
fallback_shape="$(release_shape_ref_probe fallback)"
|
||||
expect "a null merge base falls back to the base tip without guessing a warning" \
|
||||
no "$(grep -q 'release-shaped' <<<"$fallback_shape" && echo yes || echo no)"
|
||||
expect "the null merge-base fallback compares head with the base tip" \
|
||||
$'head\nbase-tip' "$(cat "$RTMP/release-shape-fallback-refs")"
|
||||
|
||||
# -- drafts are building, whoever is requested --------------------------------
|
||||
DRAFT=true HEAD_SHA=head1 REQUESTED="" REVIEWS_JSON='[]'
|
||||
expect "draft PR is building" state:building "$(decide_state)"
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
#!/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,17 +97,6 @@ path_check() {
|
|||
[ -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() {
|
||||
local token found=no
|
||||
for token in \
|
||||
|
|
@ -130,7 +119,6 @@ fixture() {
|
|||
"\$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/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
|
||||
# put on the doors' path so a Forgejo consumer can publish (#198). The
|
||||
# synthetic tree mirrors the real one, or every fixture below reports
|
||||
|
|
@ -146,7 +134,7 @@ fixture() {
|
|||
|
||||
# Exact output is the record author's copy-paste source.
|
||||
check "manifest prints the specified ordered release path" 0 \
|
||||
$'.github/workflows/release.yml\nbin/\nlib/version.sh\nlib/decide.sh\nlib/preflight.sh\nlib/facts.sh\nlib/changelog.sh\nlib/forge.sh' \
|
||||
$'.github/workflows/release.yml\nbin/\nlib/version.sh\nlib/decide.sh\nlib/facts.sh\nlib/changelog.sh\nlib/forge.sh' \
|
||||
bash "$PATH_SCRIPT"
|
||||
check "real workflow and transitive dependencies match the manifest" 0 "" \
|
||||
path_check "$ROOT"
|
||||
|
|
@ -155,56 +143,54 @@ check "drill doctrine does not duplicate the executable release path" 0 "" \
|
|||
|
||||
# A door growing a dependency must name the missing path (#237 D7).
|
||||
tree="$(fixture missing)"
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
"\$CEREMONY_DIR/lib/version.sh" "\$CEREMONY_DIR/lib/ruling.sh" \
|
||||
"\$CEREMONY_DIR/lib/changelog.sh" "\$CEREMONY_DIR/lib/version.sh" \
|
||||
"\$CEREMONY_DIR/lib/ruling.sh" \
|
||||
>"$tree/.github/workflows/release.yml"
|
||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
||||
check "a new workflow library fails with only its missing path" 0 "" \
|
||||
path_check_reports_only "$tree" \
|
||||
"release-path: missing dependency: lib/ruling.sh"
|
||||
check "a new workflow library fails with its missing path" 1 \
|
||||
"missing dependency: lib/ruling.sh" path_check "$tree"
|
||||
|
||||
# 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.
|
||||
tree="$(fixture missing-transitive)"
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
|
||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
"\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
>"$tree/.github/workflows/release.yml"
|
||||
printf '# shellcheck source=lib/ruling.sh\n. "%s"\n' \
|
||||
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/ruling.sh" \
|
||||
>>"$tree/lib/facts.sh"
|
||||
printf '#!/usr/bin/env bash\n' >"$tree/lib/ruling.sh"
|
||||
check "a new sibling library fails with only its missing path" 0 "" \
|
||||
path_check_reports_only "$tree" \
|
||||
"release-path: missing dependency: lib/ruling.sh"
|
||||
check "a new sibling library fails with its missing path" 1 \
|
||||
"missing dependency: lib/ruling.sh" path_check "$tree"
|
||||
|
||||
# A manifest may not rot into a safe-looking superset.
|
||||
tree="$(fixture extra)"
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
|
||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
"\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
>"$tree/.github/workflows/release.yml"
|
||||
sed -i '$ s|$| \\|' \
|
||||
sed -i 's| lib/forge.sh$| lib/forge.sh \\|' \
|
||||
"$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"
|
||||
check "a path no door reads fails with only its stale path" 0 "" \
|
||||
path_check_reports_only "$tree" "release-path: stale path: lib/ruling.sh"
|
||||
check "a path no door reads fails as stale" 1 "stale path: lib/ruling.sh" \
|
||||
path_check "$tree"
|
||||
|
||||
# Transitive sourcing is part of the derivation, not decoration.
|
||||
tree="$(fixture transitive)"
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\nrun: . "%s"\n' \
|
||||
printf 'run: bash "%s"\nrun: bash "%s"\nrun: . "%s"\n' \
|
||||
"\$CEREMONY_DIR/lib/facts.sh" "\$CEREMONY_DIR/lib/decide.sh" \
|
||||
"\$CEREMONY_DIR/lib/preflight.sh" "\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
"\$CEREMONY_DIR/lib/changelog.sh" \
|
||||
>"$tree/.github/workflows/release.yml"
|
||||
# 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).
|
||||
printf '#!/usr/bin/env bash\n# shellcheck source=lib/forge.sh\n. "%s"\n' \
|
||||
"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)/forge.sh" \
|
||||
>"$tree/lib/facts.sh"
|
||||
check "removing facts' version source fails with only its stale path" 0 "" \
|
||||
path_check_reports_only "$tree" "release-path: stale path: lib/version.sh"
|
||||
check "removing facts' version source fails as a stale path" 1 \
|
||||
"stale path: lib/version.sh" path_check "$tree"
|
||||
|
||||
summary
|
||||
|
|
|
|||
Loading…
Reference in a new issue