fix(ci): cache save and restore must share one path — every save was … #129
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| on: | |
| push: | |
| tags: ["v*"] | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: "Build and verify everything, publish nothing" | |
| type: boolean | |
| default: true | |
| record_hashes: | |
| description: "Record artifact hashes instead of verifying them" | |
| type: boolean | |
| default: false | |
| targets: | |
| description: "Comma-separated target triples to build (empty = all). Iterating on one lane should not build six." | |
| type: string | |
| default: "" | |
| skip_napi: | |
| description: "Skip the @dcl/abgen-node matrix" | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: "0" | |
| SOURCE_DATE_EPOCH: "315532800" | |
| ABGEN_GIT_REV: ${{ github.sha }} | |
| jobs: | |
| build-id: | |
| name: build id | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| actions: read | |
| outputs: | |
| value: ${{ steps.eval.outputs.value }} | |
| matrix: ${{ steps.matrix.outputs.value }} | |
| has_build: ${{ steps.matrix.outputs.has_build }} | |
| promote: ${{ steps.matrix.outputs.promote }} | |
| has_promote: ${{ steps.matrix.outputs.has_promote }} | |
| napi_promote: ${{ steps.matrix.outputs.napi_promote }} | |
| napi_ids: ${{ steps.matrix.outputs.napi_ids }} | |
| image_docker: ${{ steps.matrix.outputs.image_docker }} | |
| image_lambda: ${{ steps.matrix.outputs.image_lambda }} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 | |
| - name: evaluate .#buildId | |
| id: eval | |
| run: | | |
| set -eu | |
| flags=(--extra-experimental-features nix-command | |
| --extra-experimental-features flakes) | |
| # Evaluation only, no build: .#buildId is a content id over the | |
| # filtered source tree, so it is a pure function of what will be | |
| # compiled and is identical on every runner. | |
| value="$(nix eval --raw "${flags[@]}" .#buildId)" | |
| case "$value" in | |
| *[!0-9a-f]*) | |
| echo "buildId is not lowercase hex: $value" >&2; exit 1 ;; | |
| esac | |
| if [ "${#value}" -ne 12 ]; then | |
| echo "buildId must be 12 chars, got ${#value}: $value" >&2; exit 1 | |
| fi | |
| echo "ABGEN_BUILD_ID=$value" | |
| echo "value=$value" >> "$GITHUB_OUTPUT" | |
| - name: select targets and locate promotable artifacts | |
| id: matrix | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| WANT: ${{ inputs.targets }} | |
| RECORDING: ${{ inputs.record_hashes }} | |
| BUILD_ID: ${{ steps.eval.outputs.value }} | |
| run: | | |
| set -eu | |
| # buildId is global: any source change moves every target's artifacts. | |
| # Recording a filtered subset would leave the unbuilt targets' hashes | |
| # describing a tree that no longer exists, and the next tag would fail | |
| # the gate on them. Narrowing is for iteration, not for recording. | |
| if [ -n "${WANT:-}" ] && [ "${RECORDING:-false}" = "true" ]; then | |
| echo "record_hashes needs the full matrix; drop 'targets'" >&2 | |
| exit 1 | |
| fi | |
| all='[ | |
| {"target":"x86_64-unknown-linux-gnu","runner":"ubuntu-24.04","builder":"nix"}, | |
| {"target":"aarch64-unknown-linux-gnu","runner":"ubuntu-24.04-arm","builder":"nix"}, | |
| {"target":"x86_64-pc-windows-gnu","runner":"ubuntu-24.04","builder":"rustup","bin":"abgen.exe"}, | |
| {"target":"aarch64-pc-windows-gnullvm","runner":"ubuntu-24.04","builder":"rustup","bin":"abgen.exe"}, | |
| {"target":"aarch64-apple-darwin","runner":"macos-15","builder":"rustup","bin":"abgen"}, | |
| {"target":"x86_64-apple-darwin","runner":"macos-15","builder":"rustup","bin":"abgen"} | |
| ]' | |
| if [ -z "${WANT:-}" ]; then | |
| sel=$(echo "$all" | jq -c .) | |
| else | |
| sel=$(echo "$all" | jq -c --arg w "$WANT" \ | |
| '($w | split(",") | map(gsub("^\\s+|\\s+$";""))) as $want | |
| | map(select(.target as $t | $want | index($t))) as $sel | |
| | if ($sel | length) == 0 | |
| then error("no target matched: \($w)") | |
| else $sel end') | |
| fi | |
| # Input-addressed lookup: buildId is a content hash of everything | |
| # that reaches the compiler, so an unexpired artifact named | |
| # <kind>-<buildId>-<target> from a finished main run holds | |
| # byte-identical outputs for THIS tree. Promotion re-proves that | |
| # against the committed hash manifests before shipping a byte. | |
| # The current ref is also accepted so rehearsals on a branch can | |
| # promote their own previous build. Only this repo's own runs | |
| # count: a fork-PR run executes the fork's workflow files, can | |
| # upload arbitrary bytes under a precomputable name, and reports | |
| # the fork's branch name (often "main") as head_branch — but its | |
| # head_repository_id differs. Recording always builds. | |
| lookup() { | |
| [ "${RECORDING:-false}" = "true" ] && { echo ""; return; } | |
| gh api "/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$1&per_page=10" \ | |
| --jq '[.artifacts[] | |
| | select(.expired == false) | |
| | select(.workflow_run.head_repository_id == .workflow_run.repository_id) | |
| | select(.workflow_run.head_branch == "main" | |
| or .workflow_run.head_branch == env.GITHUB_REF_NAME) | |
| | select((.workflow_run.id | tostring) != env.GITHUB_RUN_ID)] | |
| | sort_by(.created_at) | reverse | |
| | if length == 0 then "" else (.[0].id | tostring) end' \ | |
| || echo "" | |
| } | |
| build_inc='[]' | |
| promote='[]' | |
| while read -r entry; do | |
| t=$(echo "$entry" | jq -r .target) | |
| id=$(lookup "archives-$BUILD_ID-$t") | |
| if [ -n "$id" ]; then | |
| promote=$(echo "$promote" | jq -c --argjson e "$entry" --arg id "$id" \ | |
| '. + [$e + {artifact_id: $id}]') | |
| else | |
| build_inc=$(echo "$build_inc" | jq -c --argjson e "$entry" '. + [$e]') | |
| fi | |
| done < <(echo "$sel" | jq -c '.[]') | |
| napi_ids='[]' | |
| napi_promote=true | |
| for t in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu \ | |
| aarch64-apple-darwin x86_64-apple-darwin x86_64-pc-windows-msvc; do | |
| id=$(lookup "napi-$BUILD_ID-$t") | |
| if [ -z "$id" ]; then napi_promote=false; break; fi | |
| napi_ids=$(echo "$napi_ids" | jq -c --arg id "$id" '. + [$id]') | |
| done | |
| [ "$napi_promote" = "true" ] || napi_ids='[]' | |
| image_docker=$(lookup "image-$BUILD_ID-dockerImage") | |
| image_lambda=$(lookup "image-$BUILD_ID-lambdaImage") | |
| { | |
| echo "value=$(jq -cn --argjson b "$build_inc" '{include: $b}')" | |
| echo "has_build=$(echo "$build_inc" | jq 'length > 0')" | |
| echo "promote=$promote" | |
| echo "has_promote=$(echo "$promote" | jq 'length > 0')" | |
| echo "napi_promote=$napi_promote" | |
| echo "napi_ids=$napi_ids" | |
| echo "image_docker=$image_docker" | |
| echo "image_lambda=$image_lambda" | |
| } | tee -a "$GITHUB_OUTPUT" | |
| build: | |
| name: ${{ matrix.target }} | |
| needs: build-id | |
| if: needs.build-id.outputs.has_build == 'true' | |
| runs-on: ${{ matrix.runner }} | |
| env: | |
| ABGEN_BUILD_ID: ${{ needs.build-id.outputs.value }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.build-id.outputs.matrix) }} | |
| steps: | |
| - name: setup (checkout + toolchains) | |
| run: | | |
| git init -q . | |
| git remote add origin "https://github.com/$GITHUB_REPOSITORY" | |
| git fetch -q --depth 1 origin "$GITHUB_SHA" | |
| git checkout -q FETCH_HEAD | |
| # SOURCE_DATE_EPOCH is pinned at the workflow level and nothing here | |
| # derives it. ABGEN_BUILD_ID comes from the build-id job; an empty | |
| # one would make crate/build.rs fall back to `git rev-parse` and put | |
| # us straight back into per-commit binaries, so fail here instead of | |
| # discovering it at hash-verify time. | |
| test -n "${ABGEN_BUILD_ID:-}" \ | |
| || { echo "ABGEN_BUILD_ID is empty (build-id job output)" >&2; exit 1; } | |
| echo "build id: $ABGEN_BUILD_ID rev: $ABGEN_GIT_REV epoch: $SOURCE_DATE_EPOCH" | |
| # Rustup legs only (nix legs exit below and never read these): | |
| # reproduces what the nix sandbox gives for free — a fixed | |
| # /build/source and $HOME — so a runner build doesn't bake | |
| # /home/runner/work/... into paths. Overwritten below with target | |
| # additions; the later GITHUB_ENV write wins. | |
| # | |
| # $HOME before $PWD is load-bearing: $PWD ($HOME/work/abgen/abgen) | |
| # nests inside $HOME, and gcc applies the LAST matching | |
| # -ffile-prefix-map, so reversing the order breaks the /build | |
| # mapping. The $HOME map covers vendored C built from ~/.cargo | |
| # (e.g. cxx's cxx.cc). | |
| { | |
| echo "RUSTFLAGS=--remap-path-prefix $PWD=/build --remap-path-prefix $HOME=/home" | |
| echo "CFLAGS=-ffile-prefix-map=$HOME=/home -ffile-prefix-map=$PWD=/build" | |
| echo "CXXFLAGS=-ffile-prefix-map=$HOME=/home -ffile-prefix-map=$PWD=/build" | |
| } >> "$GITHUB_ENV" | |
| if [ "${{ matrix.builder }}" = "nix" ]; then | |
| exit 0 | |
| fi | |
| ( | |
| if ! command -v rustup >/dev/null 2>&1; then | |
| curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs \ | |
| | sh -s -- -y --default-toolchain none --profile minimal --no-modify-path | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| export PATH="$HOME/.cargo/bin:$PATH" | |
| fi | |
| rustup toolchain install 1.97.1 --profile minimal --no-self-update | |
| rustup default 1.97.1 | |
| rustup target add ${{ matrix.target }} | |
| ) > /tmp/rustup.log 2>&1 & | |
| rustup_pid=$! | |
| if [ "${{ matrix.target }}" = "x86_64-pc-windows-gnu" ]; then | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends g++-mingw-w64-x86-64-posix cmake | |
| # A search directory holding ONLY libstdc++.a, so `-lstdc++` cannot | |
| # resolve to an import library. | |
| # | |
| # -L is the only non-positional lever here: three build scripts | |
| # (link-cplusplus, meshopt, crunch) emit dylib-kind | |
| # `rustc-link-lib=stdc++` too early in argv for -C link-arg or | |
| # -Bstatic to retract, and meshopt hardcodes | |
| # .cpp_link_stdlib("stdc++") so no CXXSTDLIB override works | |
| # either. A search dir containing only libstdc++.a forces ld to | |
| # prefer it over libstdc++.dll.a for every emitter. Same trick as | |
| # aarch64-gnullvm below. | |
| STDCXX_A="$(x86_64-w64-mingw32-g++-posix -print-file-name=libstdc++.a)" | |
| case "$STDCXX_A" in | |
| /*) ;; | |
| *) echo "libstdc++.a unresolved: $STDCXX_A" >&2; exit 1 ;; | |
| esac | |
| # Its bytes end up inside abgen.dll, so it is a pinned build input: | |
| # record the digest alongside the artifact hashes. | |
| sha256sum "$STDCXX_A" | |
| sudo mkdir -p /opt/mingw-static-cxx | |
| sudo cp "$STDCXX_A" /opt/mingw-static-cxx/libstdc++.a | |
| ls -1 /opt/mingw-static-cxx | |
| cat > /tmp/mingw-toolchain.cmake <<'EOF' | |
| set(CMAKE_SYSTEM_NAME Windows) | |
| set(CMAKE_SYSTEM_PROCESSOR x86_64) | |
| set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix) | |
| set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix) | |
| set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) | |
| EOF | |
| { | |
| echo "CMAKE_TOOLCHAIN_FILE=/tmp/mingw-toolchain.cmake" | |
| echo "CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc-posix" | |
| echo "CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc-posix" | |
| echo "CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++-posix" | |
| echo "AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar" | |
| # Read by ci/stable-dlltool.sh, which -C dlltool= points at | |
| # below. Named here so the wrapper and the rest of the lane's | |
| # binutils cannot drift apart. | |
| echo "ABGEN_REAL_DLLTOOL=x86_64-w64-mingw32-dlltool" | |
| } >> "$GITHUB_ENV" | |
| fi | |
| if [ "${{ matrix.target }}" = "aarch64-pc-windows-gnullvm" ]; then | |
| curl -fsSL -o llvm-mingw.tar.xz https://github.com/mstorsjo/llvm-mingw/releases/download/20260616/llvm-mingw-20260616-ucrt-ubuntu-22.04-x86_64.tar.xz | |
| echo "534b92e067b22a6b4441f48ae9240a3341b17825d04d577eab0cf85c44b4deda llvm-mingw.tar.xz" | sha256sum -c | |
| sudo tar -xJf llvm-mingw.tar.xz -C /opt && rm llvm-mingw.tar.xz | |
| cat > /tmp/aarch64-mingw-toolchain.cmake <<'EOF' | |
| set(CMAKE_SYSTEM_NAME Windows) | |
| set(CMAKE_SYSTEM_PROCESSOR aarch64) | |
| set(CMAKE_C_COMPILER aarch64-w64-mingw32-clang) | |
| set(CMAKE_CXX_COMPILER aarch64-w64-mingw32-clang++) | |
| set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) | |
| EOF | |
| sudo rm /opt/llvm-mingw-20260616-ucrt-ubuntu-22.04-x86_64/aarch64-w64-mingw32/lib/libc++.dll.a \ | |
| /opt/llvm-mingw-20260616-ucrt-ubuntu-22.04-x86_64/aarch64-w64-mingw32/lib/libunwind.dll.a | |
| echo "/opt/llvm-mingw-20260616-ucrt-ubuntu-22.04-x86_64/bin" >> "$GITHUB_PATH" | |
| { | |
| echo "CMAKE_TOOLCHAIN_FILE=/tmp/aarch64-mingw-toolchain.cmake" | |
| echo "CARGO_TARGET_AARCH64_PC_WINDOWS_GNULLVM_LINKER=aarch64-w64-mingw32-clang" | |
| echo "CC_aarch64_pc_windows_gnullvm=aarch64-w64-mingw32-clang" | |
| echo "CXX_aarch64_pc_windows_gnullvm=aarch64-w64-mingw32-clang++" | |
| echo "AR_aarch64_pc_windows_gnullvm=aarch64-w64-mingw32-ar" | |
| echo "BINDGEN_EXTRA_CLANG_ARGS=--target=aarch64-w64-mingw32 --sysroot=/opt/llvm-mingw-20260616-ucrt-ubuntu-22.04-x86_64/aarch64-w64-mingw32" | |
| } >> "$GITHUB_ENV" | |
| fi | |
| wait "$rustup_pid" || { cat /tmp/rustup.log; exit 1; } | |
| cat /tmp/rustup.log | |
| rustflags="--remap-path-prefix $PWD=/build --remap-path-prefix $HOME=/home" | |
| if [ "${{ matrix.target }}" = "x86_64-pc-windows-gnu" ]; then | |
| rustflags="$rustflags -C link-arg=-Wl,--no-insert-timestamp" | |
| # Self-contained abgen.dll: +crt-static swaps -lgcc_s for the | |
| # static -lgcc_eh/-l:libpthread.a (removing libgcc_s_seh-1 and | |
| # libwinpthread-1); -static-libgcc is honoured by the gcc driver | |
| # (-static-libstdc++ is g++-only, a no-op here); the -L above | |
| # forces libstdc++ to resolve to the archive. | |
| rustflags="$rustflags -C target-feature=+crt-static -C link-arg=-static-libgcc" | |
| rustflags="$rustflags -L native=/opt/mingw-static-cxx" | |
| # rustc's raw-dylib import libs bake dlltool's per-invocation temp | |
| # path into their symbols, making builds nondeterministic; | |
| # ci/stable-dlltool.sh gives dlltool a content-addressed path | |
| # instead. -C dlltool=llvm-dlltool is NOT a fix — it leaves import | |
| # descriptors missing under GNU ld. Only this lane runs dlltool; | |
| # aarch64-pc-windows-gnullvm uses LLVM's in-process COFF writer. | |
| rustflags="$rustflags -C dlltool=$PWD/ci/stable-dlltool.sh" | |
| fi | |
| if [ "${{ matrix.target }}" = "aarch64-pc-windows-gnullvm" ]; then | |
| rustflags="$rustflags -C target-feature=+crt-static -C link-arg=-Wl,--no-insert-timestamp" | |
| fi | |
| { | |
| echo "RUSTFLAGS=$rustflags" | |
| # $HOME first — see the ordering note in the earlier flags step. | |
| echo "CFLAGS=-ffile-prefix-map=$HOME=/home -ffile-prefix-map=$PWD=/build" | |
| echo "CXXFLAGS=-ffile-prefix-map=$HOME=/home -ffile-prefix-map=$PWD=/build" | |
| } >> "$GITHUB_ENV" | |
| - if: matrix.builder == 'nix' | |
| uses: ./.github/actions/nix-store-cache | |
| - if: matrix.builder == 'rustup' | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-${{ matrix.target }}-1.97.1-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: cargo-${{ matrix.target }}-1.97.1- | |
| - name: build | |
| run: | | |
| if [ "${{ matrix.builder }}" = "nix" ]; then | |
| flags=(--extra-experimental-features nix-command | |
| --extra-experimental-features flakes) | |
| # All four artifacts build in one sandboxed derivation — splitting | |
| # them across `nix build` and `nix develop --command cargo build` | |
| # breaks hermeticity, since SOURCE_DATE_EPOCH only applies inside | |
| # the sandbox. | |
| # Quiet unless it fails; the retry rebuilds only the failed | |
| # derivation with its full log. | |
| nix build .#abgen-native "${flags[@]}" \ | |
| || nix build .#abgen-native --print-build-logs "${flags[@]}" | |
| if ! [[ "$GITHUB_REF" =~ ^refs/tags/ ]]; then | |
| case "${{ matrix.target }}" in | |
| x86_64-unknown-linux-gnu) image=dockerImage ;; | |
| aarch64-unknown-linux-gnu) image=lambdaImage ;; | |
| esac | |
| nix build ".#$image" --out-link image-result "${flags[@]}" \ | |
| || nix build ".#$image" --out-link image-result --print-build-logs "${flags[@]}" | |
| cp -L image-result "$image.tar.gz" | |
| fi | |
| # crane's installFromCargoBuildLog puts executables in bin/ and | |
| # cdylib/staticlib in lib/. Stage them where every later step | |
| # already looks, so the gate, packaging and the hash manifest stay | |
| # target-relative and identical across all six legs. | |
| out="target/${{ matrix.target }}/release" | |
| mkdir -p "$out" | |
| install -m755 result/bin/abgen "$out/abgen" | |
| install -m755 result/bin/abgen-host "$out/abgen-host" | |
| install -m644 result/lib/libabgen.so "$out/libabgen.so" | |
| install -m644 result/lib/libabgen.a "$out/libabgen.a" | |
| else | |
| cargo build --release --locked --target ${{ matrix.target }} --bin abgen | |
| cargo build --release --locked --target ${{ matrix.target }} -p abgen-native | |
| fi | |
| - name: windows self-containment | |
| if: matrix.target == 'x86_64-pc-windows-gnu' | |
| run: | | |
| set -eu | |
| rc=0 | |
| for f in abgen.dll abgen.exe abgen-host.exe; do | |
| imports=$(x86_64-w64-mingw32-objdump -p \ | |
| "target/${{ matrix.target }}/release/$f" \ | |
| | sed -n 's/^\tDLL Name: //p' | sort -u) | |
| echo "$f imports:"; echo "$imports" | sed 's/^/ /' | |
| if echo "$imports" | grep -qiE '^(libstdc\+\+-6|libgcc_s_seh-1|libwinpthread-1)\.dll$'; then | |
| echo "$f imports the MinGW runtime; it is not self-contained" >&2 | |
| rc=1 | |
| fi | |
| done | |
| [ "$rc" -eq 0 ] || exit 1 | |
| echo "windows-gnu artifacts are self-contained" | |
| - name: verify artifact hashes | |
| env: | |
| ABGEN_RECORD_HASHES: ${{ inputs.record_hashes && '1' || '0' }} | |
| ABGEN_HASH_SOFT: ${{ (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main') && '0' || '1' }} | |
| run: | | |
| set -eu | |
| # One artifact list for all six legs; nothing here is lane-specific. | |
| artifacts=("target/${{ matrix.target }}/release/${{ matrix.bin || 'abgen' }}") | |
| for f in libabgen.so libabgen.dylib abgen.dll libabgen.a abgen-host abgen-host.exe; do | |
| src="target/${{ matrix.target }}/release/$f" | |
| [ -f "$src" ] && artifacts+=("$src") | |
| done | |
| echo "artifacts: ${artifacts[*]}" | |
| bash ci/verify-artifact-hashes.sh "${{ matrix.target }}" "${artifacts[@]}" | |
| - name: upload recorded hashes | |
| if: ${{ inputs.record_hashes }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: hashes-${{ matrix.target }} | |
| path: ci/artifact-hashes/${{ matrix.target }}.sha256 | |
| if-no-files-found: error | |
| - name: package and smoke test | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ABGEN_RELEASE: ${{ github.ref_name }} | |
| run: | | |
| # Slashes in branch names are flattened: a nested archive path would | |
| # dodge the root-level upload glob below, and the input-addressed | |
| # artifact store would silently hold nothing (exactly what happened | |
| # to every branch dispatch under the old `mkdir -p` approach). Tags | |
| # never contain a slash, so their names are unchanged. | |
| refsafe="${GITHUB_REF_NAME:-dev}"; refsafe="${refsafe//\//-}" | |
| dist="abgen-${refsafe}-${{ matrix.target }}" | |
| mkdir -p "$dist" | |
| # No template/ or shader/ in the archive: those assets are compiled | |
| # into the binary. ABGEN_ROOT is a hard error when unreadable, so | |
| # shipping a directory risks a wrapper pointing at whatever a | |
| # partial extraction or relocation left behind. | |
| cp LICENSE README.md "$dist/" | |
| if [ "${{ matrix.builder }}" = "nix" ]; then | |
| mkdir -p "$dist/bin" "$dist/lib" | |
| install -m755 result/bin/abgen "$dist/bin/abgen.bin" | |
| for lib in $(ldd result/bin/abgen | awk '$3 ~ /^\// {print $3}'); do | |
| install -m644 "$lib" "$dist/lib/" | |
| done | |
| interp=$(readelf -l result/bin/abgen | sed -n 's/.*interpreter: \(.*\)]/\1/p') | |
| install -m755 "$interp" "$dist/lib/ld.so" | |
| # Sets no ABGEN_ROOT / ABGEN_SHADER_BUNDLE: the assets are compiled | |
| # in, and pointing those at a shipped directory would only create a | |
| # way to fail. A user who genuinely wants to override still can — | |
| # the binary reads both, and an unreadable one is a loud error | |
| # rather than a silent fallback. | |
| cat > "$dist/abgen" <<'EOF' | |
| #!/bin/sh | |
| here="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" | |
| exec "$here/lib/ld.so" --library-path "$here/lib" "$here/bin/abgen.bin" "$@" | |
| EOF | |
| chmod 755 "$dist/abgen" | |
| else | |
| cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" "$dist/" | |
| fi | |
| # The rev is not in the binaries any more, so it ships next to them. | |
| bash ci/write-build-info.sh "$dist" "${{ matrix.target }}" | |
| bash ci/pack-archive.sh "$dist" "$dist.tar.gz" "$SOURCE_DATE_EPOCH" | |
| # The sidecar is the only remaining answer to "which commit is this", | |
| # so assert it survived packing rather than trusting that it did. | |
| # Not piped into grep -q on purpose: the default shell here is | |
| # `bash -eo pipefail`, and grep exiting early would SIGPIPE tar into | |
| # a spurious 141. | |
| members="$(tar -tzf "$dist.tar.gz")" | |
| case "$members" in | |
| *"/BUILD-INFO.txt"*) ;; | |
| *) echo "BUILD-INFO.txt missing from $dist.tar.gz" >&2; exit 1 ;; | |
| esac | |
| shasum -a 256 "$dist.tar.gz" > "$dist.tar.gz.sha256" | |
| bash ci/verify-reproducible.sh "$dist" "$dist.tar.gz" "$SOURCE_DATE_EPOCH" | |
| # Cross-compiled legs (windows from linux, x86_64-apple-darwin from | |
| # the arm64 macos-15 runner) cannot exec their own output; smoke | |
| # only when the target matches the runner's arch. Rosetta would make | |
| # the x86_64 darwin binary "work" here, but only incidentally. | |
| case "${{ matrix.target }}" in | |
| x86_64-*) want_arch=x86_64 ;; | |
| aarch64-*) want_arch=arm64 ;; | |
| *) want_arch=none ;; | |
| esac | |
| run_arch="$(uname -m)" | |
| [ "$run_arch" = aarch64 ] && run_arch=arm64 | |
| can_smoke=false | |
| if [ "$run_arch" = "$want_arch" ] && ! echo "${{ matrix.target }}" | grep -q windows; then | |
| can_smoke=true | |
| fi | |
| if [ "$can_smoke" = true ]; then | |
| rm -rf /tmp/smoke && mkdir /tmp/smoke | |
| tar -xzf "$dist.tar.gz" -C /tmp/smoke | |
| ( | |
| cd "/tmp/smoke/$dist" | |
| ./abgen --version | |
| HTTP_SERVER_PORT=5199 ./abgen & | |
| server=$! | |
| for _ in $(seq 1 40); do | |
| curl -sf http://127.0.0.1:5199/readyz && ok=1 && break | |
| sleep 0.5 | |
| done | |
| kill "$server" || true | |
| test "${ok:-0}" = 1 | |
| ) | |
| fi | |
| nat="abgen-native-${refsafe}-${{ matrix.target }}" | |
| mkdir -p "$nat/lib" "$nat/include" | |
| cp crate/abgen-native/include/abgen.h "$nat/include/" | |
| cp LICENSE "$nat/" | |
| cp unity/README.md "$nat/README.md" | |
| # No libabgen.a: large, and `cargo build -p abgen-native` already | |
| # covers the need it would serve. | |
| for f in libabgen.so libabgen.dylib abgen.dll; do | |
| src="target/${{ matrix.target }}/release/$f" | |
| [ -f "$src" ] && cp "$src" "$nat/lib/" | |
| done | |
| # No MinGW runtime beside it: the windows-gnu binaries link the C++ | |
| # runtime statically and import nothing but system DLLs, which the | |
| # self-containment gate enforces. Shipping the DLLs anyway would | |
| # document a dependency that no longer exists. | |
| test -n "$(ls -A "$nat/lib")" || { echo "no native library built" >&2; exit 1; } | |
| # Unlike abgen-host below, a library dlopen'd into Unity runs on the | |
| # host process's glibc and cannot bundle its own, so the highest | |
| # GLIBC_x.y in its verneed table is the oldest distribution it loads | |
| # on. Gate the copy that actually ships, not a build-tree artifact. | |
| # Linux legs only: .dylib and .dll have no verneed table. | |
| if [ -f "$nat/lib/libabgen.so" ]; then | |
| bash ci/check-glibc-floor.sh 2.34 "$nat/lib/libabgen.so" | |
| fi | |
| # abgen-host, and on the nix legs its own glibc with it. | |
| # | |
| # A nix-built ELF's PT_INTERP is an absolute /nix/store path, so | |
| # without bundling the loader the helper only runs on the build | |
| # machine — and the smoke test below couldn't catch that, since it | |
| # also runs there. Bundling decouples it from the host's glibc | |
| # entirely, which libabgen.so can't do since it's dlopen'd into | |
| # someone else's process. | |
| # | |
| # The wrapper must forward the real command explicitly: abgen-host | |
| # re-execs itself so RLIMIT_AS binds before mimalloc reserves | |
| # arenas, and current_exe() under a loader is the loader itself, so | |
| # a naive re-exec form drops the args. | |
| if [ "${{ matrix.builder }}" = "nix" ]; then | |
| hb="target/${{ matrix.target }}/release/abgen-host" | |
| mkdir -p "$nat/bin" "$nat/host-lib" | |
| install -m755 "$hb" "$nat/bin/abgen-host.bin" | |
| for lib in $(ldd "$hb" | awk '$3 ~ /^\// {print $3}'); do | |
| install -m644 "$lib" "$nat/host-lib/" | |
| done | |
| interp=$(readelf -lW "$hb" | sed -n 's/.*interpreter: \(.*\)]/\1/p') | |
| install -m755 "$interp" "$nat/host-lib/ld.so" | |
| cat > "$nat/abgen-host" <<'EOF' | |
| #!/bin/sh | |
| here="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" | |
| ABGEN_HOST_LOADER="$here/host-lib/ld.so" | |
| ABGEN_HOST_LIBPATH="$here/host-lib" | |
| ABGEN_HOST_BIN="$here/bin/abgen-host.bin" | |
| export ABGEN_HOST_LOADER ABGEN_HOST_LIBPATH ABGEN_HOST_BIN | |
| exec "$ABGEN_HOST_LOADER" --library-path "$ABGEN_HOST_LIBPATH" \ | |
| "$ABGEN_HOST_BIN" "$@" | |
| EOF | |
| chmod 755 "$nat/abgen-host" | |
| else | |
| for f in abgen-host abgen-host.exe; do | |
| src="target/${{ matrix.target }}/release/$f" | |
| [ -f "$src" ] && install -m755 "$src" "$nat/" | |
| done | |
| fi | |
| bash ci/write-build-info.sh "$nat" "${{ matrix.target }}" | |
| bash ci/pack-archive.sh "$nat" "$nat.tar.gz" "$SOURCE_DATE_EPOCH" | |
| nat_members="$(tar -tzf "$nat.tar.gz")" | |
| case "$nat_members" in | |
| *"/BUILD-INFO.txt"*) ;; | |
| *) echo "BUILD-INFO.txt missing from $nat.tar.gz" >&2; exit 1 ;; | |
| esac | |
| shasum -a 256 "$nat.tar.gz" > "$nat.tar.gz.sha256" | |
| bash ci/verify-reproducible.sh "$nat" "$nat.tar.gz" "$SOURCE_DATE_EPOCH" | |
| # Windows legs cross-compile from Linux and are covered by the | |
| # windows CI job instead; cross-arch legs are skipped by the same | |
| # can_smoke computed above. | |
| if [ "$can_smoke" = true ]; then | |
| rm -rf /tmp/natsmoke && mkdir /tmp/natsmoke | |
| tar -xzf "$nat.tar.gz" -C /tmp/natsmoke | |
| got=$("/tmp/natsmoke/$nat/abgen-host" --version) | |
| test -n "$got" || { echo "abgen-host printed no version" >&2; exit 1; } | |
| echo "native smoke: abgen-host $got" | |
| # --version returns before apply_memory_limit, so the check above | |
| # cannot see a broken re-exec. Drive the real path: an empty stdin | |
| # makes the re-executed image fail reading its request, which is | |
| # EXIT_PROTOCOL (64). Anything else — notably ld.so's exit 1 on | |
| # "unrecognized option" — means the cap never bound. | |
| set +e | |
| "/tmp/natsmoke/$nat/abgen-host" --max-memory-mb 512 </dev/null | |
| rc=$? | |
| set -e | |
| case "$(uname -s)" in | |
| Darwin) | |
| # Darwin enforces no per-process memory rlimit — setrlimit | |
| # returns EINVAL at every size — so the helper refuses the flag | |
| # rather than pretending, and exits EXIT_LIMIT. Asserting the | |
| # refusal rather than skipping: a cap that silently did nothing | |
| # is exactly what the refusal exists to prevent. | |
| want=65; what="refusal" ;; | |
| *) | |
| # Linux re-execs to make RLIMIT_AS bind before mimalloc reserves | |
| # its arenas, then fails reading the request from empty stdin, | |
| # which is EXIT_PROTOCOL. Anything else — notably ld.so's exit 1 | |
| # on "unrecognized option" — means the cap never bound. | |
| want=64; what="re-exec" ;; | |
| esac | |
| test "$rc" -eq "$want" || { | |
| echo "abgen-host $what is broken: expected $want, got $rc" >&2; exit 1; } | |
| echo "native smoke: --max-memory-mb $what ok" | |
| # And prove the artifacts do not depend on this machine. A nix leg | |
| # ships its own loader precisely so the archive works off NixOS; | |
| # if anything still names a build-machine path as its interpreter, | |
| # it runs here and nowhere else, and this smoke would not notice. | |
| if [ "${{ matrix.builder }}" = "nix" ]; then | |
| bad=$(readelf -lW "/tmp/natsmoke/$nat/bin/abgen-host.bin" \ | |
| | sed -n 's/.*interpreter: \(.*\)]/\1/p') | |
| case "$bad" in | |
| /nix/store/*) ;; # expected: the bundled loader handles it | |
| *) echo "unexpected interpreter $bad" >&2; exit 1 ;; | |
| esac | |
| test -x "/tmp/natsmoke/$nat/host-lib/ld.so" \ | |
| || { echo "bundled loader missing from the archive" >&2; exit 1; } | |
| echo "native smoke: bundled loader present" | |
| fi | |
| fi | |
| - name: publish archives to draft release | |
| if: startsWith(github.ref, 'refs/tags/v') && inputs.dry_run != true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| dist="abgen-${GITHUB_REF_NAME}-${{ matrix.target }}" | |
| nat="abgen-native-${GITHUB_REF_NAME}-${{ matrix.target }}" | |
| # Created as a DRAFT and published only once every asset is in | |
| # place, because immutable releases freeze assets at publication: | |
| # after that only the title and notes can change. Published here, | |
| # the first target to finish would seal the release and the other | |
| # five would fail to upload. | |
| gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 \ | |
| || gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft \ | |
| --title "$GITHUB_REF_NAME" --notes-file .github/release-notes.md \ | |
| || gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null | |
| gh release upload "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --clobber \ | |
| "$dist.tar.gz" "$dist.tar.gz.sha256" \ | |
| "$nat.tar.gz" "$nat.tar.gz.sha256" | |
| - name: upload archives (input-addressed) | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: archives-${{ needs.build-id.outputs.value }}-${{ matrix.target }} | |
| path: | | |
| abgen-*.tar.gz | |
| abgen-native-*.tar.gz | |
| if-no-files-found: error | |
| - name: upload Docker image (input-addressed) | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' && !startsWith(github.ref, 'refs/tags/') | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: image-${{ needs.build-id.outputs.value }}-dockerImage | |
| path: dockerImage.tar.gz | |
| if-no-files-found: error | |
| - name: upload Lambda image (input-addressed) | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' && !startsWith(github.ref, 'refs/tags/') | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: image-${{ needs.build-id.outputs.value }}-lambdaImage | |
| path: lambdaImage.tar.gz | |
| if-no-files-found: error | |
| promote: | |
| name: promote prebuilt targets | |
| needs: build-id | |
| if: needs.build-id.outputs.has_promote == 'true' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| actions: read | |
| env: | |
| ABGEN_BUILD_ID: ${{ needs.build-id.outputs.value }} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| - name: fetch, verify and repack | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PROMOTE: ${{ needs.build-id.outputs.promote }} | |
| run: | | |
| set -eu | |
| mkdir -p out | |
| while read -r entry; do | |
| t=$(echo "$entry" | jq -r .target) | |
| id=$(echo "$entry" | jq -r .artifact_id) | |
| echo "::group::promote $t (artifact $id)" | |
| gh api "/repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip" > "a-$t.zip" | |
| bash ci/promote-target.sh "$t" "a-$t.zip" out | |
| echo "::endgroup::" | |
| done < <(echo "$PROMOTE" | jq -c '.[]') | |
| ls -l out | |
| - name: publish archives to draft release | |
| if: startsWith(github.ref, 'refs/tags/v') && inputs.dry_run != true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 \ | |
| || gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft \ | |
| --title "$GITHUB_REF_NAME" --notes-file .github/release-notes.md \ | |
| || gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null | |
| gh release upload "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --clobber out/* | |
| napi: | |
| name: abgen-node ${{ matrix.target }} | |
| needs: build-id | |
| if: ${{ !inputs.skip_napi && needs.build-id.outputs.napi_promote != 'true' }} | |
| runs-on: ${{ matrix.runner }} | |
| env: | |
| ABGEN_BUILD_ID: ${{ needs.build-id.outputs.value }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # cache on the linux legs only: x86 primes ci's node-addon job | |
| # (same shared-key) and each linux leg warms its successor across | |
| # main pushes; mac/windows stay uncached (quota over win). | |
| - { target: x86_64-unknown-linux-gnu, runner: ubuntu-24.04, cache: "true" } | |
| - { target: aarch64-unknown-linux-gnu, runner: ubuntu-24.04-arm, cache: "true" } | |
| - { target: aarch64-apple-darwin, runner: macos-15 } | |
| - { target: x86_64-apple-darwin, runner: macos-15 } | |
| - { target: x86_64-pc-windows-msvc, runner: windows-2025 } | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| - uses: ./.github/actions/rust-setup | |
| with: | |
| targets: ${{ matrix.target }} | |
| cache: ${{ matrix.cache || 'false' }} | |
| workspaces: crate/abgen-node | |
| shared-key: abgen-node | |
| # saves only from main pushes: shareable by every PR and the next | |
| # merge build; branch/dispatch rehearsal saves would be scoped | |
| # quota waste. | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e | |
| with: | |
| node-version: 24 | |
| - name: install addon deps | |
| working-directory: crate/abgen-node | |
| run: npm i --no-audit --no-fund | |
| - name: build the addon | |
| working-directory: crate/abgen-node | |
| run: npx napi build --platform --release --target ${{ matrix.target }} | |
| - name: smoke the addon | |
| if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-apple-darwin' | |
| working-directory: crate/abgen-node | |
| run: node test/smoke.mjs | |
| - name: glibc floor | |
| if: endsWith(matrix.target, '-unknown-linux-gnu') | |
| working-directory: crate/abgen-node | |
| run: bash ../../ci/check-glibc-floor.sh 2.34 ./*.node | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: napi-${{ needs.build-id.outputs.value }}-${{ matrix.target }} | |
| path: crate/abgen-node/*.node | |
| if-no-files-found: error | |
| image: | |
| name: ghcr image | |
| needs: build-id | |
| # Tags always run (they push); otherwise only when no prebuilt artifact | |
| # exists yet — a non-tag run with a prebuilt would skip every step and | |
| # burn a runner fetching an artifact it discards. | |
| if: startsWith(github.ref, 'refs/tags/') || needs.build-id.outputs.image_docker == '' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| - if: needs.build-id.outputs.image_docker == '' | |
| uses: ./.github/actions/nix-store-cache | |
| - name: build image | |
| if: needs.build-id.outputs.image_docker == '' | |
| run: | | |
| ref="${GITHUB_REF_NAME:-dev}" | |
| # Quiet unless it fails; the retry rebuilds only the failed | |
| # derivation with its full log. | |
| nix build .#dockerImage || nix build .#dockerImage --print-build-logs | |
| echo "store path : $(readlink -f result)" | |
| echo "compressed : $(ls -lLh result | awk '{print $5}')" | |
| cp -L result image.tar.gz | |
| { | |
| echo "REF=${ref}" | |
| echo "IMG_SIZE=$(ls -lLh result | awk '{print $5}')" | |
| } >> "$GITHUB_ENV" | |
| - name: upload image artifact (input-addressed) | |
| if: needs.build-id.outputs.image_docker == '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: image-${{ needs.build-id.outputs.value }}-dockerImage | |
| path: image.tar.gz | |
| if-no-files-found: error | |
| - name: fetch prebuilt image | |
| if: needs.build-id.outputs.image_docker != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ARTIFACT_ID: ${{ needs.build-id.outputs.image_docker }} | |
| run: | | |
| gh api "/repos/$GITHUB_REPOSITORY/actions/artifacts/$ARTIFACT_ID/zip" > i.zip | |
| unzip -q i.zip | |
| # The nix legs upload the tarball as <package>.tar.gz; normalize. | |
| [ -f dockerImage.tar.gz ] && mv dockerImage.tar.gz image.tar.gz | |
| test -f image.tar.gz || { echo "no image tarball in artifact $ARTIFACT_ID" >&2; ls -l >&2; exit 1; } | |
| { | |
| echo "REF=${GITHUB_REF_NAME:-dev}" | |
| echo "IMG_SIZE=$(ls -lh image.tar.gz | awk '{print $5}')" | |
| } >> "$GITHUB_ENV" | |
| - if: needs.build-id.outputs.image_docker != '' && startsWith(github.ref, 'refs/tags/') | |
| uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 | |
| - name: push to ghcr | |
| if: startsWith(github.ref, 'refs/tags/') && inputs.dry_run != true | |
| run: | | |
| repo="${GITHUB_REPOSITORY,,}" | |
| conf="$(mktemp)" | |
| echo 'unqualified-search-registries = []' > "$conf" | |
| for tag in "${REF}" latest; do | |
| # --inputs-from .: skopeo comes from the flake's locked nixpkgs, | |
| # not whatever the flake registry resolves to today. | |
| nix run --inputs-from . nixpkgs#skopeo -- --registries-conf "$conf" --insecure-policy copy \ | |
| --dest-creds "${{ github.actor }}:${{ github.token }}" \ | |
| "docker-archive:$PWD/image.tar.gz" \ | |
| "docker://ghcr.io/${repo}:${tag}" | |
| done | |
| { | |
| echo "### abgen container image" | |
| echo "" | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| image | \`ghcr.io/${repo}:${REF}\` (and \`:latest\`) |" | |
| echo "| size | ${IMG_SIZE} compressed, no base OS |" | |
| echo "| build | \`nix build .#dockerImage\` — deterministic, pinned \`flake.lock\` |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| lambda-image: | |
| name: ECR lambda image | |
| needs: build-id | |
| # Same gate as the ghcr image: tags push, non-tag runs only build. | |
| if: startsWith(github.ref, 'refs/tags/') || needs.build-id.outputs.image_lambda == '' | |
| runs-on: ubuntu-24.04-arm | |
| permissions: | |
| contents: read | |
| id-token: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| - if: needs.build-id.outputs.image_lambda == '' | |
| uses: ./.github/actions/nix-store-cache | |
| - name: build image | |
| if: needs.build-id.outputs.image_lambda == '' | |
| run: | | |
| ref="${GITHUB_REF_NAME:-dev}" | |
| nix build .#lambdaImage || nix build .#lambdaImage --print-build-logs | |
| echo "store path : $(readlink -f result)" | |
| echo "compressed : $(ls -lLh result | awk '{print $5}')" | |
| cp -L result image.tar.gz | |
| { | |
| echo "REF=${ref}" | |
| echo "IMG_SIZE=$(ls -lLh result | awk '{print $5}')" | |
| } >> "$GITHUB_ENV" | |
| - name: upload image artifact (input-addressed) | |
| if: needs.build-id.outputs.image_lambda == '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: image-${{ needs.build-id.outputs.value }}-lambdaImage | |
| path: image.tar.gz | |
| if-no-files-found: error | |
| - name: fetch prebuilt image | |
| if: needs.build-id.outputs.image_lambda != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ARTIFACT_ID: ${{ needs.build-id.outputs.image_lambda }} | |
| run: | | |
| gh api "/repos/$GITHUB_REPOSITORY/actions/artifacts/$ARTIFACT_ID/zip" > i.zip | |
| unzip -q i.zip | |
| # The nix legs upload the tarball as <package>.tar.gz; normalize. | |
| [ -f lambdaImage.tar.gz ] && mv lambdaImage.tar.gz image.tar.gz | |
| test -f image.tar.gz || { echo "no image tarball in artifact $ARTIFACT_ID" >&2; ls -l >&2; exit 1; } | |
| { | |
| echo "REF=${GITHUB_REF_NAME:-dev}" | |
| echo "IMG_SIZE=$(ls -lh image.tar.gz | awk '{print $5}')" | |
| } >> "$GITHUB_ENV" | |
| - if: needs.build-id.outputs.image_lambda != '' && startsWith(github.ref, 'refs/tags/') | |
| uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 | |
| - name: assume ECR push role (OIDC) | |
| if: startsWith(github.ref, 'refs/tags/') && inputs.dry_run != true | |
| run: | | |
| role="${{ vars.ABGEN_LAMBDA_ECR_ROLE_ARN }}" | |
| region="${{ vars.ABGEN_LAMBDA_AWS_REGION }}" | |
| if [ -z "$role" ] || [ -z "$region" ]; then | |
| echo "::error::ABGEN_LAMBDA_ECR_ROLE_ARN / ABGEN_LAMBDA_AWS_REGION repository variables are not set" | |
| exit 1 | |
| fi | |
| token="$(curl -sS -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ | |
| "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" | jq -r .value)" | |
| creds="$(aws sts assume-role-with-web-identity \ | |
| --role-arn "$role" \ | |
| --role-session-name "abgen-lambda-ci-${GITHUB_RUN_ID}" \ | |
| --web-identity-token "$token" \ | |
| --duration-seconds 3600 \ | |
| --query Credentials --output json)" | |
| { | |
| echo "AWS_ACCESS_KEY_ID=$(echo "$creds" | jq -r .AccessKeyId)" | |
| echo "AWS_SECRET_ACCESS_KEY=$(echo "$creds" | jq -r .SecretAccessKey)" | |
| echo "AWS_SESSION_TOKEN=$(echo "$creds" | jq -r .SessionToken)" | |
| echo "AWS_REGION=${region}" | |
| } >> "$GITHUB_ENV" | |
| - name: push to ECR | |
| if: startsWith(github.ref, 'refs/tags/') && inputs.dry_run != true | |
| run: | | |
| repo="${{ vars.ABGEN_LAMBDA_ECR_REPOSITORY }}" | |
| registry="${repo%%/*}" | |
| password="$(aws ecr get-login-password)" | |
| conf="$(mktemp)" | |
| echo 'unqualified-search-registries = []' > "$conf" | |
| for tag in "${REF}" latest; do | |
| nix run --inputs-from . nixpkgs#skopeo -- \ | |
| --registries-conf "$conf" --insecure-policy copy \ | |
| --dest-creds "AWS:${password}" \ | |
| "docker-archive:$PWD/image.tar.gz" \ | |
| "docker://${repo}:${tag}" | |
| done | |
| digest="$(nix run --inputs-from . nixpkgs#skopeo -- \ | |
| --registries-conf "$conf" --insecure-policy inspect \ | |
| --creds "AWS:${password}" --format '{{.Digest}}' \ | |
| "docker://${repo}:${REF}")" | |
| { | |
| echo "### abgen-lambda container image" | |
| echo "" | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| image | \`${repo}:${REF}\` (and \`:latest\`) |" | |
| echo "| digest | \`${digest}\` |" | |
| echo "| size | ${IMG_SIZE} compressed, no base OS |" | |
| echo "| arch | arm64 (Graviton Lambda) |" | |
| echo "| build | \`nix build .#lambdaImage\` — deterministic, pinned \`flake.lock\` |" | |
| echo "| roll out | update the Lambda function to \`${repo}:${REF}\` — see lambda/README.md |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| napi-publish: | |
| name: publish @dcl/abgen-node | |
| needs: [build-id, napi] | |
| runs-on: ubuntu-24.04 | |
| if: >- | |
| !cancelled() && | |
| startsWith(github.ref, 'refs/tags/v') && inputs.dry_run != true && | |
| (needs.napi.result == 'success' || | |
| (needs.napi.result == 'skipped' && needs.build-id.outputs.napi_promote == 'true')) | |
| permissions: | |
| contents: write | |
| id-token: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e | |
| with: | |
| node-version: 24 | |
| - run: npm install -g npm@^11.5.1 | |
| - if: needs.napi.result == 'success' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c | |
| with: | |
| pattern: napi-* | |
| path: crate/abgen-node/artifacts | |
| - name: fetch promoted napi artifacts | |
| if: needs.napi.result == 'skipped' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NAPI_IDS: ${{ needs.build-id.outputs.napi_ids }} | |
| working-directory: crate/abgen-node | |
| run: | | |
| set -eu | |
| mkdir -p artifacts | |
| i=0 | |
| while read -r id; do | |
| gh api "/repos/$GITHUB_REPOSITORY/actions/artifacts/$id/zip" > "napi-$i.zip" | |
| unzip -q "napi-$i.zip" -d "artifacts/promoted-$i" | |
| i=$((i + 1)) | |
| done < <(echo "$NAPI_IDS" | jq -r '.[]') | |
| test "$i" -eq 5 | |
| find artifacts -name '*.node' | sed 's/^/promoted: /' | |
| test "$(find artifacts -name '*.node' | wc -l)" -eq 5 | |
| - name: publish | |
| working-directory: crate/abgen-node | |
| env: | |
| NPM_CONFIG_REGISTRY: https://registry.npmjs.org/ | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| npm i --no-audit --no-fund | |
| # napi reads the version from this package.json, not from the tag, and | |
| # prepublish propagates it to the per-platform manifests. | |
| npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version --ignore-scripts --allow-same-version | |
| # `napi artifacts` writes into npm/<platform>/, which must already | |
| # exist — those dirs are committed, from `napi create-npm-dir -t .`. | |
| # index.js/index.d.ts are committed for the same reason: nothing here | |
| # runs `napi build`, and they are the package's entry point. | |
| npx napi artifacts --dir artifacts | |
| ls -R ./npm | |
| # npm's prepublishOnly hook runs `napi prepublish`, which publishes | |
| # the per-platform packages and rewires optionalDependencies but | |
| # does NOT publish @dcl/abgen-node itself — `npm publish` below | |
| # does that. | |
| npm publish --access public | |
| publish: | |
| needs: [build, promote] | |
| runs-on: ubuntu-24.04 | |
| if: >- | |
| !cancelled() && | |
| startsWith(github.ref, 'refs/tags/v') && inputs.dry_run != true && | |
| needs.build.result != 'failure' && needs.build.result != 'cancelled' && | |
| needs.promote.result != 'failure' && needs.promote.result != 'cancelled' | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e | |
| with: | |
| node-version: 24 | |
| - run: npm install -g npm@^11.5.1 | |
| - name: verify, checksums, npm | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NPM_CONFIG_REGISTRY: https://registry.npmjs.org/ | |
| run: | | |
| git init -q . | |
| git remote add origin "https://github.com/$GITHUB_REPOSITORY" | |
| git fetch -q --depth 1 origin "$GITHUB_SHA" | |
| git checkout -q FETCH_HEAD | |
| # Kept in separate directories: `dist` must hold the six runtime | |
| # archives and nothing else, because npm/publish.sh reads it. The | |
| # counts are asserted per set so neither can silently go missing — | |
| # a single glob over both would let three of one and nine of the | |
| # other still total twelve. | |
| gh release download "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern 'abgen-v*.tar.gz' --pattern 'abgen-v*.tar.gz.sha256' -D dist | |
| gh release download "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern 'abgen-native-v*.tar.gz' --pattern 'abgen-native-v*.tar.gz.sha256' -D native | |
| dist_archives=(dist/*.tar.gz) | |
| native_archives=(native/*.tar.gz) | |
| test "${#dist_archives[@]}" -eq 6 | |
| test "${#native_archives[@]}" -eq 6 | |
| (cd dist && sha256sum -c ./*.sha256) | |
| (cd native && sha256sum -c ./*.sha256) | |
| # One manifest covering everything the release ships, so a reader | |
| # can't mistake a partial file for the complete one. | |
| cat dist/*.sha256 native/*.sha256 > SHA256SUMS.txt | |
| test "$(wc -l < SHA256SUMS.txt)" -eq 12 | |
| cat SHA256SUMS.txt | |
| gh release upload "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --clobber SHA256SUMS.txt | |
| for f in dist/*.sha256 native/*.sha256; do | |
| gh release delete-asset "$GITHUB_REF_NAME" "$(basename "$f")" --repo "$GITHUB_REPOSITORY" --yes | |
| done | |
| bash npm/publish.sh "${GITHUB_REF_NAME#v}" dist | |
| - name: attest build provenance | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 | |
| with: | |
| subject-path: "dist/*.tar.gz,native/*.tar.gz" | |
| - name: publish the release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false | |
| gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --json isDraft \ | |
| --jq 'if .isDraft then error("release is still a draft") else "published" end' |