amont-agent 2.4.0

A guard that inspects a shell command before Claude Code runs it
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
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
name: Release

# Cutting a release means: a tag, prebuilt binaries for every platform this
# guard claims to support, and checksums.
#
# Triggered by the TAG, never by a branch push. The tag is the decision.

on:
  push:
    tags: ["v*"]
  # So a release can be rehearsed end to end without publishing one.
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag to build (dry run — no release is published)"
        required: false

permissions:
  contents: write

defaults:
  run:
    shell: bash

jobs:
  # The tag and the manifest must agree BEFORE anything is built.
  #
  # The footgun that motivated this: a commit-msg hook rejects a commit, the
  # shell carries on to `git tag`, and the tag lands on the OLD head — so CI
  # builds and publishes the new version number from stale code. Registries
  # are immutable; the wrong bytes under a version cannot be replaced, only
  # superseded. Checking costs four lines and runs first.
  guard:
    name: the tag says what the manifest says
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.v.outputs.version }}
      dry_run: ${{ steps.v.outputs.dry_run }}
    steps:
      - uses: actions/checkout@v4
      - id: v
        run: |
          set -euo pipefail
          manifest=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
          if [ "${{ github.ref_type }}" = "tag" ]; then
            tag="${GITHUB_REF_NAME#v}"
            dry=false
          else
            tag="${{ inputs.tag }}"
            tag="${tag#v}"
            [ -n "$tag" ] || tag="$manifest"
            dry=true
          fi
          echo "manifest=$manifest tag=$tag dry_run=$dry"
          if [ "$manifest" != "$tag" ]; then
            echo "::error::tag v$tag does not match the manifest version $manifest."
            echo "::error::Commit the version bump FIRST, confirm HEAD moved, then tag."
            exit 1
          fi
          # The release notes lead with CHANGELOG.md's section for this
          # version, so a missing section would publish a release that opens
          # with nothing. Refuse now, while writing it costs one commit
          # instead of a re-release.
          if ! awk -v ver="v$manifest" '$1 == "##" && $2 == ver { found = 1 } END { exit !found }' CHANGELOG.md; then
            echo "::error::CHANGELOG.md has no '## v$manifest' section."
            echo "::error::Write what the upgrader gets, then tag."
            exit 1
          fi
          echo "version=$manifest" >> "$GITHUB_OUTPUT"
          echo "dry_run=$dry" >> "$GITHUB_OUTPUT"

  build:
    name: ${{ matrix.target }}
    needs: guard
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      # One broken target must not hide the others: a partial release is
      # something to see in full, not to discover one platform at a time.
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          # Static, so it runs on distros older than the runner's glibc — the
          # usual reason a "linux" binary fails for somebody.
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            packages: musl-tools
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            packages: gcc-aarch64-linux-gnu
            linker: aarch64-linux-gnu-gcc
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install toolchain for ${{ matrix.target }}
        run: rustup target add ${{ matrix.target }}
      - name: Install cross-compilation packages
        if: matrix.packages != ''
        # The Azure apt mirror hung `apt-get update` for a full job budget
        # three releases running in amont. Three defences, each sufficient
        # alone: the canonical archive instead of the Azure mirror, a
        # per-fetch timeout with retries, and a step budget so a hang costs
        # five minutes and a cheap rerun, not the release.
        timeout-minutes: 5
        run: |
          # The runner routes apt through `mirror+file:/etc/apt/apt-mirrors.txt`,
          # so rewriting the sources files alone still let every fetch start at
          # the Azure mirror. Rewrite the mirror LIST, which is the knob the
          # image actually reads.
          if [ -f /etc/apt/apt-mirrors.txt ]; then
            printf 'http://archive.ubuntu.com/ubuntu\tpriority:1\n' | sudo tee /etc/apt/apt-mirrors.txt
          fi
          sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g' \
            /etc/apt/sources.list /etc/apt/sources.list.d/*.sources 2>/dev/null || true
          sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 \
            -o Acquire::https::Timeout=15 update
          sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=15 \
            -o Acquire::https::Timeout=15 install -y ${{ matrix.packages }}
      - name: Point cargo at the cross linker
        if: matrix.linker != ''
        run: |
          target_upper=$(echo "${{ matrix.target }}" | tr 'a-z-' 'A-Z_')
          echo "CARGO_TARGET_${target_upper}_LINKER=${{ matrix.linker }}" >> "$GITHUB_ENV"

      - run: cargo build --release --locked --target ${{ matrix.target }}

      - name: Archive
        id: archive
        run: |
          set -euo pipefail
          version="${{ needs.guard.outputs.version }}"
          name="amont-agent-${version}-${{ matrix.target }}"
          staging="dist/$name"
          mkdir -p "$staging"
          cp README.md LICENSE "$staging/"
          if [ "${{ runner.os }}" = "Windows" ]; then
            cp "target/${{ matrix.target }}/release/amont-agent.exe" "$staging/"
            (cd dist && 7z a "$name.zip" "$name" > /dev/null)
            echo "asset=dist/$name.zip" >> "$GITHUB_OUTPUT"
          else
            cp "target/${{ matrix.target }}/release/amont-agent" "$staging/"
            (cd dist && tar czf "$name.tar.gz" "$name")
            echo "asset=dist/$name.tar.gz" >> "$GITHUB_OUTPUT"
          fi

      # Prove the thing we are about to hand people actually runs, and that it
      # still DECIDES — a binary that answers `--help` and then refuses nothing
      # is the defect `cargo test` structurally cannot see, because tests never
      # touch the packaged artifact.
      - name: The packaged binary runs, and still says no
        if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-apple-darwin' || matrix.target == 'x86_64-pc-windows-msvc'
        run: |
          set -euo pipefail
          version="${{ needs.guard.outputs.version }}"
          bin="dist/amont-agent-${version}-${{ matrix.target }}/amont-agent"
          # An `if`, not `[ … ] && …`: under `set -e` a false test as the last
          # command of a line is a FAILING step.
          if [ "${{ runner.os }}" = "Windows" ]; then bin="$bin.exe"; fi

          # CAPTURE FIRST, then grep the variable — never `"$bin" | grep -q`.
          #
          # This step runs under `bash -e -o pipefail`. `grep -q` exits at its
          # first match and closes the pipe, and the binary — correctly, since
          # v2.0.0 — then dies of SIGPIPE with status 141 like any Unix
          # filter. Under pipefail that is a FAILED step, so the more
          # correctly the binary behaves the harder this breaks.
          #
          # Both spellings were wrong here in turn, which is why this comment
          # is long: before the SIGPIPE fix the same two lines failed with 101
          # (a Rust panic writing to the closed pipe) on this same target and
          # no other. A command substitution drains the output completely, so
          # there is no early reader and no signal either way.
          help=$("$bin" --help)
          printf '%s' "$help" | grep -q 'backtest'
          rules=$("$bin" rules)
          printf '%s' "$rules" | grep -q 'pipe-to-tail'
          # The real thing: one command through the real rule engine.
          out=$("$bin" check 'git push origin main 2>&1 | tail -5')
          printf '%s\n' "$out"
          printf '%s' "$out" | grep -q 'pipe-to-tail' || {
            echo "::error::the packaged binary did not refuse a piped push"; exit 1; }
          echo "the packaged binary runs and still decides"

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.target }}
          path: ${{ steps.archive.outputs.asset }}
          if-no-files-found: error

  # The blocking twin of ci.yaml's advisory audit — same parsing, opposite
  # stakes. On a PR an advisory is information; on a TAG it is about to be
  # compiled into six binaries and served to installers, so:
  #
  #   - a VULNERABILITY fails the release;
  #   - warning-class advisories (unmaintained/unsound) stay named annotations:
  #     a gate nothing can pass is a gate people learn to delete;
  #   - a fetch failure ALSO fails, unlike ci.yaml: "could not check" is not
  #     "clean", and an unchecked tree does not ship. On a PR you retry
  #     tomorrow; a release published today is immutable today.
  audit:
    name: no known vulnerabilities ship
    needs: guard
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - name: Install cargo-audit
        run: cargo install --locked cargo-audit
      - name: Audit
        run: |
          set -euo pipefail
          if ! cargo audit --json > audit.json; then
            # Distinguish "found something" from "could not ask". Only the
            # latter is fatal here regardless of content.
            if [ ! -s audit.json ]; then
              echo "::error::cargo audit produced no output — the tree is UNVERIFIED, and an unverified tree does not ship."
              exit 1
            fi
          fi
          python3 - <<'PY'
          import json, pathlib, sys
          raw = pathlib.Path("audit.json").read_text()
          if not raw.strip():
              print("::error::empty audit report — nothing was verified")
              sys.exit(1)
          d = json.loads(raw)
          vulns = d.get("vulnerabilities", {}).get("list", [])
          for v in vulns:
              a = v.get("advisory", {})
              print(f"::error::{a.get('id')} {a.get('package')}: {a.get('title')}")
          for kind, items in d.get("warnings", {}).items():
              for w in items:
                  a = (w.get("advisory") or {})
                  print(f"::warning::{kind}: {a.get('package') or w.get('package')}")
          if vulns:
              print(f"::error::{len(vulns)} vulnerability advisories — not shipping")
              sys.exit(1)
          print("no vulnerability advisories")
          PY

  publish:
    name: publish the release
    needs: [guard, build, audit]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true
      - name: Checksums
        run: |
          set -euo pipefail
          cd dist
          ls -la
          sha256sum * > SHA256SUMS
          cat SHA256SUMS
      # A dry run stops here, having built and checksummed everything. The only
      # step it skips is the irreversible one.
      - name: Publish
        if: needs.guard.outputs.dry_run == 'false'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          # Idempotent, because re-running a release is a normal thing to want:
          # the reason to re-run is usually that a LATER step failed.
          if gh release view "${GITHUB_REF_NAME}" > /dev/null 2>&1; then
            echo "release ${GITHUB_REF_NAME} already exists — refreshing its assets"
            gh release upload "${GITHUB_REF_NAME}" dist/* --clobber
          else
            # Notes an upgrader can read, then the generated PR list. Matching
            # on $2 rather than a prefix so extracting v2.0.2 can never grab
            # v2.0.21.
            notes=$(mktemp)
            awk -v ver="${GITHUB_REF_NAME}" '
              $1 == "##" && $2 == ver { grab = 1; next }
              $1 == "##" && grab      { exit }
              grab                    { print }
            ' CHANGELOG.md > "$notes"
            printf '\n---\n\n' >> "$notes"
            gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
              -f tag_name="${GITHUB_REF_NAME}" --jq .body >> "$notes"
            gh release create "${GITHUB_REF_NAME}" \
              --title "${GITHUB_REF_NAME}" \
              --notes-file "$notes" \
              dist/*
          fi
      - name: Dry run summary
        if: needs.guard.outputs.dry_run == 'true'
        run: |
          echo "### Dry run — nothing published" >> "$GITHUB_STEP_SUMMARY"
          echo '```' >> "$GITHUB_STEP_SUMMARY"
          cat dist/SHA256SUMS >> "$GITHUB_STEP_SUMMARY"
          echo '```' >> "$GITHUB_STEP_SUMMARY"

  # IMMUTABLE, so a bad publish can only be yanked and superseded, never fixed.
  #
  # `needs: build` on purpose. If a target does not compile, that is a release
  # nobody should be able to `cargo install` either.
  publish-crates:
    name: publish to crates.io
    needs: [guard, build, publish]
    if: github.ref_type == 'tag'
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read
    env:
      VERSION: ${{ needs.guard.outputs.version }}
      CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Publish
        run: |
          set -euo pipefail
          # No poll after the upload. cargo ALREADY waits for it to become
          # available — the run log reads "waiting for X to be available at
          # registry" then "Published X" — and the crates.io API rate-limits
          # datacenter IPs, so a poll on top saw non-200 for a crate that had
          # just published fine and failed the job ten minutes later.
          #
          # Idempotency comes from cargo's own answer instead: re-running a
          # release must not fail on a crate that is already up, and "already
          # uploaded" is exactly that answer — anything else is a real failure.
          echo "→ publishing amont-agent $VERSION"
          if out=$(cargo publish --locked 2>&1); then
            printf '%s\n' "$out"
          else
            printf '%s\n' "$out"
            if printf '%s' "$out" | grep -qiE "already (been )?uploaded|already exists"; then
              echo "  amont-agent $VERSION was already published — continuing"
            else
              echo "::error::publishing amont-agent failed"
              exit 1
            fi
          fi
          echo "### Published to crates.io" >> "$GITHUB_STEP_SUMMARY"
          echo "\`cargo install amont-agent\` — v$VERSION" >> "$GITHUB_STEP_SUMMARY"

  # The tap formula, bumped by the release rather than by hand: a release is
  # not published until `brew upgrade` can see it.
  #
  # Auth is a deploy key that can write to the TAP REPOSITORY and nothing else
  # (secret TAP_DEPLOY_KEY) — narrower than any PAT. The rewrite is
  # scripts/bump-tap.py, which asserts on every surprise rather than sedding
  # hopefully, and `ruby -c` proves the result is at least a formula before
  # anything is pushed.
  publish-tap:
    name: publish to the homebrew tap
    needs: [guard, publish]
    if: github.ref_type == 'tag'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
    env:
      VERSION: ${{ needs.guard.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      # From the PUBLISHED release, not from this run's artifacts: the formula
      # must name the bytes an installer will actually download, and reading
      # them back from the release asserts they are there.
      - name: Download the published checksums
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh release download "v$VERSION" --pattern SHA256SUMS
      - name: Clone the tap
        env:
          TAP_DEPLOY_KEY: ${{ secrets.TAP_DEPLOY_KEY }}
        run: |
          set -euo pipefail
          mkdir -p ~/.ssh
          printf '%s\n' "$TAP_DEPLOY_KEY" > ~/.ssh/tap_key
          chmod 600 ~/.ssh/tap_key
          ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
          echo "GIT_SSH_COMMAND=ssh -i ~/.ssh/tap_key -o IdentitiesOnly=yes" >> "$GITHUB_ENV"
          GIT_SSH_COMMAND="ssh -i ~/.ssh/tap_key -o IdentitiesOnly=yes" \
            git clone --depth 1 git@github.com:fredericrous/homebrew-tap.git tap
      - name: Rewrite the formula
        run: |
          set -euo pipefail
          python3 scripts/bump-tap.py "$VERSION" SHA256SUMS tap/Formula/amont-agent.rb
          ruby -c tap/Formula/amont-agent.rb
      - name: Push
        run: |
          set -euo pipefail
          cd tap
          if git diff --quiet; then
            echo "the tap already carries $VERSION — nothing to publish"
            exit 0
          fi
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add Formula/amont-agent.rb
          git commit -m "chore: amont-agent $VERSION"
          git push origin HEAD
          echo "### Published to the homebrew tap" >> "$GITHUB_STEP_SUMMARY"
          echo "\`brew upgrade fredericrous/tap/amont-agent\` — v$VERSION" >> "$GITHUB_STEP_SUMMARY"

  # The only thing anywhere that runs the formula.
  #
  # `publish-tap` above proves the file PARSES (`ruby -c`) and that its
  # checksums match the release. Neither is the question a user asks, which
  # is "does `brew install` work" — and the gap between those is not
  # theoretical. amont's formula carried a `bin.install` line for a binary
  # that had left its archive three releases earlier; `bin.install` on a file
  # that is not there aborts the whole formula, so every `brew install`
  # failed outright while every release went green. Checksums matched, syntax
  # was valid, and the bumper rewrites only the version and the url/sha
  # pairs, so nothing it touched would notice. A human running `brew upgrade`
  # by hand found it.
  #
  # This job FAILS the run rather than warning. The release is already
  # published by the time it runs and cannot be unpublished, so the point is
  # not to prevent it — it is to make a broken install path impossible to
  # miss. A warning would reproduce exactly the condition that let it ship.
  verify-brew:
    name: brew can install what was published
    needs: [guard, publish-tap]
    if: github.ref_type == 'tag'
    runs-on: macos-latest
    timeout-minutes: 20
    permissions:
      contents: read
    env:
      VERSION: ${{ needs.guard.outputs.version }}
    steps:
      # No checkout: this must see only what a stranger sees — the tap and
      # the published release. Anything read out of the repository would be
      # testing the thing we already know.
      - name: Install from the tap
        run: |
          set -euo pipefail
          brew tap fredericrous/tap
          brew install fredericrous/tap/amont-agent

      - name: The installed binary is the version just published
        run: |
          set -euo pipefail
          amont-agent --version | tee /tmp/v
          grep -qx "amont-agent $VERSION" /tmp/v

      # It still DECIDES, not merely starts. A guard that installs and then
      # refuses nothing is the defect no unit test can see, because tests
      # never touch the packaged artifact — let alone the brewed one.
      - name: The installed guard still says no
        run: |
          set -euo pipefail
          out=$(amont-agent check 'git push origin main 2>&1 | tail -5')
          printf '%s\n' "$out"
          printf '%s' "$out" | grep -q 'pipe-to-tail'

      - name: brew test
        run: brew test fredericrous/tap/amont-agent

      - name: Say so
        run: |
          echo "### brew install verified" >> "$GITHUB_STEP_SUMMARY"
          echo "\`brew install fredericrous/tap/amont-agent\` produces v$VERSION" >> "$GITHUB_STEP_SUMMARY"