1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
# Cut a release binary when a v* tag is pushed.
#
# nimstatic builds itself: a throwaway host build of the tool produces the
# static one that ships, which means the release artifact is also the test that
# the tool works on a clean machine.
name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Existing tag to rebuild; defaults to the newest v* tag"
required: false
permissions:
contents: write # required — a job is read-only unless it asks
env:
NIM_VERSION: "2.2.4"
ZIG_VERSION: "0.15.1"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # tags, so a dispatch can find the latest one
# One place decides which tag is being released: the dispatch input, the
# tag that triggered the run, or — for a bare dispatch, which carries no
# inputs through the API — the newest v* tag. Anything else is a mistake
# worth stopping for, since the alternative is a release named after a
# branch.
- name: Resolve the tag
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
# Every variable here is read with a default: this runner does not
# set GITHUB_REF_TYPE, and under `set -u` one missing name is a dead
# job three steps before anything interesting happens.
REF="${GITHUB_REF:-}"
if [ -n "${INPUT_TAG:-}" ]; then
TAG="$INPUT_TAG"
else
case "$REF" in
refs/tags/*) TAG="${REF#refs/tags/}" ;;
*)
# A dispatch: no tag in the ref, so ask the API for the newest.
TAG=$(curl -sSfL -H "Authorization: Bearer $GITHUB_TOKEN" \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/tags" \
| python3 -c "import json,sys; ts=[t['name'] for t in json.load(sys.stdin) if t['name'].startswith('v')]; print(ts[0] if ts else '')")
;;
esac
fi
case "$TAG" in
v*) ;;
*) echo "refusing to release '$TAG': not a v* tag" >&2; exit 1 ;;
esac
# The runner hands a tag build the right tree already; a dispatch
# gets whatever branch it ran on, so move only when we can.
if [ -d .git ]; then
git checkout --detach "$TAG" 2>/dev/null || echo "note: building the checked-out tree, not $TAG"
fi
echo "releasing $TAG"
echo "TAG=$TAG" >> "$GITHUB_ENV"
# Toolchains come straight from upstream tarballs rather than setup
# actions: two curls, pinned versions, nothing else to trust.
- name: Install Nim and zig
run: |
set -euo pipefail
mkdir -p "$HOME/toolchains"
curl -sSfL "https://nim-lang.org/download/nim-${NIM_VERSION}-linux_x64.tar.xz" \
| tar -xJ -C "$HOME/toolchains"
curl -sSfL "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz" \
| tar -xJ -C "$HOME/toolchains"
echo "$HOME/toolchains/nim-${NIM_VERSION}/bin" >> "$GITHUB_PATH"
echo "$HOME/toolchains/zig-x86_64-linux-${ZIG_VERSION}" >> "$GITHUB_PATH"
- name: Versions
run: |
nim --version | head -1
zig version
# Alpine packages are cached so a re-run does not re-download them.
- uses: actions/cache@v4
with:
path: ~/.cache/nimstatic
key: nimstatic-alpine-${{ runner.os }}-v3.21
- name: Test
id: test
run: nim c -d:ssl --hints:off -r tests/test_nimstatic.nim
- name: Bootstrap nimstatic on the host
run: nim c -d:release -d:ssl --hints:off -o:nimstatic-host src/nimstatic.nim
- name: Build the static binary with itself
run: |
set -euo pipefail
VERSION="${TAG#v}"
NAME="nimstatic-${VERSION}-x86_64-linux"
./nimstatic-host src/nimstatic.nim -o "$NAME" -- -d:ssl --passL:-s
# Neither file(1) nor ldd is on the runner image, and `ldd … | grep`
# fails silently when ldd is missing — the pipe eats the error and
# grep just finds nothing. Read the ELF program headers instead: a
# PT_INTERP entry (type 3) is what makes a binary dynamic.
python3 -c "import struct,sys;d=open(sys.argv[1],'rb').read();assert d[:4]==b'\x7fELF';off=struct.unpack_from('<Q',d,0x20)[0];esz=struct.unpack_from('<H',d,0x36)[0];n=struct.unpack_from('<H',d,0x38)[0];t=[struct.unpack_from('<I',d,off+i*esz)[0] for i in range(n)];sys.exit('dynamic: has PT_INTERP' if 3 in t else 0)" "$NAME"
./"$NAME" --help > /dev/null
xz -9e -k "$NAME"
sha256sum "$NAME" "$NAME.xz" > SHA256SUMS
cat SHA256SUMS
echo "NAME=$NAME" >> "$GITHUB_ENV"
# The release exists already (the tag was pushed, or it is being rebuilt),
# so this attaches assets to it rather than creating one. The upload host
# is whatever the API itself advertises in upload_url, which is the one
# value that cannot go stale.
# The release exists already (the tag was pushed, or it is being rebuilt),
# so this attaches assets to it rather than creating one. The upload host
# is whatever the API advertises in upload_url — the one value that
# cannot go stale.
- name: Attach the binaries to the release
run: |
set -euo pipefail
api() { curl -sSfL -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" "$@"; }
field() { python3 -c "import json,sys; print(json.load(sys.stdin).get(sys.argv[1],''))" "$1"; }
if ! api "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/tags/$TAG" > release.json; then
api -X POST "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"nimstatic $TAG\"}" > release.json
fi
# Prefer the upload_url the API advertises; fall back to the
# id-based path when this forge does not send one.
upload_url=$(field upload_url < release.json | cut -d'{' -f1)
if [ -z "$upload_url" ]; then
release_id=$(field id < release.json)
[ -n "$release_id" ] || { echo "no release id in:"; cat release.json; exit 1; }
upload_url="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/$release_id/assets"
fi
echo "uploading to $upload_url"
for asset in "$NAME" "$NAME.xz" SHA256SUMS; do
# Replace an asset of the same name, so a re-run is idempotent.
existing=$(python3 -c "import json,sys; print(next((a['url'] for a in (json.load(open('release.json')).get('assets') or []) if a['name']==sys.argv[1]),''))" "$asset")
if [ -n "$existing" ]; then
api -X DELETE "$existing" > /dev/null
fi
api -X POST "$upload_url?name=$asset" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$asset" > /dev/null
echo "uploaded $asset"
done
- uses: actions/upload-artifact@v4
with:
name: nimstatic-x86_64-linux
path: |
nimstatic-*-x86_64-linux
nimstatic-*-x86_64-linux.xz
SHA256SUMS
|