Merge pull request #48 from dan-claude-bot/feat/release-on-merge
feat: merging a release-labeled PR is the release
This commit is contained in:
commit
5902f61da4
4 changed files with 320 additions and 22 deletions
210
.github/workflows/release.yml
vendored
210
.github/workflows/release.yml
vendored
|
|
@ -1,25 +1,54 @@
|
|||
name: release
|
||||
# The tag half of the release flow (#32; box#83's design, near-verbatim).
|
||||
# A release is a PR, then a tag: the `release: X.Y.Z` PR bumps VERSION and
|
||||
# stamps CHANGELOG.md's Unreleased section with version + date; after the
|
||||
# merge, the merge commit is tagged bare `X.Y.Z` (no `v` prefix — box's tag
|
||||
# scheme) and the tag is pushed. This workflow turns that tag into the
|
||||
# GitHub release, with the changelog section as the body — the curated
|
||||
# prose, never the auto-generated PR list.
|
||||
# Two ways in, one release out (#47; box#96's design — the merge path — on
|
||||
# top of #32/box#83's tag flow, kept verbatim as the fallback):
|
||||
#
|
||||
# No assets on purpose: for a pure-bash tree, GitHub's source tarball for
|
||||
# the tag IS the package (install.sh downloads archive/refs/tags/<tag>).
|
||||
# - MERGE (the paved road): a release is a PR — `release: X.Y.Z`, carrying
|
||||
# the `release` label, bumping VERSION and stamping CHANGELOG.md's
|
||||
# Unreleased section — and MERGING it is the ship decision. The
|
||||
# release-on-merge job asserts its way to certainty, then tags the merge
|
||||
# commit and publishes, same job. No separate, silent-when-forgotten
|
||||
# tagging step: a forgotten tag produces no red X, a failed run on main
|
||||
# does — of two unreliabilities, pick the loud one.
|
||||
# - TAG PUSH (the manual fallback and backfill): tag the merge commit bare
|
||||
# `X.Y.Z` (no `v` prefix — box's tag scheme) and push; the release job
|
||||
# below turns it into the GitHub release.
|
||||
#
|
||||
# Either way the body is the changelog section — the curated prose, never
|
||||
# the auto-generated PR list — and no assets are uploaded on purpose: for a
|
||||
# pure-bash tree, GitHub's source tarball for the tag IS the package
|
||||
# (install.sh downloads archive/refs/tags/<tag>).
|
||||
on:
|
||||
# ONE push key, both filters — YAML maps are last-key-wins, so a second
|
||||
# sibling `push:` would silently REPLACE the first and kill a door
|
||||
# (grok's round-2 catch: the tag fallback had stopped triggering).
|
||||
push:
|
||||
# Every tag, not a shape filter: a tag that mismatches VERSION must fail
|
||||
# LOUDLY below, not be silently skipped by a pattern that didn't match.
|
||||
tags: ['**']
|
||||
# The merge-is-the-release path (#47) rides pushes to MAIN, not
|
||||
# pull_request events: a pull_request run from a public FORK gets a
|
||||
# READ-ONLY GITHUB_TOKEN — `permissions:` cannot raise that ceiling —
|
||||
# and every ceremony PR this org merges is cross-repo from the bot
|
||||
# fork; the tag create would 403 after green asserts. A push to main
|
||||
# is an in-repo event with the full write token, whoever authored the
|
||||
# PR. The jobs split on the pushed ref.
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write # the tag ref, the release publish, the bump push
|
||||
# Two consumers (a declared permissions: block zeroes every unspecified
|
||||
# scope): the decide step's label read (commits/<sha>/pulls) and the bump
|
||||
# fallback's `gh pr create --label`.
|
||||
pull-requests: write
|
||||
# ...and the --label on that fallback PR rides the ISSUES API (labels.yml
|
||||
# grants the same pair for the same reason).
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
# The tag-push path — a pushed TAG ref. The merge path (a pushed main
|
||||
# head) is release-on-merge below; the two doors split on the ref.
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -46,3 +75,164 @@ jobs:
|
|||
fi
|
||||
gh release create "$GITHUB_REF_NAME" --verify-tag \
|
||||
--title "$GITHUB_REF_NAME" --notes "$notes"
|
||||
|
||||
# The merge path (#47; box#96): the `release` label is the intent, the
|
||||
# VERSION transition is the interlock. Four asserts in order, each
|
||||
# fail-loud and creating NOTHING, then tag + publish in this same job.
|
||||
# Same-job is load-bearing: the tag is created with GITHUB_TOKEN via the
|
||||
# API, and GITHUB_TOKEN-created refs do not fire `on: push: tags`
|
||||
# workflows — so the publish MUST live here (nothing else would run), and
|
||||
# the fallback job above CANNOT double-publish off our tag. A manually
|
||||
# pushed tag racing this run is caught by the nothing-exists assert.
|
||||
# NOTE: test/release.sh pins this block by awk-ing from
|
||||
# 'release-on-merge:' to EOF — keep it the last job.
|
||||
release-on-merge:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MERGE_SHA: ${{ github.sha }}
|
||||
BASE_SHA: ${{ github.event.before }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The pushed head is what ships. Full history so the before-side
|
||||
# VERSION is readable for the interlock below.
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
# The decide step — asserts 1+2 fused, because the `release` label
|
||||
# carries TWO legitimate meanings (LABELS.md: "release flow and
|
||||
# version/packaging work"): the ceremony PR that ships a version, and
|
||||
# ordinary work ON the release machinery — this very PR included.
|
||||
# The version tells them apart. A `-dev` VERSION left UNTOUCHED by the
|
||||
# PR is release-flow work: a green no-op, not a red run on main every
|
||||
# time the flow itself is improved. Everything in between is a
|
||||
# half-ceremony and dies loudly:
|
||||
# -dev, unchanged → work under the label: NOTICE + green no-op
|
||||
# -dev, changed → still a dev tree, so still work — the
|
||||
# post-release bump PR above all (bare -> -dev
|
||||
# after every release): green NOTICE no-op
|
||||
# bare, unchanged,
|
||||
# already released → work merged in the post-release window
|
||||
# (ceremony landed, the -dev bump has not):
|
||||
# NOTICE + green no-op
|
||||
# bare, unchanged,
|
||||
# never released → the label says ship, the tree names an
|
||||
# unshipped version this PR did not mint:
|
||||
# genuinely ambiguous, refuse
|
||||
# bare, changed → the ceremony: proceed
|
||||
- name: 'decide: ceremony, or release-flow work under the label?'
|
||||
id: decide
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ver="$(cat VERSION)"
|
||||
# event.before is all-zeros on a branch-create push; the merge
|
||||
# commit's first parent is main the instant before, either way.
|
||||
if ! git cat-file -e "$BASE_SHA" 2>/dev/null; then BASE_SHA="$(git rev-parse "$MERGE_SHA^1")"; fi
|
||||
base_ver="$(git show "$BASE_SHA:VERSION")"
|
||||
case "$ver" in
|
||||
*-dev)
|
||||
if [ "$base_ver" = "$ver" ]; then
|
||||
echo "NOTICE: VERSION '$ver' is -dev and unchanged by this PR — release-flow work under the release label, not a ceremony. Nothing to publish."
|
||||
echo "ceremony=no" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "NOTICE: VERSION changed ('$base_ver' -> '$ver') and still ends -dev — a dev tree is by definition not a release. This is work (the post-release bump, a renumber); nothing to publish."
|
||||
echo "ceremony=no" >> "$GITHUB_OUTPUT"
|
||||
exit 0 ;;
|
||||
esac
|
||||
if [ "$base_ver" = "$ver" ]; then
|
||||
if gh release view "$ver" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "NOTICE: VERSION '$ver' is already released and unchanged by this PR — release-flow work merged in the post-release window (before the -dev bump). Nothing to publish."
|
||||
echo "ceremony=no" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "VERSION '$ver' is bare, unchanged by this PR, and never released — the label says ship but this PR did not mint the version. Refusing to guess." >&2
|
||||
exit 1
|
||||
fi
|
||||
# The version transitioned — now the LABEL, the operator's declared
|
||||
# intent, read via the API because a push event carries no PR
|
||||
# payload (and the PR itself lives on a fork — see the trigger
|
||||
# comment). No release-labeled PR behind this commit = a version
|
||||
# transition nobody declared: refuse.
|
||||
if ! gh api "repos/$GITHUB_REPOSITORY/commits/$MERGE_SHA/pulls" \
|
||||
-q '[.[] | select(.merged_at != null) | .labels[].name] | index("release") != null' | grep -qx true; then
|
||||
echo "VERSION transitioned ('$base_ver' -> '$ver') but no merged, release-labeled PR is behind this commit — a release is a labeled ceremony PR (#47), not a bare push. Refusing." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ceremony=yes" >> "$GITHUB_OUTPUT"
|
||||
# Assert 3 — the changelog names exactly this version, and the one
|
||||
# extractor (shared with the tag job and test/release.sh) gets a
|
||||
# non-empty body out of it. The notes are kept for the publish.
|
||||
- name: assert the changelog section for this version extracts
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
run: |
|
||||
. .github/scripts/release-lib.sh
|
||||
ver="$(cat VERSION)"
|
||||
changelog_section CHANGELOG.md "$ver" > "$RUNNER_TEMP/notes.md"
|
||||
if [ ! -s "$RUNNER_TEMP/notes.md" ]; then
|
||||
echo "CHANGELOG.md has no '## $ver' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release" >&2
|
||||
exit 1
|
||||
fi
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
# Assert 4 — nothing exists yet, tag or release: a re-run of this job
|
||||
# (or a manual tag that beat it) must refuse, not clobber.
|
||||
- name: assert no tag and no release exist yet (idempotent re-runs)
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ver="$(cat VERSION)"
|
||||
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 is mid-flight on the manual path); refusing to re-release" >&2
|
||||
exit 1
|
||||
fi
|
||||
if gh release view "$ver" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "release '$ver' already exists — refusing to re-release" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Act — tag the merge commit via the API, then publish with the notes
|
||||
# assert 3 extracted. (GITHUB_TOKEN-created tag: no recursive
|
||||
# workflow runs — see the job comment.)
|
||||
- name: tag the merge commit and publish the release
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ver="$(cat VERSION)"
|
||||
gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" \
|
||||
-f ref="refs/tags/$ver" -f sha="$MERGE_SHA"
|
||||
gh release create "$ver" --verify-tag \
|
||||
--title "$ver" --notes-file "$RUNNER_TEMP/notes.md" \
|
||||
-R "$GITHUB_REPOSITORY"
|
||||
# The post-release bump, folded into the release act (#47 followup —
|
||||
# operator decision: a mechanical one-liner deserves no PR of its
|
||||
# own). X.Y.(Z+1)-dev is arithmetic, not judgment: derived, committed
|
||||
# straight to main with this job's token. A GITHUB_TOKEN push fires
|
||||
# no workflows (anti-recursion), so the bump triggers neither this
|
||||
# door nor a red run; and should branch protection ever refuse the
|
||||
# direct push, the step opens the bump PR itself and says so, loudly,
|
||||
# instead of leaving main armed to impersonate the release.
|
||||
- name: bump main to the next -dev — the release re-arms main itself
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ver="$(cat VERSION)"
|
||||
next="$(printf '%s' "$ver" | awk -F. '{ printf "%s.%s.%s-dev", $1, $2, $3 + 1 }')"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin main
|
||||
git checkout -B main origin/main
|
||||
printf '%s\n' "$next" > VERSION
|
||||
git add VERSION
|
||||
git commit -m "chore: bump main to $next — a dev install must not impersonate $ver"
|
||||
if ! git push origin main; then
|
||||
echo "direct push refused (branch protection?) — opening the bump PR instead" >&2
|
||||
git checkout -b "chore/bump-$next"
|
||||
git push origin "chore/bump-$next"
|
||||
gh pr create -R "$GITHUB_REPOSITORY" --head "chore/bump-$next" \
|
||||
--title "chore: bump main to $next" \
|
||||
--body "The post-release re-arm, opened by release.yml because the direct push was refused. One file, one line." \
|
||||
--label release
|
||||
fi
|
||||
|
|
|
|||
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -36,6 +36,25 @@ on the way to cutting its first release, and this file starts there.
|
|||
|
||||
### Added
|
||||
|
||||
- **Merging a release-labeled PR IS the release — and the release re-arms
|
||||
main itself** (#47) — the rig twin of heavy-duty/box#96, born of the
|
||||
ceremony retro: the tag was a separate, manual, silent-when-forgotten
|
||||
step, and a forgotten tag produces no red X. `release.yml` now fires on
|
||||
pushes to main (fork-sourced ceremony PRs get a read-only token on
|
||||
`pull_request` events), reading the transition from the push itself:
|
||||
`event.before` to the pushed head. A decide step answers four states —
|
||||
release-flow *work* merged under the `release` label (`-dev` endstates,
|
||||
the post-release window) no-ops green with a NOTICE; the two genuinely
|
||||
ambiguous bare states refuse loudly; a true transition then requires a
|
||||
merged, `release`-labeled PR behind the commit (read via the API — the
|
||||
label is the operator's declared intent). Then, in the same job, it
|
||||
API-creates the tag at the merge commit, publishes with the extracted
|
||||
notes — and bumps main to `X.Y.(Z+1)-dev` itself, direct push with a
|
||||
loud open-a-PR fallback, so no follow-up bump PR exists on the paved
|
||||
road. A `GITHUB_TOKEN`-created tag never fires the tag-push trigger, so
|
||||
the paths cannot double-publish — and that tag-push path survives intact
|
||||
as the documented manual fallback and backfill.
|
||||
|
||||
- **Tagged releases, and an installer that installs them** (#32) — the rig
|
||||
half of the flow designed in heavy-duty/box#83, near-verbatim. A release
|
||||
is a PR, then a tag: the `release: X.Y.Z` PR bumps `VERSION` and stamps
|
||||
|
|
|
|||
|
|
@ -46,19 +46,33 @@ labels tell you where everything is without opening anything.
|
|||
|
||||
## Releasing
|
||||
|
||||
A release is a PR, then a tag (#32; box#83's design):
|
||||
A release is a PR, and merging it is the release (#47; box#96's design, on
|
||||
top of #32/box#83's tag flow):
|
||||
|
||||
1. A small PR — `release: X.Y.Z` — bumps `VERSION` from `X.Y.Z-dev` and
|
||||
stamps `CHANGELOG.md`'s Unreleased section as `## X.Y.Z — YYYY-MM-DD`.
|
||||
CI green on it, same loop as any PR.
|
||||
2. Merge, tag the merge commit bare `X.Y.Z` (no `v` prefix — box's tag
|
||||
scheme), push the tag. `release.yml` asserts tag == `VERSION` (a
|
||||
mismatch fails loudly and creates nothing) and creates the GitHub
|
||||
release with that version's changelog section as the body. No assets —
|
||||
the source tarball for the tag is the package `install.sh` downloads.
|
||||
3. A follow-up (or the next feature PR) bumps main's `VERSION` to
|
||||
`X.Y.(Z+1)-dev`, so a dev install never impersonates the release in the
|
||||
`versions/<v>` layout.
|
||||
1. A small PR — `release: X.Y.Z`, carrying the `release` label — bumps
|
||||
`VERSION` from `X.Y.Z-dev` and stamps `CHANGELOG.md`'s Unreleased
|
||||
section as `## X.Y.Z — YYYY-MM-DD`. CI green on it, same loop as any PR.
|
||||
2. Merge it — that IS the ship decision. `release.yml`'s
|
||||
`release-on-merge` job asserts, in order, fail-loud, creating nothing:
|
||||
the merged tree's `VERSION` is non-`-dev`; this PR is the one that
|
||||
changed it (a mislabeled ordinary PR fails here); the changelog section
|
||||
for that version extracts non-empty; no tag or release exists yet.
|
||||
Then, same job, it tags the merge commit bare `X.Y.Z` (no `v` prefix —
|
||||
box's tag scheme) and publishes the GitHub release with that section as
|
||||
the body. No assets — the source tarball for the tag is the package
|
||||
`install.sh` downloads.
|
||||
3. The release re-arms main itself: the same workflow run bumps `VERSION`
|
||||
to `X.Y.(Z+1)-dev` and pushes the commit straight to main — no
|
||||
follow-up PR (it opens one only if branch protection refuses the
|
||||
direct push, loudly). A dev install therefore never impersonates the
|
||||
release in the `versions/<v>` layout. On the *manual* tag path the
|
||||
bump stays yours: open the one-line PR after publishing.
|
||||
|
||||
Manual fallback (and backfill): if the merge-path run fails, fix what it
|
||||
named, then tag the merge commit `X.Y.Z` by hand and push the tag — the
|
||||
original tag-push job still turns any correct tag into the release, and
|
||||
the merge path's nothing-exists-yet assert keeps the two from
|
||||
double-publishing.
|
||||
|
||||
## Labels — who sets what
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,81 @@ create_at="$(grep -n "gh release create" "$RY" | head -n1 | cut -d: -f1)"
|
|||
check "release.yml: the assert precedes the create" \
|
||||
0 "" test "${assert_at:-999999}" -lt "${create_at:-0}"
|
||||
|
||||
# --- release.yml, the merge path: the pins (#47; box#96's design) ------------
|
||||
# Merging the release-labeled ceremony PR IS the release. Same grep-pin
|
||||
# treatment for the merge path's load-bearing pieces: the gate, the four
|
||||
# fail-loud asserts, the same-job tag+publish, and the surviving tag-push
|
||||
# fallback.
|
||||
# The merge door rides pushes to MAIN, not pull_request events: a fork PR's
|
||||
# pull_request run gets a read-only GITHUB_TOKEN (permissions: cannot raise
|
||||
# it), and every ceremony PR this org merges is cross-repo from the bot
|
||||
# fork — the tag create would 403 after green asserts (#48 round 1). The
|
||||
# label — the operator's intent — is read via the API off the merge commit.
|
||||
check "release.yml: the merge door rides pushes to main (fork-token-proof)" 0 "" \
|
||||
grep -qF "branches: [main]" "$RY"
|
||||
# YAML maps are last-key-wins: a second sibling push: key silently replaces
|
||||
# the first and kills a door (grok's round-2 catch — the tag fallback had
|
||||
# stopped triggering). Exactly ONE push key may exist.
|
||||
check "release.yml: exactly one on.push key (duplicate keys drop a door)" 0 "1" \
|
||||
grep -cE '^ push:' "$RY"
|
||||
check "release.yml: ...and the doors split on the ref (tag door takes tags)" 0 "" \
|
||||
grep -qF "startsWith(github.ref, 'refs/tags/')" "$RY"
|
||||
# shellcheck disable=SC2016 # the $-string is a literal in the target file
|
||||
check "release.yml: the release label is read via the API off the merge commit" 0 "" \
|
||||
grep -qF 'commits/$MERGE_SHA/pulls' "$RY"
|
||||
check "release.yml: a transition without a labeled PR refuses" 0 "" \
|
||||
grep -qF "no merged, release-labeled PR is behind this commit" "$RY"
|
||||
# The decide step tells the label's two meanings apart (LABELS.md gives
|
||||
# `release` to release-flow WORK as well as to the ceremony PR): work under
|
||||
# the label is a green NOTICE no-op — in the -dev steady state and in the
|
||||
# post-release window (bare, unchanged, already released) — while every
|
||||
# half-ceremony refuses. Pin each verdict's message and the gating output.
|
||||
check "release.yml: decide — dev-tree work no-ops green (not a red run per infra PR)" 0 "" \
|
||||
grep -qF "release-flow work under the release label, not a ceremony" "$RY"
|
||||
check "release.yml: decide — a -dev endstate is always work (the bump PR no-ops green)" 0 "" \
|
||||
grep -qF "a dev tree is by definition not a release" "$RY"
|
||||
check "release.yml: decide — post-release-window work no-ops green" 0 "" \
|
||||
grep -qF "release-flow work merged in the post-release window" "$RY"
|
||||
check "release.yml: decide — bare, unchanged, never released refuses to guess" 0 "" \
|
||||
grep -qF "Refusing to guess" "$RY"
|
||||
# shellcheck disable=SC2016 # the $-refs are the inner bash -c's, deliberately
|
||||
check "release.yml: decide gates every later step on ceremony=yes" 0 "" \
|
||||
bash -c '[ "$(grep -cF "if: steps.decide.outputs.ceremony == '\''yes'\''" "$1")" -ge 3 ]' _ "$RY"
|
||||
check "release.yml: assert 3 — an empty section refuses to publish" 0 "" \
|
||||
grep -qF "refusing to publish an empty release" "$RY"
|
||||
check "release.yml: assert 4 — an existing tag or release refuses (idempotent)" 0 "" \
|
||||
grep -qF "refusing to re-release" "$RY"
|
||||
# Same-job matters: a GITHUB_TOKEN-created tag fires no tag-push workflow,
|
||||
# so the publish must live NEXT TO the tag creation. The workflow keeps
|
||||
# release-on-merge as its last job (pinned by comment there) so the awk
|
||||
# range runs to EOF; both acts must land inside it.
|
||||
MJOB="$(awk '/^ release-on-merge:/,0' "$RY")"
|
||||
mjob_has() { printf '%s' "$MJOB" | grep -qF -e "$1"; }
|
||||
check "release.yml: the merge job API-creates the tag itself" 0 "" \
|
||||
mjob_has "git/refs"
|
||||
# shellcheck disable=SC2016 # the $-string is a literal in the target file
|
||||
check "release.yml: ...at the pushed main head (github.sha = the merge commit)" 0 "" mjob_has 'sha="$MERGE_SHA"'
|
||||
# The release re-arms main itself: the post-release -dev bump is arithmetic,
|
||||
# not judgment, so it rides the same job — direct push, PR fallback.
|
||||
check "release.yml: the release bumps main to the next -dev itself" 0 "" \
|
||||
grep -qF "bump main to the next -dev" "$RY"
|
||||
check "release.yml: ...with a PR fallback when the direct push is refused" 0 "" \
|
||||
grep -qF "opening the bump PR instead" "$RY"
|
||||
check "release.yml: ...and publishes in the SAME job" 0 "" \
|
||||
mjob_has "gh release create"
|
||||
# Ordering, the marker-then-box idiom again: the last assert's refusal must
|
||||
# precede the tag creation (asserts first, acts last; defaults fail closed).
|
||||
massert_at="$(grep -n "refusing to re-release" "$RY" | head -n1 | cut -d: -f1)"
|
||||
mtag_at="$(grep -n "git/refs" "$RY" | head -n1 | cut -d: -f1)"
|
||||
check "release.yml: the merge-path asserts precede the tag" \
|
||||
0 "" test "${massert_at:-999999}" -lt "${mtag_at:-0}"
|
||||
# ...and the manual path SURVIVES: tag-push trigger plus a push-gated job,
|
||||
# the documented fallback and backfill.
|
||||
check "release.yml: the tag-push trigger survives (manual fallback intact)" 0 "" \
|
||||
grep -qF "tags: ['**']" "$RY"
|
||||
check "release.yml: the fallback job is gated to push events" 0 "" \
|
||||
grep -qF "github.event_name == 'push'" "$RY"
|
||||
|
||||
# --- the installer's ref logic, extracted ------------------------------------
|
||||
# install.sh must stay a single curl|bash file, so its channel functions live
|
||||
# inline; extract them here and drive them for real (the valid_version awk
|
||||
|
|
|
|||
Loading…
Reference in a new issue