diff --git a/.github/workflows/backup-relay-state.yml b/.github/workflows/backup-relay-state.yml index fab7333..a220a73 100644 --- a/.github/workflows/backup-relay-state.yml +++ b/.github/workflows/backup-relay-state.yml @@ -2,16 +2,17 @@ name: backup wireup-relay /data # Nightly snapshot of the wireup-relay Fly volume mounted at /data. # - tars /data on the machine via `flyctl ssh console -C tar ...` # - downloads the tar into the runner -# - uploads as a GitHub Actions artifact (90-day retention, free) +# - uploads as a GitHub Actions artifact (90-day retention) # # Restore path: # gh run download -n wireup-relay-data # flyctl ssh sftp shell -a wireup-relay <<< 'put data.tar.gz /data/' # flyctl ssh console -a wireup-relay -C 'tar xzf /data/data.tar.gz -C /data/' # -# Why not R2/S3: those need a new account + creds. GHA artifacts cost $0, -# survive 90 days, and don't add operational surface. Switch to R2 when -# the relay holds anything that matters past 90 days. +# Why not R2/S3: those need a new account + creds. GHA artifacts count toward +# pooled Actions storage, although this public repo currently receives a full +# billing discount. They survive 90 days without adding operational surface. +# Switch to R2 when the relay holds anything that matters past 90 days. on: schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b977eb..6564d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,145 +6,99 @@ on: pull_request: branches: [main] +concurrency: + group: ci-${{ github.ref }} + # Cancel superseded PRs, but let main warmers finish and save their caches. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: - test: + # Pull requests restore the shared default-branch caches but never create + # merge-ref copies. Main builds once per platform to keep those caches warm. + warm-cache-linux: + if: github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: cargo build --all-targets - # Force single-threaded test execution to keep the heavy real-process - # e2e binaries (e.g. e2e_detached_pair, with real daemons + a SAS - # handshake under a tight deadline) from self-contending under default - # parallelism on the 2-core ubuntu-latest runner. Lib + non-heavy e2e - # cost is negligible serialized; heavy e2e is where the wins are. See - # feedback/heavy-e2e-subprocess-contention. (The UDS round-trip - # Broken-pipe/os-error-32 flake this also used to mask was root-caused + - # fixed in #241 — the canned test server's single-read racing - # uds_request's two-write — so serialization now stands on heavy-e2e - # contention alone, not a live flake.) - - run: cargo test --all-targets -- --test-threads=1 - - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable with: - components: rustfmt - - run: cargo fmt --all -- --check - - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --all-targets -- -D warnings - - demo-invite: - # Runs demo-invite.sh end-to-end: local relay + paul + willard + one-paste - # invite-URL pair + bidirectional signed send/recv. Catches regressions in - # the v0.4.0 pair_drop hook, daemon-pull cursor persist, and CLI wiring. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: sudo apt-get update && sudo apt-get install -y jq + shared-key: ci-linux + save-if: true + - run: cargo build --all-targets - run: cargo build --release --bin wire - - run: WIRE=./target/release/wire bash demo-invite.sh - demo-command: - # Runs the shipped `wire demo` verb end-to-end: it self-execs to boot an - # ephemeral local relay, mint two identities, pair them, and verify a - # signed round-trip — then tears it all down. Guards the one-command - # try-before-you-install path (and its self-exec orchestration). - runs-on: ubuntu-latest + warm-cache-windows: + if: github.event_name == 'push' + runs-on: windows-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-windows + save-if: true - run: cargo build --release --bin wire - - run: ./target/release/wire demo --json | tee /dev/stderr | grep -q '"ok":true' - hello-world: - # The canonical FIRST-CONNECTION round-trip gate (ROAD_TO_1.0 §3 lifecycle - # hardening). Reproduces the README "60-second local demo" exactly — local - # relay + two independent `wire up` agents + dial-by-persona + receive + - # autonomous reply — across several fresh-box iterations to catch the - # intermittent daemon-survival / first-connection regressions a single-shot - # `wire demo` or install-smoke wouldn't. This is the harness that caught the - # #263 daemon-self-abort + one-directional-pin bugs; it must stay green. + test: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: sudo apt-get update && sudo apt-get install -y bc - - run: cargo build --release --bin wire - - run: WIRE_BIN=./target/release/wire bash scripts/hello-world-validate.sh 5 + with: + shared-key: ci-linux + save-if: false + - run: cargo build --all-targets + # Force single-threaded test execution to keep the heavy real-process + # e2e binaries from self-contending on the hosted runner. + - run: cargo test --all-targets -- --test-threads=1 - demo-hotline: - # Runs demo-hotline.sh end-to-end: local relay + 5 agents with vibes + - # full mesh via `wire add` + signed ring-send. Validates the v0.5 - # handle directory, .well-known/wire/agent resolver, pair_drop + - # pair_drop_ack flow, profile fields, and 5-way mesh under one daemon. + fmt: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: sudo apt-get update && sudo apt-get install -y jq - - run: cargo build --release --bin wire - - run: WIRE=./target/release/wire bash demo-hotline.sh + with: + components: rustfmt + - run: cargo fmt --all -- --check - integration-tests: - # Real end-to-end CLI tests (tests/it/): each boots actual relay processes - # and drives the shipped binary like a user/script would — zero-paste - # pairing + bidirectional signed messages, the on-box sister mesh, - # `wire up` onboarding (+ --offline), nuke/recovery, and group join-by-code - # with cross-member verified read. Covers the CLI seams `cargo test`'s - # library-level tests don't (arg parsing, process lifecycle, multi-agent - # connections). See tests/it/README.md. + clippy: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable + with: + components: clippy - uses: Swatinem/rust-cache@v2 - - run: sudo apt-get update && sudo apt-get install -y jq - - run: cargo build --release --bin wire - - run: WIRE=./target/release/wire tests/it/run-all.sh + with: + shared-key: ci-linux + save-if: false + - run: cargo clippy --all-targets -- -D warnings docs-lint: - # Catch deprecated command phrases drifting back into docs. Per the - # swift-harbor / dthoma1 #145 audit: README + AGENTS + INSTALL had - # several stale shapes (`wire add bob@`, `wire init `, `wire - # daemon start`, `wire.slancha.ai`, `wire up @`) that - # contradicted the canonical v0.12+ surface. Lint blocks regression. - # - # Legacy sections explicitly named "Legacy" or "Legacy flows" are - # allowed to mention deprecated verbs — the lint scopes its grep - # exclusions to those headers (handled inline via `grep -v`). + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: deprecated phrases (canonical surface drift) run: | set -e - FILES="README.md AGENTS.md INSTALL.md docs/integrations/*.md docs/AGENT_INTEGRATION.md" + FILES=(README.md AGENTS.md INSTALL.md docs/AGENT_INTEGRATION.md docs/integrations/*.md) + for file in "${FILES[@]}"; do + if [ ! -f "$file" ]; then + echo "::error::missing docs-lint input: $file" + exit 1 + fi + done fail=0 check() { local pattern="$1" local description="$2" - # Exclude lines inside Legacy sections (best-effort: grep -v on - # the line itself contains 'Legacy' marker; deeper context - # exclusion would need a real parser). - hits=$(grep -nE "$pattern" $FILES 2>/dev/null \ + hits=$(grep -nE "$pattern" "${FILES[@]}" \ | grep -v -iE '(legacy|deprecated|removed|v1\.0 removes)' \ || true) if [ -n "$hits" ]; then @@ -154,11 +108,10 @@ jobs: fi } - # The shapes swift-harbor's audit (#145) called out: - check 'wire add [a-z][a-z-]*@' 'wire add @ (use `wire dial`)' - check 'wire init ' 'wire init (use `wire up`)' - check 'wire daemon start' 'wire daemon start (no subcommand; use `wire daemon`)' - check 'wire up @' 'wire up @ (handle is DID-derived; use `wire up @`)' + check 'wire add [a-z][a-z-]*@' 'wire add @ (use wire dial)' + check 'wire init ' 'wire init (use wire up)' + check 'wire daemon start' 'wire daemon start (no subcommand; use wire daemon)' + check 'wire up @' 'wire up @ (handle is DID-derived; use wire up @)' check 'wire\.slancha\.ai' 'wire.slancha.ai (use wireup.net)' check 'relay\.slancha\.ai' 'relay.slancha.ai (use wireup.net)' @@ -170,69 +123,112 @@ jobs: fi echo "docs-lint: clean." - install-smoke: - # Fresh-user first-run path. Builds the binary THIS PR produces, puts it - # on PATH the way an install leaves it, and — from a clean cwd with an - # empty WIRE_HOME — runs the offline out-of-the-box sequence. Catches - # changes that compile + pass tests but break the new-user experience - # (first-run offline identity, --json shape, basic subcommands). Also - # This is the "test-env in CI" smoke; the demos cover deeper e2e flows. + linux-e2e: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest + env: + WIRE_HOME_FORCE: "1" steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: sudo apt-get update && sudo apt-get install -y jq + with: + shared-key: ci-linux + save-if: false + - run: sudo apt-get update && sudo apt-get install -y bc jq - run: cargo build --release --bin wire - - name: fresh-user smoke (offline, clean WIRE_HOME) + + - name: invite round-trip + run: WIRE=./target/release/wire bash demo-invite.sh + + - name: one-command demo + run: ./target/release/wire demo --json | tee /dev/stderr | grep -q '"ok":true' + + - name: first-connection round-trips + run: WIRE_BIN=./target/release/wire bash scripts/hello-world-validate.sh 5 + + - name: five-agent mesh + run: WIRE=./target/release/wire bash demo-hotline.sh + + - name: CLI integration suite + run: WIRE=./target/release/wire tests/it/run-all.sh + + - name: fresh-user and nuke smoke run: | set -euo pipefail sudo install -m 0755 target/release/wire /usr/local/bin/wire - export WIRE_HOME="$(mktemp -d)/home" WIRE_QUIET_AUTOSESSION=1 - cd "$(mktemp -d)" # clean cwd — no repo, no existing state + WIRE_HOME="$(mktemp -d)/home" + export WIRE_HOME + export WIRE_QUIET_AUTOSESSION=1 + cd "$(mktemp -d)" wire --version wire --help >/dev/null - wire whoami --json | jq -e '.initialized == false' # pre-init - # `wire up --no-local` creates the local identity but also tries to - # claim its DID-derived handle on the shared relay — that claim can - # 409 if the random persona is already taken (non-fatal). The real - # check is the local identity, so tolerate a nonzero exit and assert - # via whoami. + wire whoami --json | jq -e '.initialized == false' wire up --no-local || true wire whoami --json | jq -e '.did | startswith("did:wire:")' wire here >/dev/null - echo "install-smoke: fresh-user offline path OK" - - name: nuke smoke (dry-run changes nothing; --force resets; re-up works) - run: | - set -euo pipefail - export WIRE_HOME="$(mktemp -d)/home" WIRE_QUIET_AUTOSESSION=1 + + WIRE_HOME="$(mktemp -d)/home" + export WIRE_HOME wire up --no-local || true - wire whoami --json | jq -e '.did | startswith("did:wire:")' # identity created - wire nuke --dry-run # lists, must change nothing - wire whoami --json | jq -e '.did | startswith("did:wire:")' # dry-run kept it + wire whoami --json | jq -e '.did | startswith("did:wire:")' + wire nuke --dry-run + wire whoami --json | jq -e '.did | startswith("did:wire:")' wire nuke --force --json | jq -e 'has("removed_paths")' - wire whoami --json | jq -e '.initialized == false' # state wiped - wire up --no-local || true # fresh again + wire whoami --json | jq -e '.initialized == false' + wire up --no-local || true wire whoami --json | jq -e '.did | startswith("did:wire:")' - echo "install-smoke: nuke + re-up OK" + echo "install-smoke: fresh-user + nuke OK" + + - name: install.sh from a clean machine, then fresh-user onboarding + run: | + set -euo pipefail + DIST="$(mktemp -d)" + case "$(uname -m)" in + x86_64|amd64) triple="x86_64-unknown-linux-musl" ;; + aarch64|arm64) triple="aarch64-unknown-linux-musl" ;; + *) echo "unsupported arch $(uname -m)" >&2; exit 1 ;; + esac + cp target/release/wire "$DIST/wire-$triple" + ( cd "$DIST" && sha256sum "wire-$triple" > "wire-$triple.sha256" ) + ( cd "$DIST" && exec python3 -m http.server 8099 >/dev/null 2>&1 ) & + SRV=$!; trap 'kill "$SRV" 2>/dev/null || true' EXIT + for _ in {1..20}; do + curl -fsS "http://127.0.0.1:8099/wire-$triple.sha256" -o /dev/null && break + sleep 0.5 + done + + PFX="$(mktemp -d)/bin" + WIRE_DIST_URL="http://127.0.0.1:8099" sh install.sh --prefix "$PFX" + test -x "$PFX/wire" + "$PFX/wire" --version + WIRE_HOME="$(mktemp -d)/home" + export WIRE_HOME + export WIRE_QUIET_AUTOSESSION=1 + cd "$(mktemp -d)" + "$PFX/wire" whoami --json | jq -e '.initialized == false' + "$PFX/wire" up --offline + "$PFX/wire" whoami --json | jq -e '.did | startswith("did:wire:")' + echo "install-script-smoke: install.sh + fresh onboarding OK" install-smoke-windows: - # Windows leg of the cross-platform gate: build wire.exe and run the - # fresh-user + nuke smoke in PowerShell from a clean WIRE_HOME. Windows - # can't be exercised on the maintainer's mac/linux hosts, so this CI - # job is the primary Windows signal (plus an operator confirm on a real - # box before merge). + if: github.event_name == 'pull_request' runs-on: windows-latest + env: + WIRE_HOME_FORCE: "1" steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-windows + save-if: false - run: cargo build --release --bin wire - name: fresh-user + nuke smoke (PowerShell) shell: pwsh run: | $ErrorActionPreference = "Stop" - $PSNativeCommandUseErrorActionPreference = $true # native non-zero exit → throw + $PSNativeCommandUseErrorActionPreference = $true $wire = ".\target\release\wire.exe" $env:WIRE_HOME = Join-Path $env:RUNNER_TEMP ("wh-" + [guid]::NewGuid().ToString("N")) $env:WIRE_QUIET_AUTOSESSION = "1" @@ -240,10 +236,6 @@ jobs: & $wire --help | Out-Null $pre = (& $wire whoami --json | ConvertFrom-Json) if ($pre.initialized -ne $false) { throw "expected initialized:false pre-init" } - # `wire up --no-local` creates the local identity but also tries to - # claim its handle on the shared relay; that claim can 409 (handle - # already taken) which is non-fatal. Tolerate a nonzero exit and - # assert the real outcome (local DID) via whoami. try { & $wire up --no-local } catch { Write-Host "wire up claim non-fatal: $_" } $post = (& $wire whoami --json | ConvertFrom-Json) if (-not $post.did.StartsWith("did:wire:")) { throw "expected a did:wire: DID" } @@ -258,60 +250,3 @@ jobs: $fresh = (& $wire whoami --json | ConvertFrom-Json) if (-not $fresh.did.StartsWith("did:wire:")) { throw "re-up should recreate identity" } Write-Host "install-smoke-windows: fresh-user + nuke OK" - - install-script-smoke: - # The REAL installer, end-to-end. `install-smoke` copies the binary - # straight onto PATH and never touches install.sh — so the path a user - # actually runs (`curl … | sh`: triple detection → download → SHA-256 - # verify → --prefix install → PATH/next-steps) is untested. This serves a - # locally-built binary as a release asset over http and runs install.sh - # against it (WIRE_DIST_URL override — no real GitHub Release needed), - # then drives the fresh-user onboarding through the installed binary. - # Catches installer bugs (wrong triple/asset name, broken verify, bad - # install dir) that compile + pass tests but break first-touch install. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: sudo apt-get update && sudo apt-get install -y jq - - run: cargo build --release --bin wire - - name: install.sh from a clean machine, then fresh-user onboarding - # Serve + install + assert in ONE step so the http server's lifetime is - # controlled (a background server from an earlier step isn't guaranteed - # to survive into the next). - run: | - set -euo pipefail - DIST="$(mktemp -d)" - # Resolve the SAME triple install.sh will request on this runner, so - # the served asset name matches (mirrors install.sh's Linux branch). - case "$(uname -m)" in - x86_64|amd64) triple="x86_64-unknown-linux-musl" ;; - aarch64|arm64) triple="aarch64-unknown-linux-musl" ;; - *) echo "unsupported arch $(uname -m)" >&2; exit 1 ;; - esac - cp target/release/wire "$DIST/wire-$triple" - ( cd "$DIST" && sha256sum "wire-$triple" > "wire-$triple.sha256" ) - # Serve over http so install.sh's curl exercises the real download + - # SHA-256-sibling verify path (not a file:// shortcut, which would let - # install.sh silently cargo-fall-back to the crates.io release). - # Background the subshell itself (not `& )` inside it) so $! captures - # the server PID — `& )` backgrounds within the subshell and leaves $! - # unset, which is fatal under `set -u`. - ( cd "$DIST" && exec python3 -m http.server 8099 >/dev/null 2>&1 ) & - SRV=$!; trap 'kill "$SRV" 2>/dev/null || true' EXIT - for i in $(seq 1 20); do curl -fsS "http://127.0.0.1:8099/wire-$triple.sha256" -o /dev/null && break; sleep 0.5; done - - PFX="$(mktemp -d)/bin" - WIRE_DIST_URL="http://127.0.0.1:8099" sh install.sh --prefix "$PFX" - test -x "$PFX/wire" # landed where --prefix said - "$PFX/wire" --version - export WIRE_HOME="$(mktemp -d)/home" WIRE_QUIET_AUTOSESSION=1 - cd "$(mktemp -d)" # clean cwd, no repo - "$PFX/wire" whoami --json | jq -e '.initialized == false' # pre-init - # up --offline is PR-only — if install.sh had cargo-fallen-back to the - # crates.io release, this flag wouldn't exist, so it also proves the - # SERVED binary was the one installed. - "$PFX/wire" up --offline - "$PFX/wire" whoami --json | jq -e '.did | startswith("did:wire:")' - echo "install-script-smoke: install.sh + fresh onboarding OK" diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml index f92687b..6cef784 100644 --- a/.github/workflows/fly-deploy.yml +++ b/.github/workflows/fly-deploy.yml @@ -5,6 +5,15 @@ on: push: branches: - main + # The production Dockerfile does not consume these paths. + paths-ignore: + - '**/*.md' + - 'docs/**' + - 'skills/**' + - 'hooks/**' + - 'examples/**' + - 'scoop/**' + - '.gemini-plugin/**' jobs: deploy: name: Deploy app diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3cbf8b9..b446a77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,8 @@ jobs: - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} + # Tag-scoped caches cannot warm later branches; restore only. + save-if: false - name: Install cross (for non-host Linux builds) if: matrix.cross == true @@ -83,6 +85,8 @@ jobs: with: name: wire-${{ matrix.target }} path: dist/* + # The durable copy lives on the GitHub Release below. + retention-days: 1 release: name: github release @@ -114,6 +118,9 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + with: + # Publishing runs after the build matrix and has no reusable tag scope. + save-if: false # v0.6.8: this is the load-bearing fix. Pre-v0.6.8 release.yml # had no `cargo publish` step, so every tagged release built diff --git a/SESSION_LOG_2026_07_18.md b/SESSION_LOG_2026_07_18.md index ff6a2a0..8898a92 100644 --- a/SESSION_LOG_2026_07_18.md +++ b/SESSION_LOG_2026_07_18.md @@ -200,3 +200,103 @@ Its source file `~/Source/dotfiles-claude/codex/AGENTS.preamble.md` still says a fixed Wire session override is current. The owner of that worktree should replace that statement with: Codex exposes `CODEX_THREAD_ID`; Wire v0.17.0+ uses it automatically; do not set one global `WIRE_SESSION_ID`. + +## GitHub Actions usage reduction + +### Diagnosis + +GitHub's organization billing API showed high gross-equivalent Actions usage +but zero net charges for Wire: $44.37 gross / $0 net in May, $66.34 / $0 in +June, and $8.20 / $0 through July 18. The public repository's standard runner +usage and storage were fully discounted. + +The volume was real: + +- May: 699 workflow runs, including 404 CI runs, 177 Fly deploys, and 99 + release runs. June: 671 runs, including 460 CI and 178 Fly deploys. +- Current CI launched twelve jobs per run and built the same Linux release + binary independently in seven jobs. Twenty-five July CI runs created 300 + jobs; pull-request checks repeated after every merge on `main`. +- Fifty-five job/ref-specific Rust caches occupied 11.76 GB. +- GitHub retained 684 Actions artifacts / 2.30 GB. Six hundred eighteen were + six-platform temporary handoffs from 101 releases, duplicating durable + GitHub Release assets. Nightly relay backups accounted for only 142 MB. + +Selected design and implementation plan: + +- `docs/superpowers/specs/2026-07-18-github-actions-cost-reduction-design.md` +- `docs/superpowers/plans/2026-07-18-github-actions-cost-reduction.md` + +The pre-change structural assertion failed as expected with +`missing linux-e2e`, proving the approved consolidated job and one-day release +handoff policy were absent before implementation. + +### Decisions and changes + +- Pull requests now run six protected checks: `test`, `fmt`, `clippy`, + `docs-lint`, `linux-e2e`, and `install-smoke-windows`. Main pushes run only + Linux and Windows cache warmers. +- The Linux end-to-end job builds the release binary once, then runs the invite, + one-command demo, five-iteration hello-world, five-agent mesh, CLI integration, + fresh-user/nuke, and installer callers serially. Linux and Windows smoke jobs + set `WIRE_HOME_FORCE=1` at job scope. +- Rust caches use stable platform shared keys. Pull-request jobs restore but do + not save; main warmers save and cannot be cancelled before their post-job + cache upload. Superseded pull-request runs still cancel by ref. +- Temporary six-platform release handoffs now expire after one day; tag and + publish jobs restore but do not save Rust caches. Durable release assets are + unchanged. +- Fly deploy skips paths the production Dockerfile does not consume. Nightly + backups retain 90 days; their storage comment now describes the public-repo + discount precisely. +- `require-ci.sh` contains the six replacement contexts. It was executed only + after pull request #372 proved all six new check names green. Post-update + verification showed strict checks and admin enforcement still enabled, force + pushes and deletions still disabled, and the pull request clean/mergeable. + +At July's observed mix of 15 pull-request and 10 main CI runs, the new topology +would launch 110 jobs instead of 300 (63% fewer). Release-binary build +invocations fall from seven per CI run (175 at that mix) to one per run (25, +86% fewer). These are topology projections; actual post-merge usage still needs +measurement from GitHub. + +### Verification and review + +- `actionlint .github/workflows/*.yml`: pass. +- Structural policy assertion: pass with eight jobs, six protected pull-request + checks, one Linux release build, forced homes, one-day handoffs, expected Fly + exclusions, and exact protection contexts. +- Focused review regressions ran RED then GREEN for pull-request-only + cancellation and missing docs-lint inputs. The current docs-lint body also + ran successfully against the repository. +- `test-env/run.sh`: pass. This covered formatting, clippy, 658 passing library + tests (one ignored), 72 CLI tests, serial end-to-end suites, release build, + demos, and all 11 CLI integration scripts. Tests ran inside Docker, not the + live Wire home. +- Cross-provider review ran three bounded cycles. Accepted findings prevented + main cache-warmers from being cancelled and made missing docs inputs fail + closed. Final review returned no blocker or major findings. Remaining minors + describe preserved demo-pipeline behavior; script inspection confirmed each + demo/integration caller uses isolated state. +- Pull request #372's first GitHub run passed all six replacement checks: + docs-lint 3s, fmt 14s, clippy 45s, test 2m37s, Linux end-to-end 2m54s, and + cold-cache Windows smoke 5m30s. Both main-only warmers skipped as designed. +- GitNexus compare against `origin/main`: low risk, zero affected runtime + processes. `git diff --check`: pass. + +Live launchd services, Wire homes, existing caches, existing artifacts, Fly +state, releases, and deployments were not changed. Branch protection changed +only its required-context list after the new contexts passed. + +### Artifacts + +- `.github/workflows/ci.yml` — bounded PR checks and main cache warmers. +- `.github/workflows/release.yml` — one-day temporary handoffs and restore-only + release caches. +- `.github/workflows/fly-deploy.yml` — non-image path exclusions. +- `.github/workflows/backup-relay-state.yml` — accurate storage-cost note. +- `require-ci.sh` — post-validation protection migration caller. +- `docs/superpowers/specs/2026-07-18-github-actions-cost-reduction-design.md` — + approved design. +- `docs/superpowers/plans/2026-07-18-github-actions-cost-reduction.md` — execution + and verification plan. diff --git a/docs/superpowers/plans/2026-07-18-github-actions-cost-reduction.md b/docs/superpowers/plans/2026-07-18-github-actions-cost-reduction.md new file mode 100644 index 0000000..21cbc4e --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-github-actions-cost-reduction.md @@ -0,0 +1,233 @@ +# GitHub Actions Cost Reduction Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Preserve Wire's protected behavior coverage while cutting GitHub Actions job fan-out, repeated compilation, fragmented caches, redundant artifact retention, and docs-only deployments. + +**Architecture:** Full behavior checks run once on pull requests in six jobs; merged `main` commits run two shared-cache warmers instead of the full suite. One serial Linux end-to-end job builds the release binary once and drives every existing Linux CLI caller. Release artifacts remain durable on GitHub Releases while their temporary workflow handoffs expire after one day. + +**Tech Stack:** GitHub Actions YAML, Bash and PowerShell workflow steps, Cargo/Rust, `Swatinem/rust-cache@v2`, `actionlint`, Docker-backed `test-env/run.sh`, GitHub branch protection API. + +## Global Constraints + +- Do not delete existing Actions artifacts or caches in this branch. +- Do not mutate production Fly state, secrets, branch protection, or live Wire services during local implementation. +- Preserve every current Linux and Windows behavior command. +- Every temporary `WIRE_HOME` used by spawned Wire processes sets `WIRE_HOME_FORCE=1`. +- Keep six release targets and nightly backups at 90-day retention. +- Run GitNexus change detection before every commit and `test-env/run.sh` before publication. + +--- + +### Task 1: Prove current workflow violates the approved budget + +**Files:** +- Inspect: `.github/workflows/ci.yml` +- Inspect: `.github/workflows/release.yml` +- Inspect: `.github/workflows/fly-deploy.yml` +- Inspect: `require-ci.sh` + +**Interfaces:** +- Consumes: current workflow job IDs, cache inputs, triggers, and artifact retention. +- Produces: captured RED evidence for missing consolidation and retention controls. + +- [ ] **Step 1: Run a structural assertion against the current YAML** + +Run a Ruby YAML assertion that expects the approved end state: + +```bash +ruby -ryaml -e ' +ci = YAML.load_file(".github/workflows/ci.yml") +jobs = ci.fetch("jobs") +raise "missing linux-e2e" unless jobs.key?("linux-e2e") +raise "legacy demo job remains" if jobs.key?("demo-invite") +release = YAML.load_file(".github/workflows/release.yml") +upload = release.dig("jobs", "build", "steps").find { |s| s["uses"] == "actions/upload-artifact@v7" } +raise "release retention is not one day" unless upload.dig("with", "retention-days") == 1 +' +``` + +Expected: FAIL with `missing linux-e2e` before production YAML changes. + +- [ ] **Step 2: Record RED evidence in `SESSION_LOG_2026_07_18.md`** + +Record the command, expected failure, current 12-job PR fan-out, seven Linux release builds, 11.76 GB cache usage, and 2.30 GB artifact usage. + +### Task 2: Consolidate CI and cache ownership + +**Files:** +- Modify: `.github/workflows/ci.yml` +- Modify: `require-ci.sh` + +**Interfaces:** +- Consumes: existing demo scripts, `scripts/hello-world-validate.sh`, `tests/it/run-all.sh`, installer smoke commands, and Windows smoke commands. +- Produces: PR checks `test`, `fmt`, `clippy`, `docs-lint`, `linux-e2e`, and `install-smoke-windows`; main jobs `warm-cache-linux` and `warm-cache-windows`. + +- [ ] **Step 1: Add event conditions and concurrency** + +Keep `push: main` and `pull_request: main`; add: + +```yaml +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true +``` + +Gate the six protected jobs with `if: github.event_name == 'pull_request'`. +Gate both warmers with `if: github.event_name == 'push'`. + +- [ ] **Step 2: Establish shared restore-only PR caches** + +Use this Linux cache configuration in `test`, `clippy`, and `linux-e2e`: + +```yaml +- uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-linux + save-if: ${{ github.event_name == 'push' }} +``` + +Because those jobs run only on pull requests, they restore the default-branch +cache without saving merge-ref copies. `warm-cache-linux` uses the same +`shared-key`, runs both `cargo build --all-targets` and +`cargo build --release --bin wire`, and saves on `main`. + +Use `shared-key: ci-windows` in the Windows smoke and warmer with the same +save policy. The main warmer runs `cargo build --release --bin wire`. + +- [ ] **Step 3: Replace seven Linux jobs with `linux-e2e`** + +Set `WIRE_HOME_FORCE: "1"` at job scope, install `jq` and `bc` once, build the +release binary once, then preserve these existing steps in serial order: + +```bash +WIRE=./target/release/wire bash demo-invite.sh +./target/release/wire demo --json | tee /dev/stderr | grep -q '"ok":true' +WIRE_BIN=./target/release/wire bash scripts/hello-world-validate.sh 5 +WIRE=./target/release/wire bash demo-hotline.sh +WIRE=./target/release/wire tests/it/run-all.sh +``` + +Follow them with the existing fresh-user/nuke smoke and local HTTP installer +smoke bodies unchanged except for inheriting `WIRE_HOME_FORCE=1`. + +- [ ] **Step 4: Update the protection caller** + +Replace obsolete contexts in `require-ci.sh` with exactly: + +```json +{"context": "test"}, +{"context": "fmt"}, +{"context": "clippy"}, +{"context": "docs-lint"}, +{"context": "linux-e2e"}, +{"context": "install-smoke-windows"} +``` + +Do not execute `require-ci.sh` locally. It is the post-push migration caller. + +- [ ] **Step 5: Re-run structural assertions** + +Run the Task 1 Ruby assertion plus checks for six pull-request jobs, two push +warmers, shared cache keys, and the six protection contexts. + +Expected: PASS. + +### Task 3: Bound release storage and docs-only deploys + +**Files:** +- Modify: `.github/workflows/release.yml` +- Modify: `.github/workflows/backup-relay-state.yml` +- Modify: `.github/workflows/fly-deploy.yml` + +**Interfaces:** +- Consumes: six-target release matrix, release aggregation job, nightly relay backup, and Fly root Docker build. +- Produces: one-day temporary release handoffs, restore-only tag caches, accurate backup cost documentation, and safe deploy exclusions. + +- [ ] **Step 1: Bound tag-only cache and handoff lifetime** + +For release build and crates.io publish cache actions, add: + +```yaml +save-if: false +``` + +For the release build `upload-artifact` step, add: + +```yaml +retention-days: 1 +``` + +- [ ] **Step 2: Exclude non-image paths from Fly deploy** + +Under the `push: main` trigger, add `paths-ignore` entries for `**/*.md`, +`docs/**`, `skills/**`, `hooks/**`, `examples/**`, `scoop/**`, and +`.gemini-plugin/**`. None is copied by the root production Dockerfile. + +- [ ] **Step 3: Correct backup storage documentation** + +Replace the claim that GitHub Actions artifacts cost zero with the precise +statement that snapshots count toward pooled Actions storage even while the +current public-repository discount makes net ledger cost zero. + +### Task 4: Verify, review, publish, and hand off protection migration + +**Files:** +- Modify: `SESSION_LOG_2026_07_18.md` +- Verify: all changed workflow and caller files + +**Interfaces:** +- Consumes: changed workflows and protection caller. +- Produces: verified branch, review evidence, pushed commits, and exact live rollout handoff. + +- [ ] **Step 1: Run deterministic checks** + +```bash +actionlint .github/workflows/*.yml +git diff --check +test-env/run.sh +``` + +Expected: actionlint and diff checks exit zero; canonical container gate passes +formatting, clippy, serial Rust tests, release build, demos, and integration scripts. + +- [ ] **Step 2: Run GitNexus change detection** + +```bash +node .gitnexus/run.cjs detect-changes --scope compare --base-ref origin/main --repo wire --branch chore/reduce-actions-usage +``` + +Expected: workflow, shell caller, docs, and no Rust execution-flow changes. + +- [ ] **Step 3: Run one independent semantic review** + +Use `build-loop/scripts/review.py` with writer `codex`, reviewer `claude`, and a +packet covering preserved commands, event conditions, cache scope, retention, +deploy exclusions, branch-protection migration, and deterministic results. + +Expected: no BLOCKER or MAJOR finding. Fix accepted findings within the +three-cycle cap and rerun deterministic checks. + +- [ ] **Step 4: Commit logical units and push** + +```bash +git add .github/workflows/ci.yml require-ci.sh +git commit -m "ci: consolidate protected checks" +git add .github/workflows/release.yml .github/workflows/backup-relay-state.yml .github/workflows/fly-deploy.yml +git commit -m "ci: bound Actions storage and deploys" +git add SESSION_LOG_2026_07_18.md docs/superpowers/plans/2026-07-18-github-actions-cost-reduction.md +git commit -m "docs: record Actions usage reduction" +git push -u origin chore/reduce-actions-usage +``` + +- [ ] **Step 5: Inspect real checks without changing protection** + +Create a pull request only if publication workflow calls for it, inspect its +GitHub Actions check names and durations, and report the exact command still +required after checks are green: + +```bash +./require-ci.sh +``` + +Do not run that mutation or merge without explicit rollout authority. diff --git a/docs/superpowers/specs/2026-07-18-github-actions-cost-reduction-design.md b/docs/superpowers/specs/2026-07-18-github-actions-cost-reduction-design.md new file mode 100644 index 0000000..7f88ae1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-18-github-actions-cost-reduction-design.md @@ -0,0 +1,101 @@ +# GitHub Actions cost-reduction design + +## Goal + +Keep Wire's protected cross-platform behavior coverage while removing repeated +compilation, duplicate post-merge validation, fragmented caches, unnecessary +deploys, and long-lived release handoff artifacts. + +## Evidence + +- GitHub's billing ledger reports zero net Actions charges because Wire is a + public repository on standard runners, but gross-equivalent usage reached + $44.37 in May, $66.34 in June, and $8.20 through July 18. +- May produced 699 workflow runs, including 99 releases and 404 CI runs. June + produced 671 runs, including 460 CI runs. +- Current CI launches twelve jobs per run. Seven Linux jobs independently run + `cargo build --release --bin wire`; 25 July CI runs created 300 jobs. +- Fifty-five active Rust caches occupy 11.76 GB because the default cache key + includes job ID and Git ref. Base-branch caches are available to pull + requests, but pull-request caches are not reusable by unrelated branches. +- GitHub retains 684 artifacts / 2.30 GB. Six hundred eighteen are temporary + six-platform release handoffs already duplicated on durable GitHub Releases. +- Branch protection requires the twelve current CI job names. Any job + consolidation therefore needs a matching `require-ci.sh` context migration. + +## Selected architecture + +### Pull-request CI + +Run six protected jobs on pull requests: + +1. `test` — existing serial all-target Rust test gate. +2. `fmt` — Rust formatting. +3. `clippy` — Rust lint. +4. `docs-lint` — existing documentation surface check. +5. `linux-e2e` — one release build followed by the existing demo, hello-world, + integration, fresh-install, nuke, and installer checks in serial order. +6. `install-smoke-windows` — existing Windows build and smoke behavior. + +Serial Linux end-to-end execution is intentional. These tests already use +isolated homes and processes, and serial execution removes seven redundant +release builds without dropping a caller path. + +### Main-branch cache warming + +Do not rerun the complete protected suite after a checked pull request merges. +On `main`, run one Linux and one Windows cache-warming build. Both write stable +shared caches; pull-request jobs restore those caches but never save +pull-request-scoped copies. This preserves fast fresh PRs without cache fan-out. + +Use workflow concurrency keyed by Git ref with `cancel-in-progress: true` so a +new commit cancels obsolete work for the same PR or `main` ref. + +### Releases and deployment + +- Keep all six release targets and durable GitHub Release assets. +- Set temporary `upload-artifact` handoffs to one-day retention. +- Restore but do not save tag-scoped Rust caches; tag caches have no reusable + downstream branch. +- Keep nightly relay snapshots at 90 days; they are operational backups and + account for only 142 MB. Correct the claim that artifact storage is free. +- Ignore deploy triggers for known files absent from the production Docker + build context: Markdown, docs, skills, hooks, examples, Scoop metadata, and + Gemini plugin metadata. Source, tests, landing, assets, manifests, Docker, + Fly config, and unknown future paths still deploy. + +## Protection rollout + +Update `require-ci.sh` to require `test`, `fmt`, `clippy`, `docs-lint`, +`linux-e2e`, and `install-smoke-windows`. Do not execute that live mutation +until the branch's new checks have run successfully; otherwise the pull request +would be blocked on contexts that do not yet exist. + +## Safety boundaries + +- Do not delete current Actions artifacts or caches in this change. +- Do not alter release targets, production Fly state, secrets, branch + protection, or live Wire services during local implementation. +- Preserve every existing Linux and Windows behavioral command. +- Every temporary `WIRE_HOME` caller continues to set `WIRE_HOME_FORCE=1`. +- No new action or runtime dependency. + +## Verification + +- A structural assertion must fail against current workflows before edits and + pass afterward, proving six PR jobs, two main warmers, one Linux release + build, stable restore-only PR caches, one-day release handoffs, deploy + exclusions, and protection contexts. +- `actionlint` must accept all workflows. +- `test-env/run.sh` must pass the canonical isolated Wire gate. +- GitNexus change detection must report workflow/docs-only scope. +- A read-only independent reviewer must return no BLOCKER or MAJOR finding. +- After push, inspect real GitHub check names and durations. Live protection + migration and artifact deletion remain separate operator-visible actions. + +## Expected effect + +For July's observed mix of 15 PR runs and 10 main pushes, job fan-out falls +from 300 jobs to about 110: six jobs per PR and two cache warmers per main push. +Seven Linux release builds per PR become one. PR caches stop multiplying by job +and merge ref. Temporary release handoffs expire after one day instead of 90. diff --git a/require-ci.sh b/require-ci.sh index 6fbd24c..57dd5e9 100755 --- a/require-ci.sh +++ b/require-ci.sh @@ -11,13 +11,8 @@ gh api -X PUT repos/SlanchaAi/wire/branches/main/protection --input - <<'JSON' {"context": "fmt"}, {"context": "clippy"}, {"context": "docs-lint"}, - {"context": "demo-command"}, - {"context": "demo-invite"}, - {"context": "demo-hotline"}, - {"context": "install-smoke"}, - {"context": "install-smoke-windows"}, - {"context": "install-script-smoke"}, - {"context": "integration-tests"} + {"context": "linux-e2e"}, + {"context": "install-smoke-windows"} ] }, "enforce_admins": true,