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
|
# 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
# `file` is not on the runner image; ldd answers the only question
# that matters, and running it proves the thing actually starts.
ldd "$NAME" 2>&1 | grep -q "not a dynamic executable"
./"$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
upload_url=$(field upload_url < release.json | cut -d'{' -f1)
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
|