zc2 0.0.30

P2P compute broker with credit-based billing, WAL, and broker mesh support
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
name: Auto-release

# Release-branch gated. Fires only when the Build workflow completes
# successfully on a `release/**` branch. The flow is:
#
#   1. someone pushes to release/<X> (or merges to it)
#   2. Build + Docker + security workflows run on that push
#   3. when Build succeeds, this workflow fires
#   4. it bumps the patch version on the release branch, tags, builds
#      release binaries (Linux/macOS/Windows), uploads them, and
#      publishes the crate to crates.io
#
# master push does NOT trigger releases — master is just an integration
# branch. To cut a release, push or merge into release/<X>.

on:
  workflow_run:
    workflows: ["Build"]
    types: [completed]
    branches:
      - 'release/**'
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: auto-release
  cancel-in-progress: false

jobs:
  bump-and-release:
    # Gate on the upstream Build workflow's conclusion. workflow_dispatch
    # bypasses the gate (manual trigger is always allowed).
    if: >
      (github.event_name == 'workflow_dispatch' ||
       github.event.workflow_run.conclusion == 'success') &&
      !contains(github.event.workflow_run.head_commit.message, '[skip release]')
    runs-on: cpu
    timeout-minutes: 10
    # The ephemeral ARC (dind) `cpu` runner doesn't always export $HOME, so the
    # `git config --global` calls in the steps below abort with
    # `fatal: $HOME not set`. Pin the same writable HOME the Build workflow uses.
    env:
      HOME: /home/runner
    outputs:
      tag: ${{ steps.bump.outputs.tag }}
      new_version: ${{ steps.bump.outputs.new_version }}
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0
          token: ${{ secrets.GITHUB_TOKEN }}
          # workflow_run runs in the context of the default branch by
          # default. Pull the actual commit/branch that triggered Build.
          ref: ${{ github.event.workflow_run.head_branch || github.ref }}

      - name: Configure git
        run: |
          # Self-hosted `cpu` (lxd) runners check out into a directory whose
          # ownership differs from the runner user, so plain `git config`
          # (which writes to the repo-local .git/config) dies with
          # "fatal: not in a git directory" / "detected dubious ownership".
          # checkout@v4 only registers safe.directory in a throwaway global
          # config under a temporary HOME, which is gone by this step. Mark
          # the workspace safe and write identity to the persistent global
          # config so this and every later git step (add/commit/tag/push)
          # works on both hosted and self-hosted runners.
          git config --global --add safe.directory "$GITHUB_WORKSPACE"
          git config --global user.name "github-actions[bot]"
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

      - name: Bump patch version in Cargo.toml
        id: bump
        run: |
          set -euo pipefail
          CURRENT=$(grep -E '^version = ' Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
          IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
          NEW="${MAJOR}.${MINOR}.$((PATCH + 1))"
          echo "  current: $CURRENT"
          echo "  new    : $NEW"
          sed -i.bak -E "0,/^version = \".*\"/{s/^version = \".*\"/version = \"$NEW\"/}" Cargo.toml
          rm Cargo.toml.bak
          echo "new_version=$NEW" >> "$GITHUB_OUTPUT"
          echo "tag=v$NEW" >> "$GITHUB_OUTPUT"

      # The ARC `cpu` runner image ships no Rust toolchain -- the same reason
      # Build installs one. Without this the next step dies with
      # `cargo: command not found` (exit 127) and no release is ever cut, which
      # is why auto-release has failed on every run it has ever had.
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable
        with:
          toolchain: "stable"

      # `cargo update` resolves zc2's private git dependency `zakuro-client`
      # (zakuro-ai/zakuro-drive), which the ARC runner cannot fetch anonymously:
      # `--offline` misses (nothing is vendored) and the fallback network fetch
      # dies with "could not read Username for 'https://github.com'". This is
      # the failure #180's Rust fix uncovered. build-and-upload already solves
      # it with DRIVE_DEPLOY_KEY; mirror that here.
      #
      # NOTE the rewrite is scoped to the zakuro-drive repo, NOT to all of
      # `zakuro-ai/` as in build-and-upload. That job never pushes; this one
      # does (`git push origin HEAD:$BRANCH` over the checkout's https remote
      # + token extraheader). An org-wide insteadOf would redirect that push to
      # ssh and fail against this read-only, drive-scoped deploy key.
      - name: Git auth for private zakuro-drive dependency (read-only deploy key)
        run: |
          set -euo pipefail
          mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
          printf '%s\n' "${{ secrets.DRIVE_DEPLOY_KEY }}" > "$HOME/.ssh/zakuro_drive_deploy"
          chmod 600 "$HOME/.ssh/zakuro_drive_deploy"
          ssh-keyscan github.com >> "$HOME/.ssh/known_hosts" 2>/dev/null
          git config --global core.sshCommand "ssh -i $HOME/.ssh/zakuro_drive_deploy -o IdentitiesOnly=yes"
          git config --global url."ssh://git@github.com/zakuro-ai/zakuro-drive".insteadOf "https://github.com/zakuro-ai/zakuro-drive"

      # Only the workspace member's OWN version entry should change here.
      #
      # This previously ran `cargo update --offline --package zc2 ||
      # cargo generate-lockfile`. Once the private-dep auth was fixed the
      # offline arm still failed (nothing is vendored) and the fallback
      # `generate-lockfile` re-resolved EVERY dependency from scratch --
      # rewriting 1344 lines of Cargo.lock and pulling ratatui 0.30.2 +
      # ratatui-core in alongside the pinned 0.26.3, so v0.0.22 was tagged
      # with a lockfile that does not compile:
      #   expected `ratatui_core::style::Style`, found `ratatui::style::Style`
      #
      # `--workspace` restricts the update to workspace members, leaving
      # every external dependency pinned exactly as committed.
      - name: Update Cargo.lock
        run: |
          set -euo pipefail
          cargo update --workspace --offline || cargo update --workspace

      # Guard: a version bump touches only a couple of lines. Anything larger
      # means the lockfile was re-resolved, which is how v0.0.22 shipped
      # unbuildable. Fail loudly instead of tagging a broken tree.
      - name: Assert Cargo.lock was not re-resolved
        run: |
          set -euo pipefail
          CHANGED=$(git diff --numstat -- Cargo.lock | awk '{print $1+$2}')
          CHANGED=${CHANGED:-0}
          echo "Cargo.lock lines changed: $CHANGED"
          if [ "$CHANGED" -gt 20 ]; then
            echo "::error::Cargo.lock changed $CHANGED lines; expected only the zc2 version bump."
            git diff -- Cargo.lock | head -60
            exit 1
          fi

      # Regenerate CHANGELOG.md from conventional commits and extract the
      # new version's section as the GitHub release body (replaces the
      # noise-heavy `gh release --generate-notes`). See cliff.toml. (#103)
      - name: Install git-cliff
        uses: taiki-e/install-action@9e1e5806d4a4822de933115878265be9aaa786d9  # v2
        with:
          tool: git-cliff

      - name: Generate changelog
        run: |
          set -euo pipefail
          git-cliff --config cliff.toml --tag "${{ steps.bump.outputs.tag }}" -o CHANGELOG.md
          git-cliff --config cliff.toml --tag "${{ steps.bump.outputs.tag }}" \
            --unreleased --strip header -o release-notes.md

      - name: Commit + tag
        env:
          # workflow_run hides the source branch in nested fields.
          # workflow_dispatch falls back to the conventional ref_name.
          BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
        run: |
          set -euo pipefail
          git add Cargo.toml Cargo.lock CHANGELOG.md
          git commit -m "chore(release): v${{ steps.bump.outputs.new_version }} [skip release]"
          git tag "${{ steps.bump.outputs.tag }}"
          git push origin "HEAD:$BRANCH"
          git push origin "${{ steps.bump.outputs.tag }}"

      # The ARC `cpu` runner image ships no `gh` CLI: this step used to die with
      # `gh: command not found` (exit 127) AFTER the commit and tag had already
      # been pushed, leaving a tag with no release object -- exactly what
      # happened to v0.0.23. Use the REST API via curl, which the image does
      # have, and fail loudly on a non-2xx response.
      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ steps.bump.outputs.tag }}
        run: |
          set -euo pipefail
          BODY=$(python3 -c 'import json,sys; print(json.dumps(open("release-notes.md").read()))')
          CODE=$(curl -sS -o /tmp/release-resp.json -w '%{http_code}' \
            -X POST \
            -H "Authorization: Bearer $GH_TOKEN" \
            -H "Accept: application/vnd.github+json" \
            "https://api.github.com/repos/${{ github.repository }}/releases" \
            -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":$BODY}")
          echo "HTTP $CODE"
          if [ "$CODE" != "201" ]; then
            cat /tmp/release-resp.json
            echo "::error::Failed to create release $TAG (HTTP $CODE)"
            exit 1
          fi

  build-and-upload:
    needs: bump-and-release
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # Linux x86_64 — statically linked against musl via `cross`.
          # The self-hosted `cpu` (lxd) runner ships glibc 2.39, so a native
          # *-linux-gnu build bakes in a GLIBC_2.39 symbol requirement and
          # refuses to start on older hosts (Ubuntu 22.04 / Debian 12 etc.)
          # with: `version 'GLIBC_2.39' not found`. A musl build has zero
          # libc version dependency and runs on any Linux. Artifact name is
          # unchanged so existing download URLs keep working.
          - runner: cpu
            target: x86_64-unknown-linux-musl
            artifact: zc-linux-x86_64
            binary: zc
            cross: true
          # Linux aarch64 — cross-compiled via the `cross` helper, also musl
          # for the same portability reason (static, no glibc dependency).
          - runner: cpu
            target: aarch64-unknown-linux-musl
            # Name must match asset_name() in src/update.rs, which asks for
            # zc-linux-arm64. It said aarch64 here, so an arm64 host running
            # `zc update` got a 404 for an asset that had been built and
            # uploaded under a name nothing looks for.
            artifact: zc-linux-arm64
            binary: zc
            cross: true
          # macOS Intel — cross-built on macos-14 (Apple Silicon) via
          # `rustup target add x86_64-apple-darwin`. Avoids the queued
          # macos-13 runner pool; both targets ship in the Apple CLT so
          # the produced binary is identical to a native Intel build.
          - runner: macos-14
            target: x86_64-apple-darwin
            # Name must match asset_name() in src/update.rs (zc-darwin-x86_64)
            # and release.yml, the path actually used — v0.0.25 shipped
            # zc-darwin-arm64, not zc-macos-aarch64.
            artifact: zc-darwin-x86_64
            binary: zc
            cross: false
          # macOS Apple Silicon.
          - runner: macos-14
            target: aarch64-apple-darwin
            # See the x86_64 entry above: must match asset_name().
            artifact: zc-darwin-arm64
            binary: zc
            cross: false
          # Windows x86_64 — MSVC toolchain, produces zc.exe.
          - runner: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: zc-windows-x86_64.exe
            binary: zc.exe
            cross: false
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          ref: ${{ needs.bump-and-release.outputs.tag }}

      # zakuro-client is a git dep on the private zakuro-ai/zakuro-drive repo.
      # Auth is a read-only deploy key on zakuro-drive (DRIVE_DEPLOY_KEY holds
      # the private half): rewrite the dep URL to SSH and point git at the key.
      # cargo shells out to the git CLI (.cargo/config.toml: git-fetch-with-cli).
      - name: Git auth for private zakuro-drive dependency (read-only deploy key)
        shell: bash
        run: |
          # This job's matrix spans the self-hosted `cpu` (ARC/dind) runner AND
          # hosted macOS/Windows runners. Only the ARC runner leaves $HOME unset
          # (which would abort `git config --global` with "fatal: $HOME not set").
          # Set a writable HOME *only when missing*, and persist it to later steps
          # (cargo build) via $GITHUB_ENV — a no-op on macOS/Windows where $HOME
          # is already correct, so their builds are untouched.
          if [ -z "${HOME:-}" ]; then
            export HOME=/home/runner
            echo "HOME=/home/runner" >> "$GITHUB_ENV"
          fi
          mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
          printf '%s\n' "${{ secrets.DRIVE_DEPLOY_KEY }}" > "$HOME/.ssh/zakuro_drive_deploy"
          chmod 600 "$HOME/.ssh/zakuro_drive_deploy"
          ssh-keyscan github.com >> "$HOME/.ssh/known_hosts" 2>/dev/null
          git config --global core.sshCommand "ssh -i $HOME/.ssh/zakuro_drive_deploy -o IdentitiesOnly=yes"
          git config --global url."ssh://git@github.com/zakuro-ai/".insteadOf "https://github.com/zakuro-ai/"

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

      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32  # v2

      - name: Install cross (linux-aarch64 only)
        if: matrix.cross
        run: cargo install cross --locked

      # `cross` compiles inside its own container where the deploy key and
      # ssh aren't available. Prefetch on the host: the populated
      # ~/.cargo git/registry caches are mounted into the cross container,
      # so the build inside never needs the network for the private dep.
      - name: Prefetch dependencies on the host (cross only)
        if: matrix.cross
        run: cargo fetch --locked

      # `zc hooks` now lives in the separate zc-hooks crate (
      # private zakuro-client; this job has deploy-key access). See #104.
      - name: cargo build --release (native)
        if: ${{ !matrix.cross }}
        run: cargo build --release --target ${{ matrix.target }}

      - name: cross build --release (cross-compiled)
        if: matrix.cross
        run: cross build --release --target ${{ matrix.target }}

      - name: Package binary (Unix)
        if: runner.os != 'Windows'
        run: |
          mkdir -p dist
          cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dist/${{ matrix.artifact }}"
          chmod +x "dist/${{ matrix.artifact }}"

      - name: Package binary (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force -Path dist | Out-Null
          Copy-Item "target/${{ matrix.target }}/release/${{ matrix.binary }}" "dist/${{ matrix.artifact }}"

      # Also curl rather than `gh` -- see the note on "Create GitHub release".
      # The upload host is uploads.github.com, not api.github.com.
      - name: Upload to release
        shell: bash  # Windows runners default to pwsh, which chokes on \ continuations.
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ needs.bump-and-release.outputs.tag }}
          ARTIFACT: ${{ matrix.artifact }}
        run: |
          set -euo pipefail
          REL_ID=$(curl -sS -H "Authorization: Bearer $GH_TOKEN" \
            -H "Accept: application/vnd.github+json" \
            "https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG" \
            | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | head -1)
          [ -n "$REL_ID" ] || { echo "::error::No release found for tag $TAG"; exit 1; }
          # Replace any existing asset of the same name (the --clobber equivalent).
          EXISTING=$(curl -sS -H "Authorization: Bearer $GH_TOKEN" \
            "https://api.github.com/repos/${{ github.repository }}/releases/$REL_ID/assets" \
            | tr ',' '\n' | grep -B1 "\"name\": *\"$ARTIFACT\"" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | head -1)
          if [ -n "$EXISTING" ]; then
            curl -sS -X DELETE -H "Authorization: Bearer $GH_TOKEN" \
              "https://api.github.com/repos/${{ github.repository }}/releases/assets/$EXISTING" >/dev/null
          fi
          CODE=$(curl -sS -o /tmp/upload-resp.json -w '%{http_code}' \
            -X POST \
            -H "Authorization: Bearer $GH_TOKEN" \
            -H "Content-Type: application/octet-stream" \
            --data-binary @"dist/$ARTIFACT" \
            "https://uploads.github.com/repos/${{ github.repository }}/releases/$REL_ID/assets?name=$ARTIFACT")
          echo "HTTP $CODE"
          if [ "$CODE" != "201" ]; then
            cat /tmp/upload-resp.json
            echo "::error::Failed to upload $ARTIFACT (HTTP $CODE)"
            exit 1
          fi

      - name: Cleanup git SSH config (runner hygiene)
        if: always()
        shell: bash  # Windows defaults to pwsh, which chokes on 2>/dev/null.
        run: |
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/' 2>/dev/null || true
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/zakuro-drive' 2>/dev/null || true
          git config --global --unset-all core.sshCommand 2>/dev/null || true

  publish-crate:
    # Opt-in, and PAUSED by default. crates.io rejects git dependencies, and
    # zc2 depends on zakuro-wire (git) + the optional, private zakuro-client
    # (git, `hooks` feature). Publishing is impossible until BOTH are on a
    # registry — see #104. Setting the
    # `PUBLISH_CRATE` repo variable to `true` enables this job; until the deps
    # are registry-resolvable it will still fail at `cargo publish`, by design.
    # GitHub releases with binaries (above) are the supported channel.
    if: ${{ vars.PUBLISH_CRATE == 'true' }}
    needs: [bump-and-release, build-and-upload]
    runs-on: cpu
    timeout-minutes: 10
    environment: release
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          ref: ${{ needs.bump-and-release.outputs.tag }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable
        with:
          toolchain: "stable"

      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32  # v2

      # `cargo publish --no-verify` skips the repeat full build (already
      # done by build-and-upload). On intermittent crates.io hiccups we
      # retry twice before failing the job.
      - name: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euo pipefail
          if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
            echo "::error::CARGO_REGISTRY_TOKEN is not set. Add it to the 'release' environment (or repo secrets) to enable crates.io publishing."
            exit 1
          fi
          for attempt in 1 2 3; do
            if cargo publish --no-verify --token "$CARGO_REGISTRY_TOKEN"; then
              exit 0
            fi
            echo "publish attempt $attempt failed; retrying in 20s"
            sleep 20
          done
          exit 1