Repository navigation
v1.4.0
Hephaestus v1.4.0
hephaestus preflight, and so hephaestus publish, now checks the release
workflow's conventions before the tag is pushed; the Homebrew tap stage no
longer lets two release pull requests downgrade a formula through their merge
order; and two new commands guard the tap's merges: cmd/tap-version-guard
for the tap's required check and cmd/pr-gate for a fail-closed merge. The
publish lock no longer outlives a killed process.
Release workflow checks
hephaestus preflight reads the release workflow (preflight.workflow,
default .github/workflows/release.yml) and the CI workflow it runs from the
work tree, and checks the conventions hephaestus publish relies on, which
were verified only after the release was published:
| Check | What it requires |
|---|---|
workflow.release |
tag pushes only, read-only default permissions, one publishing job in an environment that needs the CI job |
workflow.pins |
every action pinned to a commit SHA; Hephaestus pins carry their release as a comment, and CI runs the release's commit |
workflow.tap |
the tap token only in the two named tap steps, after the plan step, with if: steps.plan.outputs.tap == 'publish' |
workflow.attestation |
every planned asset attested before publishing |
workflow.ci-gate |
ci-gate needs every CI job, with if: always() |
They read YAML only, so they work for any language, and fail closed on what
they cannot read unambiguously. They are warnings: a violation makes the
preflight WARN (exit 0) and fails --strict, so hephaestus publish stops
before pushing the tag. preflight.skip turns one off.
hephaestus check --workflows # the same checks without a tag, for pull request CI (exit 1 on a violation)See docs/preflight.md.
Homebrew tap: formula downgrades
In pull request mode every release opens its own formula pull request; merged
in the wrong order, an older one lowered the formula. The tap stage now:
- reads the other open pull requests against the tap's base branch before
proposing a formula. When one proposes a newer version (by SemVer
precedence), nothing is written and the formula is skipped, as for a newer
base branch. Older ones are named in a warning and in the new pull
request's body, which states the merge order; the scan is repeated after
opening, so racing releases report each other.brews[].allow_downgrade
turns the skip into a warning; - counts only pull requests from a branch of the tap (or of the configured
head repository). One from someone else's fork is a warning: anyone can
open one on a public tap; - fails, before writing anything and in a dry run too, when the base
branch, the pull request branch or an open pull request from the tap
already has the version being published with another sha256 for the same
archive url: the release's archives were replaced or tampered with. The
release workflows' read-only rehearsal stops such a release before it is
published.allow_downgradedoes not apply.tap.ChecksumChangesis the
rule, for the tap's CI to reuse.
cmd/tap-version-guard
A check for the tap's own CI that fails a change lowering a formula's
version:
go run github.com/magicdrive/hephaestus/cmd/tap-version-guard@<commit> [-base HEAD^1 [-each] | -against <tip>] [-head HEAD] [-strict] \
[-rollback PATH=FROM->TO ... | -approvals <comments.json> -permissions <permissions.json> -approval-head <sha> [-approval-before <time>]] \
[-summary <file>] [-allow-downgrade]-against <tip>checks an unmerged pull request head against the base
branch's current tip;-eachchecks every first-parent step of
base..head(a merge queue entry, a push of several merges).-strict, for a required check, also fails a formula with no or a
non-semantic version, versions that cannot be compared, the same version
with other artifacts, a url of another version, a removed formula, a
formula that is not a regular file, an unsafe path and aFormulaentry
that is not a directory. It reads only the formulae Homebrew loads, from
git objects only.- A rollback is authorized for exactly one downgrade,
PATH=FROM->TO: by
-rollback, or by an approval comment (/formula-rollbackwithhead:,
formula:andreason:) whose author is an admin of the tap, that was
never edited and names the pull request head. Every other finding stays an
error, and an authorization the change does not use fails.
-allow-downgradestays for compatibility.
examples/workflows/tap-version-guard.yml
runs the required check on pull_request_target (the default branch's
workflow; the pull request is fetched as objects only), needs the
formula-rollback label and an admin's approval for a rollback, posts the
commit status version-guard as well, and re-checks after each merge,
opening an issue when a downgrade landed. See
docs/tap-version-guard.md.
Merge gate
cmd/pr-gate decides whether a pull request may be merged and, only with
-merge, merges it pinned to the reviewed head (PUT pulls/{n}/merge with
sha, sent once) and verifies the merge commit's parents. Every condition
fails closed: open, not a draft, exact head and base commits, behind 0,
mergeable and clean, required checks read from the branch rules and branch
protection (or named with -require), every check, status and workflow run
completed, and a required check satisfied only by success. Without
-merge it only reads. See
docs/pr-gate.md.
Fixes
- Publish lock. The lock on
hephaestus-publish.lockis an operating
system file lock (flock on Unix, LockFileEx on Windows) instead of a file
created withO_EXCL. A publish killed withSIGKILL(or crashed) left the
file behind and every later publish failed until it was removed by hand;
the system now releases the lock with the process. A live publish's lock is
still refused, naming its pid, host and start time. Older versions, which
lock withO_EXCLon the same file, are still excluded. Publish refuses a
file system that cannot lock, and a clone on a network or user-space file
system (NFS, SMB, WebDAV, FUSE, 9p, a network drive), and checks right
before creating and pushing the tag that the lock file is still the one it
locked. - Tap head branch. A pull request head branch that someone else created
between the tap stage's check and its create (a 422 "Reference already
exists") is verified like any existing branch, instead of being used as it
was. A create whose response was lost (5xx, timeout) is followed by a read
of the branch, instead of failing the tap step. - Release stage. When the commit being released is known (
--commit,
$GITHUB_SHA) but the tag cannot be read (an API error other than 404),
the release stage stops before it creates, uploads or publishes anything.
It only warned and went on, so the build could have been published under a
tag of another commit. hephaestus publishsays to delete the local tag when another clone
pushed the same tag at another commit first.- Scripts.
scripts/fuzz.shfails whengo listorgo test -list
fails, instead of fuzzing nothing;scripts/go-env-snapshot.shfails when
go versionorgo envfails, and runs them outside any module.make fmt-checkfails whengofmtitself fails. - Hephaestus's own release workflow. A new
provenancejob releases only
a tag whose commit is in the history ofmain, and the release jobs run on
ubuntu-24.04instead ofubuntu-latest.
Upgrade notes
These changes fail closed where the previous version went on:
- A release workflow that violates a
workflow.*convention now stops
hephaestus publish(andhephaestus preflight --strict) before the tag
push. Fix the workflow, or turn the check off withpreflight.skip. - A tap that already has the version being published with another sha256
for the same archive url now fails the tap stage, in a dry run too. Release
a new version instead. - A
hephaestus-publish.lockwritten by an older version (pid N) is
refused, never taken over by its pid: empty the file once no older publish
runs. hephaestus publishrefuses a clone on a network or user-space file
system; publish from a clone on a local disk.
Compatibility
- Unchanged: configuration keys, the Action,
publish planandpublish status, and the existing JSON fields and check IDs. - New: the preflight checks
workflow.release,workflow.pins,
workflow.tap,workflow.attestationandworkflow.ci-gate(warnings),
hephaestus check --workflows,cmd/tap-version-guard,cmd/pr-gateand
examples/workflows/tap-version-guard.yml. - Changed: in pull request mode the tap stage skips a formula when an
open pull request from the tap proposes a newer version (a warning with
brews[].allow_downgrade), and fails on the same version with other
checksums (above).
Known limitations
cmd/pr-gateserializes merges on one machine with a lock file created
withO_EXCLin the temporary directory: a gate killed withSIGKILL
leaves it behind (remove it by hand), and another user of a shared
temporary directory can create it first. A merge's safety rests on the
pinnedshaand the check of the merge commit's parents, not on the lock.hephaestus checkshows the tap pull request branch as a template;
hephaestus preflight --tagrenders it.
Verification
CI succeeded on the release commit's parent on main (run 40511957192),
including the tests added with these changes: every check between creating
and pushing the tag, failure injection for every GitHub write of a release,
the race of two release pull requests (TestTOCTOU), and go test without
network.
Full Changelog: v1.3.0...v1.4.0