Skip to content

Commit 3810bfd

Browse files
committed
feat: mirror-zig-toolchain composite action
Fetch a pinned Zig toolchain, verify every tarball against upstream's minisign key (signature + trusted-comment filename check), and upload it to a GitHub Release the repo controls. For restricted-egress environments (github.com-only sandboxes that reach neither ziglang.org nor the community mirrors) and for retention of purged -dev builds. Generalizes the mirror workflow hand-rolled in chicoxyzzy/cynic so any repo can delegate to it.
0 parents  commit 3810bfd

5 files changed

Lines changed: 381 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: test-${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15+
16+
jobs:
17+
mirror-and-verify:
18+
name: Mirror a tagged release and verify assets
19+
# Needs contents:write to manage a release, which fork PRs can't be
20+
# granted — so skip on PRs from forks; push and same-repo PRs run it.
21+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: write
25+
steps:
26+
- name: Check out repository
27+
uses: actions/checkout@v4
28+
29+
- name: Run the action (single platform, throwaway tag)
30+
uses: ./
31+
with:
32+
version: 0.14.1
33+
platforms: x86_64-linux
34+
release-tag: ci-test
35+
release-title: CI test (safe to delete)
36+
37+
- name: Assert the expected assets exist on the release
38+
env:
39+
GH_TOKEN: ${{ github.token }}
40+
run: |
41+
set -euo pipefail
42+
assets="$(gh release view ci-test --json assets -q '.assets[].name')"
43+
echo "$assets"
44+
for want in \
45+
zig-x86_64-linux-0.14.1.tar.xz \
46+
zig-x86_64-linux-0.14.1.tar.xz.minisig \
47+
SHA256SUMS; do
48+
echo "$assets" | grep -qx "$want" || { echo "missing asset: $want"; exit 1; }
49+
done
50+
echo "all expected assets present"
51+
52+
- name: Clean up the throwaway release
53+
if: always()
54+
env:
55+
GH_TOKEN: ${{ github.token }}
56+
run: gh release delete ci-test --cleanup-tag --yes || true

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
*.tar.xz
3+
*.zip
4+
*.minisig
5+
SHA256SUMS

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Sergey Rubanov
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# mirror-zig-toolchain
2+
3+
A GitHub composite action that fetches a pinned Zig toolchain, verifies
4+
every byte against the official [minisign][minisign] key, and uploads it
5+
to a GitHub Release you control.
6+
7+
It exists for two situations the normal download path doesn't cover:
8+
9+
- **Restricted egress.** Some environments (sandboxed dev containers,
10+
locked-down CI) only allow a `github.com`-shaped hole and can reach
11+
neither `ziglang.org` nor the community mirrors. Mirroring the pinned
12+
tarball onto *your own* repo's Release makes it fetchable there.
13+
- **Retention.** `ziglang.org/builds` purges old `-dev` (nightly)
14+
builds, and the community mirrors keep them only so long. Copying the
15+
tarball onto a Release you control removes the retention clock — the
16+
bytes stay fetchable for as long as the pin lives in your
17+
`build.zig.zon`.
18+
19+
It does **not** install Zig into a workflow — for that, use
20+
[xyzzylabs/setup-zig][setup-zig]. This action *produces* a mirror;
21+
setup-zig (or a small bootstrap script) *consumes* one.
22+
23+
## Usage
24+
25+
Mirror the version pinned in `build.zig.zon` onto a `zig-toolchain`
26+
release on the current repo:
27+
28+
```yaml
29+
name: mirror-zig
30+
on:
31+
workflow_dispatch:
32+
permissions:
33+
contents: write
34+
jobs:
35+
mirror:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v4
39+
- uses: xyzzylabs/mirror-zig-toolchain@v1
40+
```
41+
42+
Pin an explicit version and a subset of platforms:
43+
44+
```yaml
45+
- uses: xyzzylabs/mirror-zig-toolchain@v1
46+
with:
47+
version: 0.17.0-dev.1275+59a628c6d
48+
platforms: |
49+
x86_64-linux
50+
aarch64-linux
51+
```
52+
53+
## Inputs
54+
55+
| Name | Default | Description |
56+
|-----------------|----------------------------------|-------------------------------------------------------------------------------------------------|
57+
| `version` | _auto_ | Zig version to mirror. Empty → read `.minimum_zig_version` from `version-file`. |
58+
| `version-file` | `build.zig.zon` | File to read the version from when `version` is empty. |
59+
| `platforms` | the four x86_64/aarch64 × linux/macos | Space- or newline-separated `<arch>-<os>` targets, in Zig's tarball naming. |
60+
| `release-tag` | `zig-toolchain` | Release tag to upload to. Created if missing; existing assets are kept. |
61+
| `release-title` | `Zig toolchain mirror` | Title used only when the release is first created. |
62+
| `mirror` | `''` | Optional single mirror base URL to try first. Cannot be `ziglang.org`. |
63+
| `token` | `${{ github.token }}` | Token with `contents: write` to manage the release. |
64+
65+
## What it uploads
66+
67+
For each requested platform: the `.tar.xz` (or `.zip` for Windows)
68+
tarball and its `.minisig` sidecar, plus a single `SHA256SUMS` covering
69+
all tarballs. Re-running for a new pin adds the new version's assets and
70+
leaves the previous ones in place, so an in-flight branch on the old pin
71+
keeps working.
72+
73+
## How the verification works
74+
75+
For every tarball, the action tries your optional `mirror`, then the
76+
live community mirror list (hardcoded fallback if `ziglang.org` is
77+
down), then `ziglang.org` itself. The first source whose bytes pass
78+
**both** checks wins:
79+
80+
1. `minisign -V` against upstream's public key (signature + trusted
81+
comment).
82+
2. The signed filename in the trusted comment matches the requested
83+
tarball — so a validly-signed but *different* tarball can't be passed
84+
off under the requested name.
85+
86+
An unverifiable tarball is never uploaded. Requires `minisign` on the
87+
runner; the action installs it via `apt-get` or `brew` if absent
88+
(ubuntu and macOS runners work out of the box).
89+
90+
## Consuming the mirror
91+
92+
In CI, point [xyzzylabs/setup-zig][setup-zig] at your release, or in a
93+
restricted local sandbox, fetch the asset directly, e.g.:
94+
95+
```sh
96+
gh release download zig-toolchain \
97+
--repo <owner>/<repo> \
98+
--pattern 'zig-x86_64-linux-<version>.tar.xz*'
99+
minisign -Vm zig-x86_64-linux-<version>.tar.xz \
100+
-P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
101+
```
102+
103+
## License
104+
105+
MIT. See [LICENSE](LICENSE).
106+
107+
[minisign]: https://jedisct1.github.io/minisign/
108+
[setup-zig]: https://github.com/xyzzylabs/setup-zig

action.yml

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
name: 'Mirror Zig toolchain'
2+
description: 'Fetch a pinned Zig toolchain, verify it against the official minisign key, and upload it to a GitHub Release for restricted-egress or long-retention use.'
3+
author: 'Sergey Rubanov'
4+
branding:
5+
icon: 'hard-drive'
6+
color: 'orange'
7+
8+
inputs:
9+
version:
10+
description: >
11+
Zig version to mirror (e.g. "0.14.1" or
12+
"0.17.0-dev.1275+59a628c6d"). Leave empty to read
13+
`.minimum_zig_version` from `version-file`.
14+
required: false
15+
default: ''
16+
version-file:
17+
description: 'Path to a build.zig.zon to read the version from when `version` is empty.'
18+
required: false
19+
default: 'build.zig.zon'
20+
platforms:
21+
description: >
22+
Space- or newline-separated <arch>-<os> targets to mirror,
23+
e.g. "x86_64-linux aarch64-macos". Arch/OS use Zig's tarball
24+
naming (x86_64, aarch64 / linux, macos, windows).
25+
required: false
26+
default: |
27+
x86_64-linux
28+
aarch64-linux
29+
x86_64-macos
30+
aarch64-macos
31+
release-tag:
32+
description: 'GitHub Release tag to upload assets to. Created if it does not exist.'
33+
required: false
34+
default: 'zig-toolchain'
35+
release-title:
36+
description: 'Title for the release when it is first created.'
37+
required: false
38+
default: 'Zig toolchain mirror'
39+
mirror:
40+
description: >
41+
Optional single mirror base URL to try first
42+
(e.g. "https://pkg.machengine.org/zig"). Cannot be ziglang.org.
43+
required: false
44+
default: ''
45+
token:
46+
description: 'GitHub token with `contents: write` to manage the release.'
47+
required: false
48+
default: ${{ github.token }}
49+
50+
runs:
51+
using: composite
52+
steps:
53+
- name: Ensure minisign is available
54+
shell: bash
55+
run: |
56+
set -euo pipefail
57+
if command -v minisign >/dev/null 2>&1; then exit 0; fi
58+
if command -v apt-get >/dev/null 2>&1; then
59+
sudo apt-get update && sudo apt-get install -y minisign
60+
elif command -v brew >/dev/null 2>&1; then
61+
brew install minisign
62+
else
63+
echo "mirror-zig-toolchain: minisign is required but not installed, and no apt-get/brew found." >&2
64+
exit 1
65+
fi
66+
67+
- name: Fetch, verify, and upload the pinned toolchain
68+
shell: bash
69+
env:
70+
GH_TOKEN: ${{ inputs.token }}
71+
IN_VERSION: ${{ inputs.version }}
72+
IN_VERSION_FILE: ${{ inputs.version-file }}
73+
IN_PLATFORMS: ${{ inputs.platforms }}
74+
IN_RELEASE_TAG: ${{ inputs.release-tag }}
75+
IN_RELEASE_TITLE: ${{ inputs.release-title }}
76+
IN_MIRROR: ${{ inputs.mirror }}
77+
run: |
78+
set -euo pipefail
79+
80+
# Upstream's minisign public key, from https://ziglang.org/download
81+
# (the same constant mlugg/setup-zig and xyzzylabs/setup-zig pin).
82+
MINISIGN_KEY='RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U'
83+
SOURCE_TAG='mirror-zig-toolchain'
84+
85+
log() { printf '%s\n' "$*" >&2; }
86+
die() { log "mirror-zig-toolchain: ERROR: $*"; exit 1; }
87+
88+
if [ -n "$IN_MIRROR" ]; then
89+
case "$IN_MIRROR" in
90+
*ziglang.org*) die "ziglang.org cannot be used as a mirror override" ;;
91+
esac
92+
fi
93+
94+
VERSION="$IN_VERSION"
95+
if [ -z "$VERSION" ]; then
96+
[ -f "$IN_VERSION_FILE" ] || die "version is empty and $IN_VERSION_FILE not found"
97+
VERSION="$(grep -m1 '\.minimum_zig_version' "$IN_VERSION_FILE" | sed 's/.*"\(.*\)".*/\1/')"
98+
[ -n "$VERSION" ] || die "could not read .minimum_zig_version from $IN_VERSION_FILE"
99+
fi
100+
log "Zig version to mirror: $VERSION"
101+
102+
# Live community mirror list, falling back to the hardcoded set
103+
# (mlugg/setup-zig's fallback list) if ziglang.org is unreachable.
104+
MIRRORS="$(curl -fsSL --max-time 15 https://ziglang.org/download/community-mirrors.txt || true)"
105+
if [ -z "$MIRRORS" ]; then
106+
MIRRORS='https://pkg.machengine.org/zig
107+
https://zigmirror.hryx.net/zig
108+
https://zig.linus.dev/zig
109+
https://zig.squirl.dev
110+
https://zig.florent.dev
111+
https://zig.mirror.mschae23.de/zig
112+
https://zigmirror.meox.dev
113+
https://ziglang.freetls.fastly.net
114+
https://zig.tilok.dev
115+
https://zig-mirror.tsimnet.eu/zig
116+
https://zig.karearl.com/zig
117+
https://pkg.earth/zig
118+
https://fs.liujiacai.net/zigbuilds'
119+
fi
120+
121+
case "$VERSION" in
122+
*-dev*) CANONICAL='https://ziglang.org/builds' ;;
123+
*) CANONICAL="https://ziglang.org/download/$VERSION" ;;
124+
esac
125+
126+
# Verify a downloaded tarball + sidecar against upstream's key and
127+
# confirm the signed filename matches what we requested.
128+
verify() {
129+
local tarball="$1" sig="$2" expected="$3"
130+
minisign -Vm "$tarball" -x "$sig" -P "$MINISIGN_KEY" >/dev/null 2>&1 || return 1
131+
local signed
132+
signed="$(sed -n '3p' "$sig" | grep -oE 'file:[^[:space:]]+' | head -1 | cut -d: -f2-)"
133+
[ "$signed" = "$expected" ] || return 1
134+
return 0
135+
}
136+
137+
ASSETS=()
138+
# shellcheck disable=SC2086
139+
for plat in $IN_PLATFORMS; do
140+
case "$plat" in
141+
*-*) : ;;
142+
*) die "invalid platform '$plat' (expected <arch>-<os>)" ;;
143+
esac
144+
arch="${plat%-*}"
145+
os="${plat##*-}"
146+
ext='.tar.xz'
147+
[ "$os" = 'windows' ] && ext='.zip'
148+
tarball="zig-${arch}-${os}-${VERSION}${ext}"
149+
sig="${tarball}.minisig"
150+
log "== $tarball =="
151+
152+
ok=0
153+
SOURCES=()
154+
[ -n "$IN_MIRROR" ] && SOURCES+=("$IN_MIRROR")
155+
while IFS= read -r m; do
156+
m="$(printf '%s' "$m" | tr -d '[:space:]')"
157+
[ -n "$m" ] && SOURCES+=("$m")
158+
done <<< "$MIRRORS"
159+
SOURCES+=("$CANONICAL")
160+
161+
for base in "${SOURCES[@]}"; do
162+
log " trying $base"
163+
curl -fsSL --max-time 300 "$base/$tarball?source=$SOURCE_TAG" -o "$tarball" || { rm -f "$tarball"; continue; }
164+
curl -fsSL --max-time 60 "$base/$sig?source=$SOURCE_TAG" -o "$sig" || { rm -f "$tarball" "$sig"; continue; }
165+
if verify "$tarball" "$sig" "$tarball"; then
166+
log " verified from $base"
167+
ok=1
168+
break
169+
fi
170+
log " verification failed; discarding"
171+
rm -f "$tarball" "$sig"
172+
done
173+
[ "$ok" = 1 ] || die "could not obtain a verified $tarball from any source"
174+
ASSETS+=("$tarball" "$sig")
175+
done
176+
177+
[ "${#ASSETS[@]}" -gt 0 ] || die "no assets were produced"
178+
179+
# Checksums over every mirrored tarball (not the sidecars).
180+
sha256sum zig-*"$VERSION"*.tar.* > SHA256SUMS 2>/dev/null || sha256sum zig-* | grep -v '\.minisig' > SHA256SUMS
181+
ASSETS+=("SHA256SUMS")
182+
183+
NOTES="Minisign-verified copies of the Zig toolchain (\`$VERSION\`), for environments that cannot reach ziglang.org or the community mirrors. Every tarball was verified against upstream's minisign key before upload."
184+
185+
if ! gh release view "$IN_RELEASE_TAG" >/dev/null 2>&1; then
186+
log "Creating release $IN_RELEASE_TAG"
187+
gh release create "$IN_RELEASE_TAG" --title "$IN_RELEASE_TITLE" --notes "$NOTES"
188+
fi
189+
log "Uploading ${#ASSETS[@]} assets to $IN_RELEASE_TAG"
190+
gh release upload "$IN_RELEASE_TAG" "${ASSETS[@]}" --clobber
191+
log "Done."

0 commit comments

Comments
 (0)