forjar 1.32.0

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
name: Binary Release

# Attaches cross-compiled `forjar` binaries to a GitHub Release as tar.gz +
# sha256 on github-hosted runners. This runs in parallel with the existing
# release.yml (which uses [self-hosted, clean-room, X64] runners) and is the
# durable path for binary attachment — github-hosted runners are not
# subject to self-hosted infra availability/queueing.
#
# Background: v1.3.0 only had 1/6 expected binaries attached because the
# self-hosted matrix in release.yml didn't complete reliably. This workflow
# reliably attaches 4 Linux binaries (musl + gnu × x86_64 + aarch64).
# macOS targets are intentionally deferred to a follow-up.
#
# Triggered by:
#   - `push: tags v*` — the durable path: creates the GitHub Release itself
#     (idempotently) on a hosted runner, so a tag always produces a release
#     with binaries even when release.yml's self-hosted verify job fails
#     or the clean-room queue is down.
#   - `release: published` — fires when a release is published by hand
#   - `workflow_dispatch` — manual re-run / backfill, takes a tag input
#
# Targets: 4 Linux (x86_64/aarch64 × musl/gnu). musl variants are static
# and ideal for Docker / scratch / Alpine; gnu variants are smaller and
# match typical Linux distributions. aarch64 builds use `cross`.

# PMAT-170: this workflow no longer fires on a tag push or on the release
# event. release.yml (clean-room) is the one producer of a release and its
# assets; when both ran on the same tag they raced to upload overlapping
# Linux targets and to overwrite SHA256SUMS. Keep this file for the manual
# backfill case only (the clean-room queue is down): `workflow_dispatch`.
on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag to attach binaries to (e.g. v1.3.0)'
        required: true
        type: string
      unlocked:
        description: 'Drop --locked (backfill of pre-v1.4.3 tags whose Cargo.lock is stale)'
        required: false
        type: boolean
        default: false

permissions:
  contents: write  # create the release + upload assets

concurrency:
  group: binary-release-${{ inputs.tag }}
  cancel-in-progress: false

env:
  CROSS_VERSION: v0.2.5

jobs:
  # Resolve the tag once and make sure the GitHub Release exists before any
  # build leg runs. This used to say "hosted runners only, no self-hosted
  # dependency" and was a deliberate choice; PMAT-547 reverses it, because a
  # GitHub-hosted runner is not something this project may spend.
  ensure-release:
    name: Ensure release exists
    runs-on: [self-hosted, clean-room, X64]
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - name: Resolve release tag
        id: tag
        run: |
          TAG="${{ inputs.tag }}"
          if [ -z "$TAG" ]; then
            echo "::error::No tag resolved (release event missing tag_name and no dispatch input)"
            exit 1
          fi
          case "$TAG" in
            v*) ;;
            *) echo "::error::Resolved ref '$TAG' is not a v* tag"; exit 1 ;;
          esac
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "Releasing forjar $TAG"

      - name: Create GitHub Release if missing (idempotent)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${{ steps.tag.outputs.tag }}"
          # Mirrors release.yml create-release: never clobber an existing
          # release or its hand-written notes.
          if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
            echo "Release $TAG already exists — leaving notes intact, will upload assets."
          else
            gh release create "$TAG" \
              --title "$TAG" \
              --generate-notes \
              --verify-tag \
              --prerelease \
              --repo "${{ github.repository }}"
          fi

  build:
    name: ${{ matrix.target }}
    needs: ensure-release
    # THIS LEG WAS PINNED TO ubuntu-22.04 AND IS NO LONGER PINNED TO ANYTHING.
    #
    # 22.04 pinned the gnu builds to glibc 2.35, because a binary linked
    # against a newer glibc refuses to start on an older host and
    # ubuntu-latest had moved to 24.04 / glibc 2.39. PMAT-547 moves the leg to
    # the fleet, whose glibc is whatever the fleet has — which is not a thing
    # this workflow could measure before it ran.
    #
    # So it measures it. The step below records the builder's glibc and the
    # maximum GLIBC_ version the produced binary demands, into the job summary
    # and the log, on every release. musl legs are static and aarch64 legs
    # build inside cross containers, so only the x86_64-gnu leg is exposed;
    # the record is what turns "we think it still runs on old hosts" into a
    # number a reader can check against the oldest host they care about.
    runs-on: [self-hosted, clean-room, X64]
    # infra#430: sixteen clean-room runners share one ~/.cargo, and an hourly
    # reaper deletes registry/src entries by mtime -- under a live build, which
    # makes rustc read a source file that has just vanished:
    #   couldn't read .../registry/src/.../<crate>/src/<file>.rs: No such file
    #   error: could not parse/generate dep info at: .../deps/<crate>.d
    # MEASURED on this branch: msrv and coverage both died that way the first
    # time PMAT-547 moved them off GitHub-hosted runners, where each job had a
    # private ~/.cargo by construction. A registry private to this runner takes
    # the job off the shared surface. The path is outside the workspace, so it
    # survives the checkout clean and is a cache rather than a cold download.
    # The host-side fix is paiml/infra's.
    env:
      CARGO_HOME: ${{ github.workspace }}/../cargo-home-${{ github.job }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            cross: false
          - target: x86_64-unknown-linux-gnu
            cross: false
          - target: aarch64-unknown-linux-musl
            cross: true
          - target: aarch64-unknown-linux-gnu
            cross: true
    steps:
      - name: Resolve release tag
        id: tag
        run: |
          TAG="${{ needs.ensure-release.outputs.tag }}"
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "Building forjar for $TAG → ${{ matrix.target }}"

      - name: Checkout at tag
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
        with:
          ref: ${{ steps.tag.outputs.tag }}

      # Contract assertions (src/generated_contracts.rs) are git-tracked and
      # the contract crates are workspace members under crates/ (#423) — no
      # sibling checkout, no registry fetch for contract code, and no
      # pv codegen needed (the old provable-contracts path-dep dance caused
      # issues #104/#112/#113).

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      # The workspace pins a specific channel in `rust-toolchain.toml`
      # which overrides the dtolnay action for any cargo invocation in
      # the tree. Add the target explicitly to that pinned toolchain so
      # native cross-compiles (x86_64-musl) don't fail with `can't find
      # crate for core` mid-build.
      - name: Install target on workspace-pinned toolchain
        if: ${{ !matrix.cross }}
        run: rustup target add ${{ matrix.target }}

      # PMAT-547: this leg moved from a hosted runner to the fleet, where
      # apt-get may be absent and sudo may not be passwordless. release.yml
      # already builds musl targets on [self-hosted, clean-room, X64] and guards the
      # call exactly this way, so this is the shape that is known to work
      # there. If musl-tools is already present -- which is why the guard is
      # safe -- the build proceeds; if it is genuinely missing AND cannot be
      # installed, the build fails at the link step naming what it wanted,
      # which is a better failure than apt-get exiting 127 here.
      - name: Install musl tools
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          if command -v musl-gcc >/dev/null 2>&1; then
            echo "musl-gcc already present: $(command -v musl-gcc)"
          elif command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -qq
            sudo apt-get install -y --no-install-recommends musl-tools
          else
            echo "::warning::musl-gcc is absent and apt-get is not available on this runner. The build will reach the link step and fail there naming the missing linker. Install musl-tools on the fleet image, or give this leg a runner that has it." >&2
          fi

      # Cross is used for aarch64 targets — pinned to v0.2.5 (matches the
      # legacy release.yml convention).
      - name: Install cross
        if: matrix.cross
        run: |
          dir="$RUNNER_TEMP/cross"
          mkdir -p "$dir"
          curl -sL "https://github.com/cross-rs/cross/releases/download/${CROSS_VERSION}/cross-x86_64-unknown-linux-musl.tar.gz" \
            | tar xz -C "$dir"
          echo "$dir" >> "$GITHUB_PATH"

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2
        with:
          # The runner image MUST be part of the key: objects cached on
          # 24.04 (glibc 2.38, __isoc23_* symbols) fail to link on 22.04
          # (v1.4.4 x86_64-gnu leg failed exactly this way).
          shared-key: binary-${{ matrix.target }}-u2204

      - name: Build forjar
        run: |
          LOCKED="--locked"
          if [ "${{ inputs.unlocked }}" = "true" ]; then
            LOCKED=""
            echo "::warning::building without --locked (stale-lock backfill mode)"
          fi
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --features vendored-openssl --bin forjar --target ${{ matrix.target }} $LOCKED
          else
            cargo build --release --features vendored-openssl --bin forjar --target ${{ matrix.target }} $LOCKED
          fi

      - name: Strip binary
        run: |
          TARGET="${{ matrix.target }}"
          BIN_PATH="target/${TARGET}/release/forjar"
          # Each cross image ships only the matching ${triple}-strip;
          # the gnu image lacks musl-strip and vice-versa.
          case "$TARGET" in
            aarch64-unknown-linux-musl) STRIP=aarch64-linux-musl-strip ;;
            aarch64-unknown-linux-gnu)  STRIP=aarch64-linux-gnu-strip ;;
            *)                          STRIP="" ;;
          esac
          if [ -n "$STRIP" ]; then
            docker run --rm -v "$PWD/target:/target:Z" \
              "ghcr.io/cross-rs/${TARGET}:main" \
              "$STRIP" "/$BIN_PATH" || true
          else
            strip "$BIN_PATH" || true
          fi
          ls -la "$BIN_PATH"

      # PMAT-547. The gnu legs used to be pinned to ubuntu-22.04 so they linked
      # against glibc 2.35; the pin is gone and the fleet's glibc is whatever
      # the fleet has. An unmeasured floor is not a floor, so record it: the
      # builder's own glibc, and the highest GLIBC_ version the binary actually
      # demands -- which is the number that decides whether an old host can run
      # it. AND it fails the build above GLIBC_CEILING=2.35, the floor ubuntu-22.04
      # enforced before this branch removed the pin. An earlier version of this
      # comment said the step never fails and that raising the floor is a reader's
      # decision; that made a disclosed weakening out of an enforced gate, and two
      # review lanes refused it.
      - name: Record the glibc floor this binary demands
        if: ${{ !contains(matrix.target, 'musl') }}
        run: |
          TARGET="${{ matrix.target }}"
          BIN_PATH="target/${TARGET}/release/forjar"
          # `ldd --version` reports the glibc of the HOST running this step.
          # For the aarch64 leg the binary was built inside a cross container
          # on an x86_64 host, so the host's glibc is not the one it linked
          # against and printing it would be a measurement of the wrong thing.
          # The DEMANDED floor below is read out of the binary itself and is
          # correct on every leg; the builder line is only claimed where it is
          # true.
          case "$TARGET" in
            x86_64-unknown-linux-gnu)
              builder="$(ldd --version 2>/dev/null | head -1 || echo 'ldd unavailable')" ;;
            *)
              builder="not applicable — ${TARGET} was cross-built, so this host's glibc is not what it linked against" ;;
          esac
          demanded="$(strings -a "$BIN_PATH" 2>/dev/null \
            | sed -n 's/.*GLIBC_\([0-9][0-9.]*\).*/\1/p' \
            | sort -V | tail -1)"
          [ -n "$demanded" ] || demanded="none found (static, or strings unavailable)"
          printf 'builder: %s\nbinary demands at most GLIBC_%s\n' "$builder" "$demanded"
          # shellcheck disable=SC2016  # the backticks below are MARKDOWN code
          # fences in a printf FORMAT string, not command substitutions: they must
          # reach $GITHUB_STEP_SUMMARY literally. Disabled by name with a reason
          # rather than left as three findings a receipt then has to explain away.
          {
            printf '### glibc floor — `%s`\n\n' "$TARGET"
            printf -- '- builder: `%s`\n' "$builder"
            printf -- '- binary demands at most: `GLIBC_%s`\n' "$demanded"
          } >> "$GITHUB_STEP_SUMMARY"

          # AND IT IS A GATE, not only a record (PMAT-547, review round 9).
          #
          # This leg was pinned to ubuntu-22.04 precisely so the gnu binaries could
          # not demand more than glibc 2.35. Moving it to the fleet removed that pin,
          # and a measurement that never fails is not what the pin was: a fleet
          # upgraded to a newer glibc would publish binaries that refuse to start on
          # an old host, silently, exactly as before the pin existed. A review lane
          # named it as a weakened gate and it was right.
          #
          # 2.35 is not a guess: it is the floor ubuntu-22.04 enforced. Measured on
          # the fleet today, the binary demands GLIBC_2.34, so this ceiling passes
          # with room and fires the day the fleet moves past what the pin allowed.
          # Raising it is a decision someone writes here, in the open, with a reason.
          GLIBC_CEILING=2.35
          case "$demanded" in
            none*) ;;   # static or unreadable: the arms above already say which
            *)
              highest="$(printf '%s\n%s\n' "$demanded" "$GLIBC_CEILING" | sort -V | tail -1)"
              if [ "$highest" != "$GLIBC_CEILING" ]; then
                echo "::error::${TARGET} demands GLIBC_${demanded}, above the ${GLIBC_CEILING} floor ubuntu-22.04 used to enforce. A host with an older glibc cannot start this binary. Either lower what the build links against, or raise GLIBC_CEILING here with a reason."
                exit 1
              fi
              ;;
          esac

      - name: Package archive
        id: package
        run: |
          TAG="${{ steps.tag.outputs.tag }}"
          # Strip the leading 'v': assets are forjar-<x.y.z>-<target>.tar.gz,
          # matching release.yml, the homebrew checksum patching, and the
          # name install.sh resolves (VERSION_NUM="${TAG#v}").
          VERSION="${TAG#v}"
          TARGET="${{ matrix.target }}"
          ARCHIVE="forjar-${VERSION}-${TARGET}"
          mkdir -p "$ARCHIVE"
          cp "target/${TARGET}/release/forjar" "$ARCHIVE/"
          for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE; do
            [ -f "$f" ] && cp "$f" "$ARCHIVE/"
          done
          tar czf "${ARCHIVE}.tar.gz" "$ARCHIVE"
          shasum -a 256 "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256"
          echo "archive=${ARCHIVE}.tar.gz" >> "$GITHUB_OUTPUT"
          echo "sha=${ARCHIVE}.tar.gz.sha256" >> "$GITHUB_OUTPUT"
          du -h "${ARCHIVE}.tar.gz"

      - name: Upload assets to release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release upload "${{ steps.tag.outputs.tag }}" \
            "${{ steps.package.outputs.archive }}" \
            "${{ steps.package.outputs.sha }}" \
            --clobber

  # Combined SHA256SUMS regenerated from every tarball on the release —
  # install.sh's verify_checksum prefers this file (with per-asset .sha256
  # as fallback). Runs after all build legs so it covers the full set;
  # macOS assets added later by release.yml get covered on its own
  # checksums pass or the next re-run.
  checksums:
    name: Aggregate SHA256SUMS
    needs: [ensure-release, build]
    runs-on: [self-hosted, clean-room, X64]
    steps:
      - name: Regenerate and upload SHA256SUMS
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          TAG="${{ needs.ensure-release.outputs.tag }}"
          mkdir assets && cd assets
          # PMAT-230: --clobber on every `gh release download`. NOT load-bearing
          # here and said so rather than implied: this job is on a hosted
          # (ephemeral) runner and `mkdir assets` above gives it a fresh
          # directory, so the fixed-path collision that killed the v1.28.0
          # release in release.yml cannot happen on this line today. It carries
          # the flag because the rule is uniform over every workflow — a rule
          # that names the two files it was written for is a rule a third call
          # site walks past — and because `runs-on` has moved under these jobs
          # before.
          gh release download "$TAG" --repo "${{ github.repository }}" --pattern '*.tar.gz' --clobber
          # #324/#325: this job regenerates SHA256SUMS from whatever is CURRENTLY
          # attached to the release, so anything stray that reached the object
          # gets laundered into the authoritative checksum file. That is how
          # v1.18.0 ended up with a 10-line SHA256SUMS, four lines of it for
          # 1.17.0. #324 added this guard to release.yml, the other producer,
          # and left this one open. Do not delete it as redundant: the two
          # workflows are not ordered against each other, and either can run
          # alone via workflow_dispatch.
          ver="${TAG#v}"
          stray=0
          for f in *.tar.gz; do
            case "$f" in
              forjar-"$ver"-*) ;;
              *) echo "::error::$TAG carries $f, which is not this release"; stray=1 ;;
            esac
          done
          # A CLEAN DIRECTORY IS NOT PROOF. An empty download would also pass a
          # "no strays" test while publishing a SHA256SUMS that describes
          # nothing, so assert the denominator too.
          n=$(ls -1 *.tar.gz 2>/dev/null | wc -l)
          echo "$TAG holds $n archive(s)"
          [ "$n" -gt 0 ] || { echo "::error::no archives downloaded — nothing to checksum"; exit 1; }
          [ "$stray" -eq 0 ] || exit 1
          sha256sum -- *.tar.gz > SHA256SUMS
          # Sidecars too. This job used to refresh only SHA256SUMS, so a tarball
          # that release.yml later --clobber'd kept an earlier, now-invalid
          # `.sha256` naming bytes that no longer existed.
          for f in *.tar.gz; do sha256sum -- "$f" > "$f.sha256"; done
          cat SHA256SUMS
          gh release upload "$TAG" SHA256SUMS ./*.tar.gz.sha256 --clobber --repo "${{ github.repository }}"

  summary:
    name: Summary
    needs: [ensure-release, build]
    runs-on: [self-hosted, clean-room, X64]
    if: always()
    steps:
      - name: Write summary
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${{ needs.ensure-release.outputs.tag }}"
          STATUS="${{ needs.build.result }}"
          {
            echo "### Binary Release: $TAG"
            echo ""
            if [ "$STATUS" = "success" ]; then
              echo "- Build matrix: **4/4 targets succeeded**"
            else
              echo "- Build matrix: **partial** (status=$STATUS)"
            fi
            echo "- Targets:"
            echo "  - x86_64-unknown-linux-musl"
            echo "  - x86_64-unknown-linux-gnu"
            echo "  - aarch64-unknown-linux-musl"
            echo "  - aarch64-unknown-linux-gnu"
          } >> "$GITHUB_STEP_SUMMARY"