# 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(' /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