action.yml at the repository root is a composite action. It installs the
Hephaestus release binary that belongs to the commit you reference, then runs
the hephaestus command in your workspace: the local stages (build,
archive, checksum) in a step without tokens, then the publishing stages
(release, formula, tap) in a step with them. It does not check out
your repository and does not install your build toolchain: add
actions/checkout and, for example, actions/setup-go before it.
- uses: magicdrive/hephaestus@<full commit SHA of the release tag> # vX.Y.Z
with:
command: run
stages: build,archive,checksumPin the action to the full commit SHA that a release tag points to (for
v1.2.0: git rev-parse v1.2.0^{commit}), or reference it by that tag. The
action does not compile anything. Its first step
(action/install-hephaestus.sh) runs before
any of your build code, and does the following:
- Reads the release named in
action/VERSIONof the pinned commit. The release workflow refuses to publish a tag that this file does not name, so a release commit always names its own release. - Downloads
hephaestus_<version>_<os>_<arch>.tar.gz(Windows:.zip) andhephaestus_checksums.txt, fromhttps://github.com/magicdrive/hephaestus/releases/download/<tag>/only (https, redirects https only). - Checks the archive's SHA-256 against the checksum file.
- Verifies the archive's provenance attestation with
gh attestation verify. The attestation must have been signed bymagicdrive/hephaestus/.github/workflows/release.ymlon a GitHub-hosted runner, forrefs/tags/<tag>and, when pinned by SHA, for exactly that commit (--source-digest). This binds the binary to the commit you pinned. - Extracts only the regular file
hephaestus[.exe]at the archive root: links, directories, nested paths and duplicates are rejected, and no other member is written. It then checks that the binary reportshephaestus <tag>.
Any failure stops the action. There is no fallback, and no other version is
tried. A branch reference (@main) or the tag of another release fails,
because the action could not tell which binary belongs to it.
Requirements: a GitHub-hosted runner, or another runner with bash,
curl, tar and gh, on Linux, macOS or Windows, x64 or ARM64. The step
uses the job's github.token for the read-only attestation lookup. No step
that runs your code sees that token, and it does not depend on the
github-token input.
No Go (or any other toolchain) is downloaded or set up, and nothing is
written to $GITHUB_PATH or $GITHUB_ENV. Your build commands run with the
PATH, go and GOTOOLCHAIN that your own steps set up, before and after
the action. A runner without Go can run the action for a project that is
not written in Go. CI compares the environment before and after the action
on six platforms (action-isolation, action-install).
Earlier versions behaved differently:
- v1.1.0 ran
actions/setup-go. That step put its Go first onPATHand exportedGOTOOLCHAIN=localfor the rest of the job. - The unreleased v1.1.1 fixes compiled Hephaestus with a private Go. They ship in v1.2.0.
From v1.2.0, a project that relied on the action's Go must set up its own Go, as the examples always did.
When a workflow runs the action from its own checkout of this repository
(uses: ./), the version in action/VERSION is usually not released yet.
Only in that case does the action use the binary named by the
HEPHAESTUS_ACTION_BINARY environment variable. Hephaestus's CI sets it to
a binary built from the same commit (the action-binary job). Every other
caller ignores the variable.
The action runs on Linux, macOS and Windows runners (CI runs action-smoke
and action-boundary on all three). Its steps use bash; on Windows that is
Git Bash, paths are passed to Go as D:/a/_temp-style paths and the binary
is hephaestus.exe. Build commands named bash are found by PATH; on
Windows put Git Bash first (C:\Program Files\Git\bin) if a WSL bash
could shadow it.
| Input | Default | Meaning |
|---|---|---|
command |
run |
run, check, build, archive, checksum, release, formula, tap, preflight or plan (release plan). Any other value fails the step. |
stages |
"" |
With command: run: comma-separated stages (build,archive,checksum,release,formula,tap) or all. Empty runs every stage the configuration declares. Ignored by other commands. |
config |
"" |
Configuration file, relative to working-directory. Empty: hephaestus.yaml (or .yml, .hephaestus.yaml, .hephaestus.yml). |
tag |
"" |
Release tag. Empty: the pushed tag (github.ref_name when the workflow runs for a tag). |
strict |
"false" |
With command: preflight: "true" treats WARN and UNKNOWN checks as failures. |
offline |
"false" |
With command: preflight: "true" runs only the checks that need no network. |
snapshot |
"false" |
"true" builds without a tag (version 0.0.0-SNAPSHOT). Configured release, formula and tap stages (they need a tag) are skipped; naming them in stages is an error. |
dry-run |
"false" |
"true" plans release and tap writes without performing them (reads still use the tokens when given). |
fresh |
"false" |
"true" ignores an existing manifest instead of continuing from it, and deletes the outputs it records that this run writes again (--fresh). |
working-directory |
. |
Directory to run in (the project root when the configuration is there). |
github-token |
${{ github.token }} |
Token for the release repository, exported as GITHUB_TOKEN. Needs contents: write to publish. Set only in the publishing step (release, formula, tap, and the publish phase of run). |
tap-token |
"" |
Token for the Homebrew tap, exported as HOMEBREW_TAP_GITHUB_TOKEN. contents: write (and pull-requests: write for pull request mode) on the tap. GITHUB_TOKEN cannot write to another repository. Set only in the publishing step, like github-token. |
tag, snapshot, dry-run, fresh and stages are not passed to
command: check. Inputs reach the script through environment variables, never
by ${{ }} interpolation into shell code, and command is checked against
the list above.
| Output | Value |
|---|---|
status |
published, no-op, dry-run or failed. no-op also covers runs that write nothing to GitHub (for example build, archive and checksum only). |
no-op |
"true" when the run succeeded and changed nothing on GitHub. |
version |
The version being released (1.2.3, or 0.0.0-SNAPSHOT). |
artifacts |
JSON list of archives and checksum files: [{"name", "path", "sha256", "url"}]. url is empty until the release is published. |
release-url |
URL of the GitHub Release (set when the release stage ran). |
release-id |
Numeric id of the GitHub Release. |
uploaded-assets |
JSON list of asset names uploaded or replaced by this run. |
formula-paths |
Newline-separated, project-relative paths of generated formulae. |
formula-commit-sha |
Newline-separated tap commit SHAs written by this run. |
pull-request-url |
Newline-separated tap pull request URLs (opened or reused). |
preflight-status |
With command: preflight: pass, warn, unknown or fail. |
preflight-exit-code |
With command: preflight: 0 pass, 1 fail (or strict WARN/UNKNOWN), 2 usage, 3 some checks unknown. The step fails on any non-zero code. |
preflight-report |
With command: preflight: path of the JSON report (schema). |
prerelease |
With command: plan: "true" when the tag is released as a prerelease. |
tap |
With command: plan: publish when the release updates the Homebrew tap, skip otherwise. |
The step also writes a Markdown summary (stages, artifacts with SHA-256,
release assets and their status, formulae, tap results, warnings). Known
token values are replaced by *** in errors, the summary and outputs, and
tokens are registered with ::add-mask:: for the log.
| Stage | GITHUB_TOKEN permissions |
Tap token |
|---|---|---|
check, build, archive, checksum, formula |
contents: read (checkout only) |
— |
release |
contents: write |
— |
tap |
— | contents: write on the tap; pull-requests: write in pull request mode |
Declare permissions: contents: read at the top of the workflow and grant
contents: write only to the job that publishes. id-token: write and
attestations: write are needed only if you add
actions/attest-build-provenance.
- Two tokens, two scopes. The workflow's
GITHUB_TOKENpublishes the release in the same repository. It cannot write to another repository, so the tap needs its own token. Keep them separate; never use one personal access token for both. - Tap token: a GitHub App installation token or a fine-grained personal
access token, limited to the tap repository with
contents: writeandpull-requests: write. A GitHub App token also makes the tap commits signed by GitHub. Avoid classic personal access tokens. - Store it in an environment (for example
release) with protection rules (required reviewers, tag-only deployment), not as a plain repository secret. - Give each step only what it needs. The action runs
check,build,archive,checksumand the local phase ofrun(--phase local) in a step whose environment has no token at all, andrelease,formula,tapand the publish phase ofrun(--phase publish) in a separate step that gets them. Steps inherit the job's and the workflow'senv, so the local step blanksGITHUB_TOKEN,GH_TOKEN,GITHUB_PAT,HOMEBREW_TAP_GITHUB_TOKEN, the GitHub Enterprise token names and the runner's OIDC request token; pass tokens through the action's inputs, never through job or workflowenv, because a token under any other name would still be inherited by the local step. CI checks this boundary on real Linux, macOS and Windows runners (action-boundaryinci.yml:/procon Linux,ps -Eon macOS, the build's own environment on Windows). Hephaestus also removes the tokens, the variablesrepository.tokenreads, the runner'sACTIONS_*tokens and credential-looking variables from the environment of build commands unless a build names them in itsenv. - That is not isolation. Build code runs as the same user as everything
else in the job. It can read the environment of the processes around it
(
ps -Eon macOS,/proc/PID/environof the step's shell on Linux; Hephaestus itself is non-dumpable on Linux), and a process left running by a build step can wait for the publishing step and read its environment. When you publish, build and publish in separate jobs (see below). - Templates do not expose tokens.
{{ .Env.NAME }}hides the token variables, the variablesrepository.tokenreads, and variables whose name containsTOKEN,SECRET,PASSWORD,PASSWD,PASSPHRASE,CREDENTIAL,PRIVATE_KEY,API_KEY,APIKEYorACCESS_KEY, or has a_-separated segmentPAT,AUTH,KEY,PASSorPWD(except the shell'sPWDandOLDPWD), unless they are declared in the configuration's top-levelenv. A rendered value that contains a token is an error. Onlybuilds[].envvalues andbrews[].repository.tokensee the complete environment. Name detection is a heuristic: keep credentials under recognizable names. - Set
github-token: ""on steps that do not publish (pull request CI, snapshot builds).
command: plan prints what a release workflow does for a tag
(hephaestus publish plan, publish.md). It sets the outputs
prerelease and tap, so the steps that use the tap token can be skipped
for a prerelease and never receive the token. The tag is the tag input,
or the pushed tag. The plan reads only the configuration: it needs no token
and runs no project code. A prerelease whose plan would update the tap fails
the step. hephaestus publish expects a release job built this way:
- name: Plan the release
id: plan
uses: magicdrive/hephaestus@<full commit SHA of a release tag> # vX.Y.Z
with:
command: plan
- name: Generate the formula and open a tap pull request
if: steps.plan.outputs.tap == 'publish'
uses: magicdrive/hephaestus@<full commit SHA of a release tag> # vX.Y.Z
with:
command: run
stages: formula,tap
github-token: ""
tap-token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}command: preflight runs hephaestus preflight — read-only
release safety checks — before you push a tag, e.g. in a manually dispatched
"prepare release" workflow:
permissions:
contents: read
actions: read
jobs:
preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: magicdrive/hephaestus@<full-commit-sha>
id: preflight
with:
command: preflight
tag: ${{ inputs.tag }}
strict: "true"It writes the human-readable report to the log, an annotation for every
FAIL, WARN and UNKNOWN, a summary table, and the JSON report to the
file in the preflight-report output. The step's exit code is the
command's. The token (github-token) is only read with; checks that need
more than the workflow token can read (secret names, rulesets) are
UNKNOWN, which fails with strict: "true". The preflight runs no
repository code, so this step may hold the token.
For anything you publish, run the build in a job that holds no write token and no tap token, and publish from another job:
- Build job —
permissions: contents: read, noenvironment, no secrets. Runcommand: runwithstages: build,archive,checksumandgithub-token: "", then uploaddist/(includingdist/hephaestus-manifest.json) withactions/upload-artifact. - Publish job —
needs: build,permissions: contents: write,environment: release. Download the artifact intodist/and runcommand: runwithstages: release,formula,tapand the tokens.
Jobs run on separate runners, so nothing the build started can observe the publishing job. A complete file is release-split-jobs.yml.
A single job (the example below) is simpler and still keeps tokens out of the build step's environment, but it does not isolate them: code from the build step that keeps running (a daemon started by a build script, for example) can read a later step's environment. Hephaestus warns when one process runs build and publishing stages with a token set.
The configuration runs commands (builds[].command), and a build compiles
code from the checked-out tree. Treat hephaestus.yaml like a workflow file:
- Never run publishing stages (
release,tap, orrunwithoutstages) onpull_requestorpull_request_target. Publish only from tag pushes (or a protectedworkflow_dispatch). - Never combine
pull_request_target(orworkflow_run) with a checkout of the pull request's head. That gives the fork's code your secrets and a write token. - For pull request CI, use
snapshot: "true",stages: build,archive,checksum,github-token: "", notap-token, andpermissions: contents: read. - Protect
hephaestus.yaml, build scripts and release notes withCODEOWNERS. In particular review changes tobuilds[].command,release.github.api_url/upload_url,notes_fileand template.Envreferences. - Use
concurrency: release-${{ github.ref }}withcancel-in-progress: falseon the release workflow, so two runs for one tag do not race.
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- name: Build, archive, checksum
uses: magicdrive/hephaestus@<full-commit-sha>
with:
command: run
stages: build,archive,checksum
github-token: ""
- name: Publish the GitHub Release
uses: magicdrive/hephaestus@<full-commit-sha>
with:
command: release
- name: Formula and tap pull request
id: brew
uses: magicdrive/hephaestus@<full-commit-sha>
with:
command: run
stages: formula,tap
github-token: ""
tap-token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}Each step continues from the manifest the previous one saved in dist/.
Complete files are in examples/workflows:
| File | Pattern |
|---|---|
release-single-job.yml |
The example above. |
release-split-jobs.yml |
Build in a job with contents: read; pass dist/ (including the manifest) as an artifact to a publishing job with contents: write and the release environment. Build code never runs where a write token exists. |
goreleaser-formula-only.yml |
GoReleaser builds and releases; Hephaestus imports dist/artifacts.json and publishes only the Homebrew formula. |
matrix-then-publish.yml |
Build natively on several runners (matrix), upload the binaries, then package and publish them in one job with the existing provider. |
pull-request.yml |
Snapshot build on pull requests without any token. |
Notes for the split pattern: the publishing job trusts the dist/ artifact
the build job produced (the release stage re-hashes every file against the
manifest, which detects corruption but not a build job that wrote a
consistent but malicious manifest). Keep both jobs in the same workflow run,
triggered only by tags.
- Every run of the action downloads and verifies the release binary (about 5 MB) again; nothing is cached between steps, which other steps could modify.
GITHUB_SHAprovides.Commit;GITHUB_REPOSITORYprovides the release repository unless the configuration names one;GITHUB_API_URLprovides the API URL on GitHub Enterprise Server.- The exit code is non-zero when any stage fails; outputs and the summary are written in every case.