Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 106 additions & 18 deletions .github/workflows/release-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,49 @@ jobs:
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: block
# The reviewed mirror hosts below (all of .github/zig-mirrors.txt)
# must be probed for liveness before one can be pinned into the
# build matrix (see compose_build_matrix) — this list can't be
# derived from that file at runtime, since harden_runner is the
# very first step and runs before checkout_release_workflows.
# Keep in sync by hand: adding a mirror to zig-mirrors.txt means
# adding its host here too, or the probe can never reach it.
allowed-endpoints: >
github.com:443
ziglang.org:443
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fs.liujiacai.net:443
pkg.earth:443
pkg.hexops.org:443
zig-mirror.tsimnet.eu:443
zig.bcr.ist:443
zig.chainsafe.dev:443
zig.karearl.com:443
zig.linus.dev:443
zig.mirror.mschae23.de:443
zig.savalione.com:443
zig.squirl.dev:443
zig.tilok.dev:443
zig.vortan.dev:443
ziglang.freetls.fastly.net:443
zigmirror.com:443
zigmirror.hryx.net:443

- name: checkout_release_workflows
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# This reusable workflow's own repo content (the reviewed mirror
# list, see #31) — not the caller's, which is what a bare
# checkout would default to, and not github.workflow_sha either
# (that identifies the *caller's* workflow file, which won't
# exist as a commit in this repo at all). job.workflow_repository
# + job.workflow_sha are the ones documented for a reusable
# workflow checking out its own source.
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
Comment thread
purpleclay marked this conversation as resolved.
persist-credentials: false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
sparse-checkout: |
.github/zig-mirrors.txt
Comment thread
coderabbitai[bot] marked this conversation as resolved.
sparse-checkout-cone-mode: false

- name: require_tag_ref
env:
Expand All @@ -99,20 +140,67 @@ jobs:

# zigbuild legs additionally need taiki-e/install-action fetching
# the cargo-zigbuild binary from GitHub releases, and zig itself.
# mlugg/setup-zig picks a mirror at random per run (confirmed:
# two real runs picked two different ones), so rather than
# allowlisting whichever one we happened to observe, fetch the
# complete, authoritative list it draws from and allow all of it,
# plus ziglang.org as the documented last-resort fallback.
zig_mirrors=$(curl -fsSL https://ziglang.org/download/community-mirrors.txt \
| sed -E 's#^https://##; s#/.*$##' \
| sed 's/$/:443/' \
| tr '\n' ' ')
zigbuild_endpoints="${common_endpoints} release-assets.githubusercontent.com:443 ziglang.org:443 ${zig_mirrors}"
# community-mirrors.txt is mutable, externally maintained content,
# not something we've reviewed; trusting it directly would let an
# upstream compromise silently authorize a new build-time egress
# target with no review on our side (#31). Only trust a mirror
# that's BOTH currently live upstream AND already reviewed into
# .github/zig-mirrors.txt: a mirror going away upstream drops out
# with no PR needed; a new one only becomes trusted once someone
# reviews and commits it. Compare full base URLs, not just
# hostnames — a path change on an already-approved host (e.g. a
# hijacked /zig subpath) is exactly the kind of unreviewed change
# this is meant to catch, and it would slip through a
# hostname-only comparison.
live_mirrors=$(curl -fsSL https://ziglang.org/download/community-mirrors.txt \
| sed -E 's#/+$##' | sort -u)
reviewed_mirrors=$(sed -E 's#/+$##' .github/zig-mirrors.txt | sort -u)
approved_mirrors=$(comm -12 <(echo "$live_mirrors") <(echo "$reviewed_mirrors"))

if [ -z "$approved_mirrors" ]; then
echo "::error::no zig mirrors are both live upstream and reviewed in .github/zig-mirrors.txt — the reviewed list has likely gone stale, update it before retrying"
exit 1
fi

# Pin one specific, reviewed mirror for setup_zig's `mirror:`
# override rather than letting it pick at random from the live
# list (which would bypass the review above entirely). Probe
# candidates in random order and commit to the first that actually
# responds for the exact tarball this run needs: when `mirror:` is
# set, setup-zig does not retry or fall back to any other mirror
# on failure (confirmed against its source), so pinning one we
# haven't confirmed is reachable risks failing the whole leg with
# no in-run recovery. Both zigbuild legs run on ubuntu-24.04
# runners regardless of the cross-compile target, so there's only
# ever one filename to check. Reviewed mirrors serve it flat, at
# <base>/<file> — most do NOT mirror ziglang.org/download's own
# <base>/<version>/<file> layout, confirmed by probing all of
# them (9 of 16 404 with a version segment). HEAD, not a ranged
# GET: one reviewed mirror (zigmirror.com) rejects HEAD with 405,
# but a ranged GET fails on more (mirrors that ignore Range and
# stream the whole ~55MB file, timing out) — HEAD has fewer
# false negatives overall across the reviewed set.
zig_tarball="zig-x86_64-linux-${ZIG_VERSION}.tar.xz"
zig_mirror=""
while IFS= read -r candidate; do
if curl -fsSL --head --max-time 10 "${candidate}/${zig_tarball}" >/dev/null 2>&1; then
zig_mirror="$candidate"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
break
fi
done <<<"$(printf '%s\n' "$approved_mirrors" | shuf)"

if [ -z "$zig_mirror" ]; then
echo "::error::none of the approved zig mirrors responded for ${zig_tarball} — check mirror availability or review .github/zig-mirrors.txt"
exit 1
fi

zig_mirror_host=$(printf '%s' "$zig_mirror" | sed -E 's#^https://##; s#/.*$##')
zigbuild_endpoints="${common_endpoints} release-assets.githubusercontent.com:443 ${zig_mirror_host}:443"

if ! matrix=$(jq -c \
--arg common "$common_endpoints" \
--arg zigbuild_endpoints "$zigbuild_endpoints" \
--arg zig_mirror "$zig_mirror" \
'
if type != "array" then
error("targets must be a JSON array of strings")
Expand All @@ -125,8 +213,8 @@ jobs:
else . end
| [ .[] | . as $t |
{ target: $t } +
( { "x86_64-unknown-linux-musl": { "runs-on": "ubuntu-24.04", "zigbuild": true, "allowed-endpoints": $zigbuild_endpoints },
"aarch64-unknown-linux-musl": { "runs-on": "ubuntu-24.04", "zigbuild": true, "allowed-endpoints": $zigbuild_endpoints },
( { "x86_64-unknown-linux-musl": { "runs-on": "ubuntu-24.04", "zigbuild": true, "allowed-endpoints": $zigbuild_endpoints, "zig-mirror": $zig_mirror },
"aarch64-unknown-linux-musl": { "runs-on": "ubuntu-24.04", "zigbuild": true, "allowed-endpoints": $zigbuild_endpoints, "zig-mirror": $zig_mirror },
"x86_64-apple-darwin": { "runs-on": "macos-15", "zigbuild": false, "allowed-endpoints": $common },
"aarch64-apple-darwin": { "runs-on": "macos-15", "zigbuild": false, "allowed-endpoints": $common }
}[$t] // error("unsupported target: \($t)") )
Expand Down Expand Up @@ -179,12 +267,12 @@ jobs:
with:
version: ${{ env.ZIG_VERSION }}
use-cache: false
# No mirror override: ziglang.org is explicitly disallowed as one
# by the action itself (protects the official site from being
# hammered by CI). Default behaviour — a randomised community
# mirror, falling back to ziglang.org only as a last resort — is
# correct here; see #23 for tracking a move to a pinned mirror +
# egress-policy: block once we have enough audit data.
# Pinned to the specific mirror the plan job already probed and
# reviewed (#31) — without this, setup-zig fetches the live
# community-mirrors.txt itself and picks at random, which neither
# this workflow's review process nor its egress allowlist would
# ever see.
mirror: ${{ matrix.zig-mirror }}

- name: install_cargo_zigbuild
if: ${{ matrix.zigbuild }}
Expand Down
40 changes: 40 additions & 0 deletions .github/zig-mirrors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Keeping `zig-mirrors.txt` in sync

`zig-mirrors.txt` is the reviewed set of Zig community mirrors trusted as a build-job egress target for zigbuild legs. `release-rust.yml` only allows a mirror through if it's *both* in this file *and* currently live upstream (see [issue #31](https://github.com/purpleclay/release-workflows/issues/31) for why it isn't just fetched from upstream directly).

This is a deliberately manual process — a bot opening a routine PR risks training reviewers to rubber-stamp it, which defeats the point of requiring review at all. Automating this is tracked as a possible future issue if the manual upkeep ever becomes a real burden; it isn't expected to.

## When to check

There's no fixed schedule. Worth checking when:

- A zigbuild release fails with "no zig mirrors are both live upstream and reviewed" (`compose_build_matrix`'s hard-failure path) — this means the two lists have diverged enough to share nothing at all, and needs attention immediately.
- Roughly every few months, or whenever you're touching this workflow for another reason anyway.

## How to check

```bash
diff <(curl -fsSL https://ziglang.org/download/community-mirrors.txt | sort) \
<(sort .github/zig-mirrors.txt)
```

Lines prefixed `<` are new upstream entries — this is the part that needs actual scrutiny, not a rubber stamp. Lines prefixed `>` are in the committed file but no longer live upstream — safe to drop, no review needed, they can't be reached anyway.

## Reviewing a new entry

For each newly-added host, actually verify it before adding it — don't just accept it because it's on the official list. At minimum:

1. Use the mirror's exact base URL as listed upstream, including its path — the path is part of what's being trusted, not incidental, and `release-rust.yml` compares the full URL when deciding what to approve (see #31). Most reviewed mirrors serve the archive flat, at `<base>/<file>` — unlike `ziglang.org/download` itself, which nests it under `<base>/<version>/<file>`; try the flat form first (only a minority of mirrors need the version segment). Download the archive *and* its `.minisig` sibling, then verify against the [Zig Software Foundation's public key](https://ziglang.org/download/) — a `200` response alone proves nothing, since a malicious mirror could serve any tarball it likes:

```sh
archive=zig-x86_64-linux-<version>.tar.xz
curl -fsSL "https://<mirror-base-url>/$archive" -o "$archive"
curl -fsSL "https://<mirror-base-url>/$archive.minisig" -o "$archive.minisig"
minisign -Vm "$archive" -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
```

`minisign` only checks that the signature matches the downloaded bytes — it does not check that the signed file is the one you actually asked for. A mirror could substitute the signature and archive for a *different*, still-genuinely-signed release. Verify that manually: the command above prints a `Trusted comment` line containing a `file:` field — confirm it reads exactly `file:zig-x86_64-linux-<version>.tar.xz`, matching the archive name you requested.

2. Check who operates it, if that's discoverable (project README, DNS WHOIS, whether it's referenced elsewhere in the Zig community). Prefer mirrors run by identifiable people/organizations over anonymous ones.

Then update `.github/zig-mirrors.txt` to match upstream, **and** add the new host to the `plan` job's `harden_runner` `allowed-endpoints` in `release-rust.yml` — `compose_build_matrix` probes reviewed mirrors for liveness before pinning one, and that probe can't reach a host harden-runner hasn't been told about. Commit both together as a normal PR, reviewed the same as any other change to this workflow.
16 changes: 16 additions & 0 deletions .github/zig-mirrors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
https://fs.liujiacai.net/zigbuilds
https://pkg.earth/zig
https://pkg.hexops.org/zig
https://zig-mirror.tsimnet.eu/zig
https://zig.bcr.ist
https://zig.chainsafe.dev
https://zig.karearl.com/zig
https://zig.linus.dev/zig
https://zig.mirror.mschae23.de/zig
https://zig.savalione.com
https://zig.squirl.dev
https://zig.tilok.dev
https://zig.vortan.dev/zig
https://ziglang.freetls.fastly.net
https://zigmirror.com
https://zigmirror.hryx.net/zig
8 changes: 4 additions & 4 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
rules:
superfluous-actions:
ignore:
# release-rust.yml:169 — dtolnay/rust-toolchain: explicit toolchain +
# release-rust.yml:250 — dtolnay/rust-toolchain: explicit toolchain +
# cross-target install; hand-rolling rustup here would just reimplement
# this action with less auditability.
- release-rust.yml:169
- release-rust.yml:250
stale-action-refs:
ignore:
# release-rust.yml:169 — dtolnay/rust-toolchain intentionally ships no
# release-rust.yml:250 — dtolnay/rust-toolchain intentionally ships no
# tags: master/stable/beta/nightly are branch aliases by design, so
# this pin is the tip of "master", not a stale reference.
- release-rust.yml:169
- release-rust.yml:250
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
!.envrc
!.github/renovate.json
!.github/workflows/*.yml
!.github/zig-mirrors.md
!.github/zig-mirrors.txt
!.github/zizmor.yml
!.gitignore
!.zed/settings.json
Expand All @@ -15,6 +17,7 @@
!README.md
!RELEASE.md
!SECURITY.md
!typos.toml

# Recurse through sub-directories applying the same patterns
!*/
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
buildInputs =
[
alejandra
minisign
nil
typos
zizmor
Expand Down
12 changes: 12 additions & 0 deletions typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[files]
# A list of third-party hostnames, not prose — spellcheck will keep
# producing false positives here as mirrors are added/removed over time
# (e.g. "zig.bcr.ist", "zig.squirl.dev" are real domains, not typos).
extend-exclude = [".github/zig-mirrors.txt"]

[default.extend-words]
# The same mirror hostnames also appear in release-rust.yml's harden_runner
# allowed-endpoints (can't be excluded like zig-mirrors.txt above, since
# that's a real code file). Keep in sync with zig-mirrors.txt.
ist = "ist"
squirl = "squirl"