Repository navigation
Releases: magicdrive/hephaestus
Release list
v1.5.1
What's Changed
- ci: run actionlint, bound every CI job, keep the e2e's released actions by @magicdrive in #56
Full Changelog: v1.5.0...v1.5.1
v1.5.0
Hephaestus v1.5.0
Hephaestus can now be installed without Homebrew or Go: install.sh installs a release from
GitHub Releases on macOS and Linux (amd64, arm64), checked against the release's checksum file.
Nothing else changes: the commands, the configuration, the release pipeline and the action are
those of v1.4.0.
Install script
curl -fsSLO https://raw.githubusercontent.com/magicdrive/hephaestus/main/install.sh
sh install.sh # the latest stable release, into ~/.local/bin
sh install.sh --version v1.4.0 # a given release (older ones and pre-releases too)
sh install.sh --upgrade # the latest stable release again
sh install.sh --dir "$HOME/bin" # another directory (created if missing)- The archive must match the SHA-256 of
hephaestus_checksums.txt; a missing or malformed
checksum file stops the install. Only thehephaestusbinary is taken out of the archive,
after its entries have been checked (no absolute paths,.., links or devices), and it
replaces the installed one with a single rename after it ran and reported the version asked
for. A failed or interrupted run leaves the installed binary as it was. - It never uses
sudoand never changesPATH; it refuses Homebrew's directories and does not
replace a symbolic link (keepbrew upgrade hephaestusfor a Homebrew installation). - Every release from v0.1.0 on can be installed. The archives keep carrying build provenance
attestations (gh attestation verify <file> --repo magicdrive/hephaestus). - Windows is not supported by the script (use the
.zipof the release, or the action).
The script is the installer of Enma with one more
configuration line, VERSION_PREFIX, because hephaestus --version prints hephaestus v1.5.0
(unchanged). See docs/installer.md.
Documentation
- README: the Installation section shows Homebrew, the install script, the archives, the action
and building from source.go install github.com/magicdrive/hephaestus@latestis no longer
documented.
Verification
internal/installerruns the script against a fake github.com (local TLS, no network) with
sh, dash, bash, ksh and BusyBox, curl and GNU Wget: install, reinstall, upgrade, downgrade,
pre-releases, HTTP failures, truncated and stalled downloads, checksum and archive attacks,
install directories, Homebrew, interrupted and concurrent runs, and the README's commands.- CI installs the snapshot archives it has just built with the script (
TestDistArchives).
Full Changelog: v1.4.0...v1.5.0
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 ...
v1.3.0
Hephaestus v1.3.0
hephaestus publish no longer assumes that
releases are cut from main: it tells the repository's default branch
from the release branch, and checks everything that guards a release on
the release branch.
Release branch
hephaestus publish check v1.2.3 # the default branch: main, master, develop...
hephaestus publish check -branch release/stable v1.2.3 # a named release branchpreflight:
branch: release/stable # read by hephaestus publish and hephaestus preflightThe release branch is -branch, else preflight.branch, else the release
repository's default branch on GitHub. A repository whose default branch is
develop can release from release/stable; one on master needs no
setting. The preflight prints both:
[ok ] default branch develop (GitHub, origin/HEAD)
[ok ] release branch release/stable (preflight.branch) at 1a2b3c4d5e6f on GitHub
Everything is checked on the release branch, never on the default branch on
its behalf:
- it is a branch name that exists on GitHub: not a tag, a commit SHA, a full
ref or a refspec; - the clone is on it, clean, with
HEAD=origin/<branch>= its head on
GitHub; - CI succeeded on it, for exactly that commit: a green default branch does
not count; - an active ruleset covering it requires
ci-gate; - right before the tag push it is fetched again and its head on GitHub read
again: a branch that moved stops publish before anything is pushed; hephaestus preflight --strict, which publish also runs, checks the same
branch.
When a branch cannot be read (no network, no access), or a local
refs/remotes/origin/HEAD names another branch than the default branch,
nothing further is checked and nothing is tagged.
See docs/publish.md.
Fixes
- A ruleset covering
~DEFAULT_BRANCHcounted as protecting whichever branch
was checked. It now counts only for the repository's default branch, in
hephaestus publishand inhephaestus preflight(github.ruleset.branch
and the required checks ofgithub.ci).
Release candidate
v1.3.0-rc.1 was released with this hephaestus publish itself (make publish): the preflight resolved main as both the default branch and the
release branch, re-read its head on GitHub before the tag push, and the
release was verified (assets, checksums, provenance attestations, the tap
left untouched). Its binaries ran on Linux, macOS and Windows, amd64 and
arm64 (verify-release.yml), and passed publish check on a repository
whose default branch is master.
Compatibility
- Unchanged: configuration (
preflight.branchis the existing key), the
Action,publish planandpublish status, JSON fields and check IDs. - New: the
-branchflag ofhephaestus publish, and itsdefault branchandrelease branchchecks. A project onmaingets the same
checks as before plus these two; the ruleset check keeps its name
(main ruleset). - Stricter:
hephaestus preflightwith apreflight.branchother than the
default branch now warns (github.ruleset.branch) when only the default
branch's ruleset covers it.
Known limitations
hephaestus checkshows the tap pull request branch as a template;
hephaestus preflight --tagrenders it.
Full Changelog: v1.2.0...v1.3.0
v1.3.0-rc.1
Hephaestus v1.3.0-rc.1
Release candidate of v1.3.0. hephaestus publish no longer assumes that
releases are cut from main: it tells the repository's default branch
from the release branch, and checks everything that guards a release on
the release branch.
Release branch
hephaestus publish check v1.2.3 # the default branch: main, master, develop...
hephaestus publish check -branch release/stable v1.2.3 # a named release branchpreflight:
branch: release/stable # read by hephaestus publish and hephaestus preflightThe release branch is -branch, else preflight.branch, else the release
repository's default branch on GitHub. A repository whose default branch is
develop can release from release/stable; one on master needs no
setting. The preflight prints both:
[ok ] default branch develop (GitHub, origin/HEAD)
[ok ] release branch release/stable (preflight.branch) at 1a2b3c4d5e6f on GitHub
Everything is checked on the release branch, never on the default branch on
its behalf:
- it is a branch name that exists on GitHub: not a tag, a commit SHA, a full
ref or a refspec; - the clone is on it, clean, with
HEAD=origin/<branch>= its head on
GitHub; - CI succeeded on it, for exactly that commit: a green default branch does
not count; - an active ruleset covering it requires
ci-gate; - right before the tag push it is fetched again and its head on GitHub read
again: a branch that moved stops publish before anything is pushed; hephaestus preflight --strict, which publish also runs, checks the same
branch.
When a branch cannot be read (no network, no access), or a local
refs/remotes/origin/HEAD names another branch than the default branch,
nothing further is checked and nothing is tagged.
See docs/publish.md.
Fixes
- A ruleset covering
~DEFAULT_BRANCHcounted as protecting whichever branch
was checked. It now counts only for the repository's default branch, in
hephaestus publishand inhephaestus preflight(github.ruleset.branch
and the required checks ofgithub.ci).
Compatibility
- Unchanged: configuration (
preflight.branchis the existing key), the
Action,publish planandpublish status, JSON fields and check IDs. - New: the
-branchflag ofhephaestus publish, and itsdefault branchandrelease branchchecks. A project onmaingets the same
checks as before plus these two; the ruleset check keeps its name
(main ruleset). - Stricter:
hephaestus preflightwith apreflight.branchother than the
default branch now warns (github.ruleset.branch) when only the default
branch's ruleset covers it.
Known limitations
hephaestus checkshows the tap pull request branch as a template;
hephaestus preflight --tagrenders it.
Full Changelog: v1.2.0...v1.3.0-rc.1
v1.2.0
Hephaestus v1.2.0
The GitHub Action no longer compiles
Hephaestus: it installs the release binary that belongs to the commit you
pinned, verified by its checksum and its provenance attestation, and leaves
your toolchain alone. This release also brings hephaestus publish, and the
fixes prepared for v1.1.1 (never released).
The Action runs the verified release binary
- uses: magicdrive/hephaestus@<full commit SHA of v1.2.0> # v1.2.0Before your build runs, the Action:
- reads the release named in
action/VERSIONof the pinned commit; the
release workflow refuses a tag that this file does not name; - downloads
hephaestus_<version>_<os>_<arch>.tar.gz(.zipon Windows)
andhephaestus_checksums.txt, from that release only; - checks the archive's SHA-256;
- verifies the archive's provenance attestation (
gh attestation verify).
It must have been signed bymagicdrive/hephaestus'srelease.ymlon a
GitHub-hosted runner, for the release tag and, when pinned by SHA, for
exactly the pinned commit; - extracts only the regular file
hephaestus[.exe]at the archive root,
and checks that it reports the expected version.
Any failure stops the Action. It never falls back to building from source.
As a result:
- No toolchain is involved. Nothing is compiled, no Go is downloaded or
set up, and nothing is written to$GITHUB_PATHor$GITHUB_ENV. Your
build commands use the toolchain your workflow set up. A runner without Go
can run the Action for a project in any language. - The binary is bound to your pin. The attestation proves that the
binary was built by Hephaestus's release workflow from the commit you
pinned. - Supported platforms are unchanged: Linux, macOS and Windows, on x64
and ARM64. A runner needsbash,curl,tarandgh, which
GitHub-hosted runners have.
Reference the Action by a release. Use the full SHA of a release tag's
commit (recommended) or the tag itself. A branch such as @main now fails,
because no release binary belongs to it.
hephaestus publish
The tool that releases Hephaestus is now a command for any project:
make publish vX.Y.Z that runs hephaestus publish. In order, it:
- checks that the release can be made: branch, clean worktree, in sync
with origin, the tag unused, CI green on the commit, rulesets, the
release environment, the tap, and the generic preflight; - asks for confirmation;
- pushes an annotated tag;
- follows the release workflow;
- verifies the release: assets, checksums, attestations and the tap pull
request.
See docs/publish.md.
The Action gains command: plan, with the outputs prerelease and tap.
The steps that use the tap token can then run only when the tap is
published. A prerelease never even receives the token.
Fixes (prepared for v1.1.1)
- The Action no longer changes your Go toolchain. v1.1.0 put its own Go
first onPATHand exportedGOTOOLCHAIN=local, so your Go modules were
built with Go 1.26.0, or failed to build. PWDis no longer redacted as a credential. Paths under the current
directory are no longer printed as***. A value matched only by its
variable's name is not redacted when it is the path of an existing file or
directory. Known tokens are always redacted.--freshremoves the previous run's outputs (archives, checksum files
and formulae) fromdist, if they are unchanged and Hephaestus wrote them.
Release candidate
v1.2.0-rc.1 (commit bae0e2b) was published as a prerelease. Before this
release, it was verified on GitHub in two ways:
- The Action's release path, pinned to that commit. The Action
downloaded the release binary, checked its checksum, verified its
attestation, extracted it and checked its version. This ran on Linux,
macOS and Windows, x64 and ARM64. The job's toolchain was unchanged
afterwards. - The five projects that adopted Hephaestus. git-parallel, kjak, ark,
enma and goreg ran CI, snapshots and Homebrew end-to-end tests.
v1.2.0 differs from the release candidate only in action/VERSION, these
notes, the changelog and CI.
Compatibility
- Unchanged: configuration, CLI flags, Action inputs and outputs, JSON
fields and check IDs. New: thepublishcommand, and the Action'splancommand with the outputsprereleaseandtap. - The Action: it must be referenced by a release tag or that tag's
commit, and it no longer provides Go. Set up your own Go, as the examples
always did. - Template variables:
{{ .Env.PWD }}and{{ .Env.OLDPWD }}are now
visible to templates.
Known limitations
- Homebrew 4.6 parses the version of archive URLs such as
tool_1.2.3_darwin_arm64.tar.gzas64; Homebrew 7.0 parses it
correctly.brew updateupdates Homebrew itself. hephaestus checkshows the tap pull request branch as a template;
hephaestus preflight --tagrenders it.
Full Changelog: v1.1.0...v1.2.0
v1.2.0-rc.1
Hephaestus v1.2.0-rc.1
Release candidate of v1.2.0. The GitHub Action no longer compiles
Hephaestus: it installs the release binary that belongs to the commit you
pinned, verified by its checksum and its provenance attestation, and leaves
your toolchain alone. This release also brings hephaestus publish, and the
fixes prepared for v1.1.1 (never released).
The Action runs the verified release binary
- uses: magicdrive/hephaestus@<full commit SHA of v1.2.0> # v1.2.0Before your build runs, the Action:
- reads the release named in
action/VERSIONof the pinned commit; the
release workflow refuses a tag that this file does not name; - downloads
hephaestus_<version>_<os>_<arch>.tar.gz(.zipon Windows)
andhephaestus_checksums.txt, from that release only; - checks the archive's SHA-256;
- verifies the archive's provenance attestation (
gh attestation verify).
It must have been signed bymagicdrive/hephaestus'srelease.ymlon a
GitHub-hosted runner, for the release tag and, when pinned by SHA, for
exactly the pinned commit; - extracts only the regular file
hephaestus[.exe]at the archive root,
and checks that it reports the expected version.
Any failure stops the Action. It never falls back to building from source.
As a result:
- No toolchain is involved. Nothing is compiled, no Go is downloaded or
set up, and nothing is written to$GITHUB_PATHor$GITHUB_ENV. Your
build commands use the toolchain your workflow set up. A runner without Go
can run the Action for a project in any language. - The binary is bound to your pin. The attestation proves that the
binary was built by Hephaestus's release workflow from the commit you
pinned. - Supported platforms are unchanged: Linux, macOS and Windows, on x64
and ARM64. A runner needsbash,curl,tarandgh, which
GitHub-hosted runners have.
Reference the Action by a release. Use the full SHA of a release tag's
commit (recommended) or the tag itself. A branch such as @main now fails,
because no release binary belongs to it.
hephaestus publish
The tool that releases Hephaestus is now a command for any project:
make publish vX.Y.Z that runs hephaestus publish. In order, it:
- checks that the release can be made: branch, clean worktree, in sync
with origin, the tag unused, CI green on the commit, rulesets, the
release environment, the tap, and the generic preflight; - asks for confirmation;
- pushes an annotated tag;
- follows the release workflow;
- verifies the release: assets, checksums, attestations and the tap pull
request.
See docs/publish.md.
The Action gains command: plan, with the outputs prerelease and tap.
The steps that use the tap token can then run only when the tap is
published. A prerelease never even receives the token.
Fixes (prepared for v1.1.1)
- The Action no longer changes your Go toolchain. v1.1.0 put its own Go
first onPATHand exportedGOTOOLCHAIN=local, so your Go modules were
built with Go 1.26.0, or failed to build. PWDis no longer redacted as a credential. Paths under the current
directory are no longer printed as***. A value matched only by its
variable's name is not redacted when it is the path of an existing file or
directory. Known tokens are always redacted.--freshremoves the previous run's outputs (archives, checksum files
and formulae) fromdist, if they are unchanged and Hephaestus wrote them.
Compatibility
- Unchanged: configuration, CLI flags, Action inputs and outputs, JSON
fields and check IDs. New: thepublishcommand, and the Action'splancommand with the outputsprereleaseandtap. - The Action: it must be referenced by a release tag or that tag's
commit, and it no longer provides Go. Set up your own Go, as the examples
always did. - Template variables:
{{ .Env.PWD }}and{{ .Env.OLDPWD }}are now
visible to templates.
Known limitations
- Homebrew 4.6 parses the version of archive URLs such as
tool_1.2.3_darwin_arm64.tar.gzas64; Homebrew 7.0 parses it
correctly.brew updateupdates Homebrew itself. hephaestus checkshows the tap pull request branch as a template;
hephaestus preflight --tagrenders it.
Full Changelog: v1.1.0...v1.2.0-rc.1
v1.1.0
Hephaestus v1.1.0
Shell completion, for Hephaestus itself and for the formulae it generates.
A minor release: it adds a command and a configuration key, as the
compatibility policy
allows in 1.x.0.
Hephaestus completes itself
hephaestus completion bash|zsh|fish prints a completion script for bash
(3.2 or later), zsh and fish. It completes:
- commands;
- each command's flags, in
--flagor-flagform. A flag already given
is not offered again, unless it can be repeated. - the fixed values of
--format,--template,--phase,--stagesand
--skip, as--flag valueor--flag=value. Comma-separated lists
continue after a comma. - file names for
--config,--reportand--output.
Candidates come from the same flag definitions the commands parse, so
completion offers only what Hephaestus accepts. Generating them reads no
configuration, token or repository and uses no network.
Homebrew users get the completions with this release. The formula
installs the three scripts into Homebrew's completion directories, on a new
install and on brew upgrade from 1.0.0:
etc/bash_completion.d/hephaestusshare/zsh/site-functions/_hephaestusshare/fish/vendor_completions.d/hephaestus.fish
Homebrew installs the files, but each shell must also be set up to load
them. A fish installed by Homebrew needs nothing more. bash needs
bash-completion@2, and zsh needs Homebrew's site-functions on FPATH
before compinit. The
README
has the lines to add, and the commands for installing without Homebrew.
Completions in the formulae Hephaestus generates
brews[].completions installs bash, zsh and fish completions with a
formula, whatever language the CLI is written in. There are two ways to
provide them, and they can be combined for different shells:
- Generated: the installed executable prints each script at install
time, through Homebrew'sgenerate_completions_from_executable. You set
the arguments (args, e.g.[completion]), how the shell is passed
(shell_parameter_format:arg,flag,none,cobra,clap,
clickortyper) and the shells. - Shipped: the scripts are included in the archive (
files) and
installed withbash_completion.install,zsh_completion.installand
fish_completion.installunder the names Homebrew expects.
brews:
- name: mycli
completions:
generate:
args: [completion]The configuration is checked before anything is published:
- the shell names and the format are valid;
- the executable is installed into
binfor every archive; - no shell's completion is installed twice;
- every shipped file exists in each archive.
Every value is written as a Ruby string literal. A formula without
completions is generated exactly as before. Details are in the
configuration reference.
Testing
- Real shells: bash 5 and macOS's bash 3.2, zsh (through its completion
system, in a pseudo-terminal) and fish, on Linux and macOS in CI. The
cases cover commands, flags, values,--flag=value, comma lists, file
names, a quoted value with a space, an already given flag,--and an
invalid shell. - Homebrew, with a shell-script fixture whose bash and zsh completions
are generated and whose fish completion is shipped:brew style,
brew audit --strict, install, the three files in place, completion in
the three shells,brew test, uninstall. - Homebrew, upgrading Hephaestus itself: v1.0.0 is installed from the
production tap's formula, then upgraded withbrew upgradeto a formula
generated from this release'shephaestus.yaml. The three completion
files appear and matchhephaestus completion, completion works in the
three shells with the installed binary, andbrew testand uninstall
pass.
Compatibility
- Additions only: the
completioncommand and thebrews[].completions
key. No existing command, flag, configuration key, Action input or
output, JSON field or check ID changed.hephaestus helplists two more
commands (completion,help). - A configuration that uses
brews[].completionsis rejected by
Hephaestus 1.0.0 and older, because unknown keys are errors.
Limitations
- Tags, repositories and other free-form values are not completed.
- PowerShell completion is not provided.
generateinstalls only bash,
zsh and fish, including withshell_parameter_format: cobraortyper,
whose Homebrew default would add PowerShell. - A custom
shell_parameter_formatprefix string is not supported; use
extra_installfor such a case.
Full Changelog: v1.0.0...v1.1.0
v1.0.0
Hephaestus v1.0.0
Hephaestus 1.0 is release automation for command-line tools written in any
language. From one hephaestus.yaml it turns your executables into
reproducible archives, a checksum file and a GitHub Release, then delivers
a Homebrew formula to your tap as a pull request. You choose how the
executables are made: Hephaestus runs your build command per target, or
imports binaries that are already built. It is a single Go binary and a
composite GitHub Action.
1.0 is about releasing without surprises:
- Check before you tag.
hephaestus preflightchecks the repository,
the configuration, the release notes and GitHub (rulesets, environment,
CI, existing releases) and tells you what would block the release. - Never half publish. A release is created as a draft. It is published
only after every asset has been uploaded and verified against the local
build. - Never destroy what is already public. A published release is never
edited, its assets are never replaced or deleted, and Hephaestus never
deletes a release. - Know where you stand after a failure.
hephaestus statusreads, and
never writes, what a release run left behind and names the next safe
action. Re-running resumes from that point.
From 1.0.0 Hephaestus follows Semantic Versioning, with the stable surface
defined in the
compatibility policy.
Key features
Release pipeline
- Build: your own command per target, or prebuilt binaries imported from
a path template. - Archive:
tar.gzandziparchives with an explicit file list. They
are deterministic: fixed timestamps, order and modes, so the same inputs
give the same bytes. A noarch archive (targets: [{os: all, arch: all}])
is one archive for every platform, for scripts. - Checksum: one SHA-256 file over the archives.
- GitHub Release: draft first, idempotent and compatible with immutable
releases. Every asset is compared by digest;make_latestand
prerelease: autoare supported. - Homebrew formula: generated from the manifest of what was built,
uploaded and checksummed, in current Homebrew style (on_macos,
on_linux,on_arm,on_intel), or a singleurlfor a noarch archive. - Tap pull request: the formula is delivered to your tap as a pull
request, never by force-push. Prereleases can skip the tap
(skip_upload: auto).
Safety
- Strict preflight:
hephaestus preflight --tag vX.Y.Z --strictruns 26
read-only checks. It is also available in the Action (command: preflight), with a JSON report. - Release notes validation: the notes file and the changelog must name
the tag as a whole version.v1.2.0is not satisfied byv1.2.0-rc.1. - Published releases are left as published: a published release's
title, notes and prerelease flag are never edited; a difference is only
reported. Its assets are never replaced or deleted, on mutable releases
too. - Conflict detection: an asset that differs from the local build stops
the run. So does a tag that points to another commit, or a release for the
same tag created by a concurrent run; nothing is overwritten or deleted. - Re-reading before every write: a draft is read again right before it
is published, before one of its assets is deleted, and before its title or
notes are updated. A release that was published in the meantime is left
alone. - Safe recovery: a lost API response is followed by a lookup, never by
a blind second create, and an interrupted run resumes from where it
stopped. The recovery guide
covers 12 failure scenarios. - Read-only status:
hephaestus status --tag vX.Y.Zreports one of
not-started,tagged,draft,published-incomplete,conflict,
publishedorcompleted. It compares assets by digest and finds each
formula's tap pull request. Use--format jsonfor scripts. - Token hygiene: publishing tokens are removed from the build's
environment and redacted from every output.
Artifact quality
- Reproducible: rebuilding the v1.0.0-rc.2 tag from a clean checkout
reproduced all of its published archives byte for byte. - Checksums and attestations: every archive and the checksum file carry
a build provenance attestation from the release workflow
(gh attestation verify <file> --repo magicdrive/hephaestus). - Formula install collision detection: a formula that would install
the same file twice, a directory and a file inside it, or two files to
one destination is rejected when it is generated. Such a formula could
never install.
Developer experience
hephaestus init --template go|rust|script|prebuiltwrites a
startinghephaestus.yamlthat passescheckandrun --snapshot, and
whose formula passesbrew audit --strict. For Rust,run --snapshot
needs the template's Rust targets installed. Prereleases skip the tap.
initnever overwrites a file.run --snapshotbuilds archives and checksums without a tag. Release,
formula and tap stages need a tag and are skipped.- Composite GitHub Action: builds Hephaestus from the commit you pin,
and runsrun,check, any single stage, orpreflight. - Documentation: a CLI reference,
configuration, archives, the GitHub Action, recovery, GoReleaser
compatibility and migration.
Supported platforms
Release archives are published for six platforms. What was verified differs
by platform:
| Platform | Archive | Tests from source in CI | Published binary run (v1.0.0-rc.2) |
|---|---|---|---|
| macOS amd64 | yes | weekly extended CI (macos-15-intel) |
yes: verify-release workflow; full local dogfooding (templates, Homebrew install) |
| macOS arm64 | yes | every change (macos-latest) |
yes: verify-release workflow |
| Linux amd64 | yes | every change | yes: verify-release workflow; container |
| Linux arm64 | yes | every change (ubuntu-24.04-arm) |
yes: verify-release workflow; container |
| Windows amd64 | yes | every change | yes: verify-release workflow |
| Windows arm64 | yes | — | yes: verify-release workflow (windows-11-arm) |
"Published binary run" means the archive was downloaded from the release
and checked against the checksum file, then version, --help, init and
check were run. Generated formulas are installed and tested with Homebrew
on macOS and Linux in CI.
Compatibility with v0.2.0
-
Configuration: v0.2.0 configurations load unchanged; Hephaestus's own
and magicdrive/t's are kept as regression fixtures. -
CLI, Action, check IDs, JSON: changes are additions only:
- commands
statusandinit; preflight --report;- Action inputs
strictandoffline, and outputspreflight-status,
preflight-exit-codeandpreflight-report; - the check
config.release-notes; - configuration
preflight.changelogand noarch targets.
Nothing was removed or renamed. The preflight JSON report keeps
schema_version: 1. - commands
Some behaviors changed, each to protect what is already published, or to
reject something that could never have worked:
- A published release's title, notes and prerelease flag are never
edited. v0.2.0 updated them on a re-run. A difference is now a warning,
and the formula and tap can still be resumed. - A published release's assets are never replaced or deleted.
replace_existing_assetsnow applies to drafts only. v0.2.0 replaced a
differing asset of a published mutable release when it was set. An
incomplete asset on a published release is reported, not removed. - A draft that lost a concurrent race is not deleted. When two runs
race for a tag, the losing run stops with a conflict and leaves its empty
draft for you to delete. run --snapshotdoes not run the formula stage, just as it already
skipped release and tap: a formula needs a tag. Naming formula, release or
tap in--stagestogether with--snapshotis an error.- A formula that installs the same file twice is rejected when it is
generated, as are the other install collisions above.
Known limitations
- Rust cross-builds: the Rust template builds each target with
cargo build --target, so every target needs its Rust target and a linker
installed. Alternatively, build on one runner per platform and import the
binaries as prebuilt. The template was verified for the host target
(x86_64-apple-darwin) only; its other targets were not built. - Recovery is tested against a fake GitHub API. Failures were injected
there: lost responses, concurrent runs, digest conflicts and failed tap
pull requests. Against the real GitHub API, the normal release path is
exercised by every release. - No conditional updates on GitHub: Hephaestus re-reads a release right
before each write. Another writer can still act in the single API call
between that read and the write. The release workflow serializes runs per
tag (concurrency), which avoids this in normal use. - Incomplete uploads wait: if a failed upload leaves an incomplete
asset, that asset is deleted only when it is more than 10 minutes old, so
that an upload by another run in progress is never removed. A re-run after
that time continues. - Prereleases and the tap:
brews[].skip_uploaddefaults tofalse, so
a prerelease is published to the tap unless you setskip_upload: auto
(asinitdoes) ortrue. The default changes only in a major release.
Installation
- Homebrew: when this release is published, the 1.0.0 formula goes to
magicdrive/homebrew-tap as
a pull request, which is merged after review. From then on,
brew install magicdrive/tap/hephaestusinstalls 1.0....
v1.0.0-rc.2
Hephaestus v1.0.0-rc.2
The second release candidate of Hephaestus 1.0. It fixes what dogfooding
v1.0.0-rc.1 found (snapshots of init configurations, the init formula
test, a false failure in make publish's verification) and hardens
concurrent and repeated releases: Hephaestus no longer deletes a release
in any situation. A prerelease: it is not marked latest and does not update
the Homebrew tap; v0.2.0 stays the latest stable release.
For everything new in 1.0 (preflight in the Action, status, init,
noarch archives, the recovery guide, the compatibility policy), see the
v1.0.0-rc.1 notes
and the changelog.
Fixed since rc.1
- Snapshots of
initconfigurations:run --snapshotskips the formula
stage as well as release and tap (all three need a tag). Withbrews
configured, which everyinittemplate has, rc.1'srun --snapshot
failed with "a tag is required". Naming one of them in--stageswith
--snapshotis an error. initformula test: the templates' test is
system bin/"NAME", "--version"; rc.1's"#{bin}/NAME"failed
brew audit --strict.make publishverification: right after the release run, GitHub can
report the job's steps only partially. The tap-step check now reads them
again (6 reads, 20 seconds apart) and, if they are still incomplete,
reports the check as inconclusive (exit 3, re-runmake publish-watch)
instead of failing a correct release. A step with the wrong result is
still a failure. rc.1'smake publishexited 1 for this reason; the
release itself was correct.
Changed: release safety
- No release is ever deleted. When two runs race for one tag, the run
whose new draft loses (to another run's lower-id draft, or to a published
release) now stops with a conflict and leaves its empty draft for you to
delete. rc.1 deleted that draft, but once a draft is listed another run
may already be using it. The GitHub client has no call to delete a
release. - The draft is re-read right before it is published. A release
published meanwhile is not edited; a draft whose assets changed (added,
removed, incomplete or different) or that lost to another release is not
published. A draft is also re-read before one of its assets is deleted
and beforedraft: trueupdates its title or notes, and after publishing
each asset's digest is checked before its URL is recorded in the
manifest. - A published release's assets are never replaced or deleted, on
mutable releases too.replace_existing_assetsnow applies to drafts
only; an incomplete asset on a published release is reported, not
removed. (rc.1 and v0.2.0 replaced a differing asset of a published
mutable release whenreplace_existing_assets: truewas set.) - An incomplete asset seen right after a failed upload is deleted only
when it is older than the 10-minute grace period: a recent one cannot be
told apart from another run's upload in progress, so the asset fails and
a later re-run continues.
Testing
- Regression tests for each fix: snapshots of every
inittemplate (Go,
script and prebuilt in CI; Rust when cargo and the template's four
targets are installed), the formula test line, and lagging step data
(complete later; never complete is inconclusive). - Recovery and concurrency tests against a fake GitHub API: the losing
draft (to a lower-id draft and to a published release) deletes nothing
and a re-run continues with the winner; a lost create reusing another
run's draft; the pre-publish re-read (published meanwhile, asset added,
changed or removed, another release published); a draft published
before an asset delete or adraft: trueupdate; another build's
content published meanwhile; starters after a failed
upload; published assets never replaced or deleted. Plus the rc.1 suite:
lost create, upload and publish responses, digest conflicts, tag pointing
to another commit, immutable re-runs, tap pull request failure after
publishing.
Compatibility
- v0.2.0 and rc.1 configurations load unchanged. No configuration key,
CLI flag, Action input or output, or check ID was removed or renamed. - Two behaviors are narrower, both for safety:
replace_existing_assets
no longer touches a published release, and a lost concurrent race stops
with a conflict instead of deleting a draft. brews[].skip_uploadstill defaults tofalse;inittemplates set
skip_upload: auto.
Limitations
- Windows arm64 archives are built but not run (no CI runner).
- The Rust template does not cross-compile by itself: each target needs its
Rust target and a linker, or a build per runner imported as prebuilt
binaries. - Recovery is tested against a fake GitHub API; against the real API only
the normal release path is exercised. The attestation step is covered by
real releases, not by a test. - GitHub has no conditional update: between a re-read and the write that
follows it (one API call), another writer could still change the
release. - A transient upload failure that leaves an incomplete asset now fails the
run instead of retrying at once; re-run after 10 minutes.
Full Changelog: v1.0.0-rc.1...v1.0.0-rc.2