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

InputDefaultWhat it does
target""The Zuke target to run. Omit to harden and check out only.
egress-policyauditaudit records outbound traffic; block enforces allowed-endpoints. See below.
allowed-endpoints""Space-separated host:port list permitted under block.
persist-credentialsfalseLeave the token in git config, for a later push.
fetch-depth1Commits 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.