diff --git a/.github/workflows/release-rust.yml b/.github/workflows/release-rust.yml index 6ca9468..1309197 100644 --- a/.github/workflows/release-rust.yml +++ b/.github/workflows/release-rust.yml @@ -67,6 +67,29 @@ jobs: outputs: matrix: ${{ steps.plan.outputs.matrix }} steps: + - name: harden_runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: block + allowed-endpoints: > + github.com:443 + ziglang.org: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. workflow_sha pins to the exact + # commit of release-workflows this run is executing from, + # regardless of what ref the caller specified. + repository: purpleclay/release-workflows + ref: ${{ github.workflow_sha }} + persist-credentials: false + sparse-checkout: | + .github/zig-mirrors.txt + sparse-checkout-cone-mode: false + - name: require_tag_ref env: REF_TYPE: ${{ github.ref_type }} @@ -84,7 +107,40 @@ jobs: TARGETS: ${{ inputs.targets }} run: | set -euo pipefail - if ! matrix=$(jq -c ' + + # cargo needs both the registry index and the actual crate-file + # host on every leg, plus the toolchain installer — verified + # against real harden-runner audit logs, not guessed (see #23). + common_endpoints="github.com:443 index.crates.io:443 static.crates.io:443 static.rust-lang.org:443" + + # 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 a single fixed + # mirror isn't viable — but 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. + live_mirrors=$(curl -fsSL https://ziglang.org/download/community-mirrors.txt \ + | sed -E 's#^https://##; s#/.*$##' | sort -u) + reviewed_mirrors=$(sed -E 's#^https://##; s#/.*$##' .github/zig-mirrors.txt | sort -u) + zig_mirrors=$(comm -12 <(echo "$live_mirrors") <(echo "$reviewed_mirrors") | sed 's/$/:443/' | tr '\n' ' ') + + if [ -z "$zig_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 + + zigbuild_endpoints="${common_endpoints} release-assets.githubusercontent.com:443 ziglang.org:443 ${zig_mirrors}" + + if ! matrix=$(jq -c \ + --arg common "$common_endpoints" \ + --arg zigbuild_endpoints "$zigbuild_endpoints" \ + ' if type != "array" then error("targets must be a JSON array of strings") elif length == 0 then @@ -96,10 +152,10 @@ jobs: else . end | [ .[] | . as $t | { target: $t } + - ( { "x86_64-unknown-linux-musl": { "runs-on": "ubuntu-24.04", "zigbuild": true }, - "aarch64-unknown-linux-musl": { "runs-on": "ubuntu-24.04", "zigbuild": true }, - "x86_64-apple-darwin": { "runs-on": "macos-15", "zigbuild": false }, - "aarch64-apple-darwin": { "runs-on": "macos-15", "zigbuild": false } + ( { "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-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)") ) ]' <<<"$TARGETS"); then echo "::error::release-rust supports: x86_64-unknown-linux-musl, aarch64-unknown-linux-musl, x86_64-apple-darwin, aarch64-apple-darwin" @@ -120,9 +176,10 @@ jobs: contents: read steps: - name: harden_runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: - egress-policy: audit + egress-policy: block + allowed-endpoints: ${{ matrix.allowed-endpoints }} - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -166,14 +223,14 @@ jobs: tool: cargo-zigbuild@${{ env.ZIGBUILD_VERSION }} checksum: true - - name: Build (zigbuild) + - name: build (zigbuild) if: ${{ matrix.zigbuild }} env: BIN: ${{ inputs.bin }} TARGET: ${{ matrix.target }} run: cargo zigbuild --locked --release --target "$TARGET" --bin "$BIN" - - name: Build (native) + - name: build (native) if: ${{ !matrix.zigbuild }} env: BIN: ${{ inputs.bin }} @@ -232,9 +289,19 @@ jobs: release-url: ${{ steps.publish.outputs.url }} steps: - name: harden_runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2 + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: - egress-policy: audit + egress-policy: block + allowed-endpoints: > + api.github.com:443 + fulcio.sigstore.dev:443 + github.com:443 + rekor.sigstore.dev:443 + release-assets.githubusercontent.com:443 + tmaproduction.blob.core.windows.net:443 + tuf-repo-cdn.sigstore.dev:443 + tuf-repo.github.com:443 + uploads.github.com:443 - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 158bd5e..31bf584 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,12 @@ jobs: - name: harden_runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: - egress-policy: audit + egress-policy: block + allowed-endpoints: > + api.github.com:443 + cafe.github.com:443 + github.com:443 + release-assets.githubusercontent.com:443 - name: checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/zig-mirrors.md b/.github/zig-mirrors.md new file mode 100644 index 0000000..3280093 --- /dev/null +++ b/.github/zig-mirrors.md @@ -0,0 +1,33 @@ +# 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 + +```sh +diff <(curl -fsSL https://ziglang.org/download/community-mirrors.txt | sort) \ + <(sort .github/zig-mirrors.txt) +``` + +Lines prefixed `<` are in the committed file but no longer live upstream — safe to drop, no review needed, they can't be reached anyway. Lines prefixed `>` are new upstream entries — this is the part that needs actual scrutiny, not a rubber stamp. + +## 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. Confirm it resolves and serves a real Zig release tarball. It should return `200`, not a redirect to somewhere unexpected or a `404`. + ```sh + curl -sIL "https:///zig-x86_64-linux-.tar.xz" + ``` +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 commit — a normal PR, reviewed the same as any other change to this workflow. diff --git a/.github/zig-mirrors.txt b/.github/zig-mirrors.txt new file mode 100644 index 0000000..d43254d --- /dev/null +++ b/.github/zig-mirrors.txt @@ -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 diff --git a/.github/zizmor.yml b/.github/zizmor.yml index df8f06e..6052fd6 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,13 +1,13 @@ rules: superfluous-actions: ignore: - # release-rust.yml:139 — dtolnay/rust-toolchain: explicit toolchain + + # release-rust.yml:196 — dtolnay/rust-toolchain: explicit toolchain + # cross-target install; hand-rolling rustup here would just reimplement # this action with less auditability. - - release-rust.yml:139 + - release-rust.yml:196 stale-action-refs: ignore: - # release-rust.yml:139 — dtolnay/rust-toolchain intentionally ships no + # release-rust.yml:196 — 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:139 + - release-rust.yml:196 diff --git a/.gitignore b/.gitignore index ff91a0f..8a37c15 100644 --- a/.gitignore +++ b/.gitignore @@ -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 @@ -15,6 +17,7 @@ !README.md !RELEASE.md !SECURITY.md +!typos.toml # Recurse through sub-directories applying the same patterns !*/ diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000..8b6e555 --- /dev/null +++ b/typos.toml @@ -0,0 +1,5 @@ +[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"]