The zuke-build/zuke action
The steps every Zuke job starts with — harden the runner, check out, run a
target — are published as a composite action on the GitHub
Marketplace, so a workflow does not repeat them. It is also what
cicd() emits as the first step
of every generated GitHub job, so Zuke's own workflows exercise the same
entry point it ships.
Using the action
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: zuke-build/zuke@v1
with:
target: ci It is the first step, with nothing before it: a remote action is fetched by the runner rather than from your workspace, so it needs no checkout — which is the point, since harden-runner only governs what runs after it.
Running a target needs a committed ./zuke launcher in the
repository (which is what zuke setup
writes); the step fails with an annotation saying so if there isn't one. A
job that only wants the hardening and the checkout omits
target and gets neither requirement.
Inputs
| Input | Default | What it does |
|---|---|---|
target | "" | The Zuke target to run. Omit to harden and check out only. |
egress-policy | audit | audit records outbound traffic; block enforces allowed-endpoints. See below. |
allowed-endpoints | "" | Space-separated host:port list permitted under block. |
persist-credentials | false | Leave the token in git config, for a later push. |
fetch-depth | 1 | Commits to fetch. 0 is the full history, which a secret scan needs. |
ref | "" | Branch, tag, or SHA to check out. Empty follows the event. Refused on secret-bearing events — see below. |
deno-version | "" | Install this Deno. Usually unnecessary — a committed ./zuke launcher bootstraps its own. |
Pin it, as you would any other action
@v1 is a tag that moves. Whoever can move it
can run code in your job — and because you have delegated your hardening to
this step, a moved tag could simply not harden. Pin the full commit SHA,
with the version beside it:
- uses: zuke-build/zuke@<40-character-sha> # v1.0.2
with:
target: ci
Dependabot bumps that line for you and writes the new version into the
comment. The @v1 form above is the shorter thing to read in a
snippet; it is not the thing to commit.
The v1 tag namespace belongs to the action alone. The
packages release under component tags (core-v1.9.0,
cli-v1.0.0, …), so v1 here means
this action and never "Zuke 1.0". See
Versioning.
egress-policy starts at audit, not block
Note the divergence: harden-runner's own default is block, and
this action's is audit. That is deliberate —
block with an empty allowed-endpoints fails a
build on its first outbound request, which is a poor first run — but it
means the default configuration records egress rather than
enforcing it. To actually enforce, run once on audit, take the
endpoint list from the run's insights, then set both:
- uses: zuke-build/zuke@<40-character-sha> # v1.0.2
with:
target: ci
egress-policy: block
allowed-endpoints: "jsr.io:443 deno.land:443 dl.deno.land:443"
One caveat worth confirming before you rely on it: harden-runner's
blocking support has historically been Linux-only on
GitHub-hosted runners, degrading to audit on macOS and Windows rather than
failing. A matrix job that sets block may therefore be
enforcing on one leg and recording on the others — check
harden-runner's
own docs for the version you have pinned.
ref is refused on every secret-bearing event
ref checks out something other than what the event points at —
the head branch of a pull request, say, so a job can push a fix back to it.
On pull_request that is ordinary: the token is read-only and
secrets are absent.
On pull_request_target it is not. That event
runs with the base repository's secrets and a writable token, so a
ref aimed at a contributor's head puts their zuke.ts in the workspace — and the last step executes it. No
configuration makes that safe, so the action refuses it outright:
Error: Refusing to check out a custom ref on a pull_request_target event: it
runs with this repository's secrets and a writable token, so checking out a ref
a contributor controls hands them both. Use 'pull_request', or drop the 'ref'
input.
The guard is an allow-list, not a deny-list for one event.
A custom ref is permitted only on pull_request,
push, merge_group,
workflow_dispatch, schedule, and
release; every other event — issue_comment,
workflow_run, and anything GitHub adds later — is refused. A
deny-list of what is dangerous today would be silently wrong the moment the
list grew.
It is also not keyed on target: a
ref checkout with no target is refused just the same, because a
caller who omits it and writes run: ./zuke ci in their own next
step reaches the identical outcome.
Generated workflows use it too
cicd() emits this action as
every generated GitHub job's prelude step, pinned by SHA
with its version comment, so the workflow Zuke writes for you starts exactly
the way the snippet above does. A job that already pins its own checkout
action, or whose steps cover the prelude, keeps what it declares.
class Ci extends Build {
// Every generated job opens with the pinned zuke-build/zuke action.
pipeline = cicd({ provider: "github" });
}
// Opt a job (or the whole pipeline) out of the prelude:
// cicd({ provider: "github", pipeline: { bootstrap: false } })
// …or pin the action yourself:
// cicd({ provider: "github", pipeline: { bootstrap: { action: { ref: "…", version: "v1.0.2" } } } })
Zuke's own workflows all open with
uses: zuke-build/zuke@<sha>, so the repository dogfoods
the entry point it publishes.
Log conventions
Independently of the action, when Zuke detects it is running under GitHub
Actions (GITHUB_ACTIONS=true) it switches to that runner's log
conventions automatically — no configuration:
- each target becomes a collapsible log group, so the workflow log stays tidy and every target is easy to find;
- a failing target emits an
::error::annotation, surfaced on the run and in the diff; - secret values are masked through the runner's own mask directive; and
- the per-target summary is written to the job summary as a table.
See Console output for the renderer behind it, and Code-first CI/CD for generating the workflow itself.