From 8e45e9e611e660cae5b620094972ef4173d656b4 Mon Sep 17 00:00:00 2001 From: geekbrother Date: Thu, 11 Jun 2026 18:46:27 +0200 Subject: [PATCH 1/3] ci: migrate app-check Rust caching to Swatinem/rust-cache Replace the per-job hand-rolled actions/cache (restore + "clean target if Cargo.lock changed" + save), keyed only on Cargo.lock, with Swatinem/rust-cache in clippy / tests / tests-suites / unused-deps. - Each job uses a Swatinem shared-key (clippy, tests, tests-suites, unused-deps) and maps the existing knobs: prefix-key <- rust-cache-prefix, save-if <- rust-cache-save. Swatinem keys on rustc + Cargo.lock + workspace and prunes stale workspace artifacts before saving, fixing the immutable-cache staleness of a Cargo.lock-only key. - Formatting job no longer caches anything: `cargo fmt -- --check` does not compile, so restoring/saving ~/.cargo + target/ was pure waste. - sccache steps removed: no persistent backend was ever configured (the object store was not cached), so it gave no cross-run benefit and disabled incremental compilation, fighting the target/ cache. The `use-sccache` input is kept (deprecated, no-op) so existing callers and ci.yml don't break. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci-check-app.yml | 181 +++++------------------------ 1 file changed, 28 insertions(+), 153 deletions(-) diff --git a/.github/workflows/ci-check-app.yml b/.github/workflows/ci-check-app.yml index e247418..44a3553 100644 --- a/.github/workflows/ci-check-app.yml +++ b/.github/workflows/ci-check-app.yml @@ -36,7 +36,12 @@ on: type: boolean default: true use-sccache: - description: 'Run sccache-cache before running tests' + # DEPRECATED / no-op: sccache had no persistent backend configured here + # (its object store was never cached), so it provided no cross-run + # benefit and disabled incremental compilation, which fights the + # Swatinem/rust-cache target/ cache. The input is kept so existing + # callers (and ci.yml) don't break; it no longer wires anything. + description: '(deprecated, no-op) Run sccache-cache before running tests' type: boolean default: true use-postgresql: @@ -83,22 +88,12 @@ jobs: toolchain: ${{ inputs.rust-toolchain }} components: clippy - - name: Restore Cargo cache - id: cache-restore - uses: actions/cache/restore@v4 + - name: Rust cache + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-clippy- - - - name: Clean target if Cargo.lock changed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: rm -rf target/ + prefix-key: ${{ inputs.rust-cache-prefix }} + shared-key: clippy + save-if: ${{ inputs.rust-cache-save }} - name: Install Protoc if: ${{ inputs.install-protoc == true }} @@ -106,24 +101,9 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run sccache-cache - if: ${{ inputs.use-sccache == true }} - uses: mozilla-actions/sccache-action@v0.0.4 - - name: Clippy run: cargo clippy --workspace --all-features --all-targets -- -D warnings - - name: Save Cargo cache - if: ${{ inputs.rust-cache-save }} - uses: actions/cache/save@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} - formatting: name: Formatting runs-on: ${{ inputs.run-label }} @@ -139,41 +119,11 @@ jobs: toolchain: ${{ inputs.rust-toolchain-formatting }} components: rustfmt - - name: Restore Cargo cache - id: cache-restore - uses: actions/cache/restore@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-formatting-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-formatting- - - - name: Clean target if Cargo.lock changed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: rm -rf target/ - - - name: Run sccache-cache - if: ${{ inputs.use-sccache == true }} - uses: mozilla-actions/sccache-action@v0.0.4 - + # `cargo fmt -- --check` does not compile anything, so there is no target/ + # or dependency cache to restore here. - name: Check Formatting run: cargo fmt -- --check - - name: Save Cargo cache - if: ${{ inputs.rust-cache-save }} - uses: actions/cache/save@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-formatting-${{ hashFiles('**/Cargo.lock') }} - tests: name: Unit Tests runs-on: ${{ inputs.run-label }} @@ -189,22 +139,12 @@ jobs: with: toolchain: ${{ inputs.rust-toolchain }} - - name: Restore Cargo cache - id: cache-restore - uses: actions/cache/restore@v4 + - name: Rust cache + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-tests-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-tests- - - - name: Clean target if Cargo.lock changed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: rm -rf target/ + prefix-key: ${{ inputs.rust-cache-prefix }} + shared-key: tests + save-if: ${{ inputs.rust-cache-save }} - name: Install Protoc if: ${{ inputs.install-protoc == true }} @@ -212,24 +152,9 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run sccache-cache - if: ${{ inputs.use-sccache == true }} - uses: mozilla-actions/sccache-action@v0.0.4 - - name: Unit Tests run: cargo test ${{ inputs.test-args }} - - name: Save Cargo cache - if: ${{ inputs.rust-cache-save }} - uses: actions/cache/save@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-tests-${{ hashFiles('**/Cargo.lock') }} - tests-suites: name: Tests Suite "${{ matrix.element }}" if: (inputs.test-suites != '') && (inputs.test-suites != '[]') @@ -258,22 +183,12 @@ jobs: with: toolchain: ${{ inputs.rust-toolchain }} - - name: Restore Cargo cache - id: cache-restore - uses: actions/cache/restore@v4 + - name: Rust cache + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-tests-suites-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-tests-suites- - - - name: Clean target if Cargo.lock changed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: rm -rf target/ + prefix-key: ${{ inputs.rust-cache-prefix }} + shared-key: tests-suites + save-if: ${{ inputs.rust-cache-save }} - name: Install Protoc if: ${{ inputs.install-protoc == true }} @@ -281,24 +196,9 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Run sccache-cache - if: ${{ inputs.use-sccache == true }} - uses: mozilla-actions/sccache-action@v0.0.4 - - name: Test Suite run: cargo test --test ${{ matrix.element }} - - name: Save Cargo cache - if: ${{ inputs.rust-cache-save }} - uses: actions/cache/save@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-tests-suites-${{ hashFiles('**/Cargo.lock') }} - unused-dependencies: name: Unused Dependencies runs-on: ${{ inputs.run-label }} @@ -315,26 +215,12 @@ jobs: with: toolchain: ${{ inputs.rust-toolchain-udeps }} - - name: Restore Cargo cache - id: cache-restore - uses: actions/cache/restore@v4 + - name: Rust cache + uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-unused-deps-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-unused-deps- - - - name: Clean target if Cargo.lock changed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: rm -rf target/ - - - name: Run sccache-cache - if: ${{ inputs.use-sccache == true }} - uses: mozilla-actions/sccache-action@v0.0.4 + prefix-key: ${{ inputs.rust-cache-prefix }} + shared-key: unused-deps + save-if: ${{ inputs.rust-cache-save }} - name: Install cargo-udeps run: cargo install cargo-udeps@0.1.43 --locked @@ -342,17 +228,6 @@ jobs: - name: Check Dependencies run: cargo udeps --all-targets - - name: Save Cargo cache - if: ${{ inputs.rust-cache-save }} - uses: actions/cache/save@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/ - ~/.cargo/git/ - target/ - key: ${{ inputs.rust-cache-prefix }}-${{ runner.os }}-cargo-unused-deps-${{ hashFiles('**/Cargo.lock') }} - license: name: Licenses runs-on: ${{ inputs.run-label }} From 5721318b78c62caa69683b19f5d789a6156d87ee Mon Sep 17 00:00:00 2001 From: geekbrother Date: Fri, 12 Jun 2026 15:53:42 +0200 Subject: [PATCH 2/3] ci: add opt-in mold linker for the test jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mold is a linker, so it only helps jobs that link a binary — tests and tests-suites, not clippy (check-only) or formatting (no compile). Add a `use-mold` input (default false) and, when enabled, install mold+clang and set RUSTFLAGS before the Swatinem cache step (so the linker flags are part of the cache key) in those two jobs only. Opt-in by default because this is a shared workflow: mold cannot read ThinLTO bitcode objects without the LLVM plugin, so `cargo test --release` / LTO test builds would fail at link time. Plumb the matching `rust-use-mold` input through ci.yml. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci-check-app.yml | 25 +++++++++++++++++++++++++ .github/workflows/ci.yml | 5 +++++ 2 files changed, 30 insertions(+) diff --git a/.github/workflows/ci-check-app.yml b/.github/workflows/ci-check-app.yml index 44a3553..4356600 100644 --- a/.github/workflows/ci-check-app.yml +++ b/.github/workflows/ci-check-app.yml @@ -44,6 +44,15 @@ on: description: '(deprecated, no-op) Run sccache-cache before running tests' type: boolean default: true + use-mold: + # Opt-in (default false): mold is a linker, so it only helps jobs that + # link a binary — i.e. tests / tests-suites, not clippy (check-only) or + # fmt (no compile). Keep this OFF for `cargo test --release` / LTO test + # builds: mold can't read ThinLTO bitcode objects without the LLVM + # plugin and the final link fails. + description: 'Use the mold linker (clang driver) for the test jobs. Only for non-LTO debug test builds.' + type: boolean + default: false use-postgresql: description: 'Run postgresql before running tests' type: boolean @@ -139,6 +148,14 @@ jobs: with: toolchain: ${{ inputs.rust-toolchain }} + # Before the cache step so RUSTFLAGS is part of the Swatinem cache key. + - name: Set up mold linker + if: ${{ inputs.use-mold }} + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends mold clang + echo "RUSTFLAGS=${RUSTFLAGS:+$RUSTFLAGS }-C linker=clang -C link-arg=-fuse-ld=mold" >> "$GITHUB_ENV" + - name: Rust cache uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 with: @@ -183,6 +200,14 @@ jobs: with: toolchain: ${{ inputs.rust-toolchain }} + # Before the cache step so RUSTFLAGS is part of the Swatinem cache key. + - name: Set up mold linker + if: ${{ inputs.use-mold }} + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends mold clang + echo "RUSTFLAGS=${RUSTFLAGS:+$RUSTFLAGS }-C linker=clang -C link-arg=-fuse-ld=mold" >> "$GITHUB_ENV" + - name: Rust cache uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 680d18f..20a7090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,10 @@ on: description: 'Run `sccache-cache` before running rust tests' type: boolean default: true + rust-use-mold: + description: 'Use the mold linker for the rust test jobs (opt-in; non-LTO debug builds only)' + type: boolean + default: false rust-use-postgresql: description: 'Run postgresql before running rust tests' type: boolean @@ -133,6 +137,7 @@ jobs: rust-backtrace: ${{ inputs.rust-backtrace }} install-protoc: ${{ inputs.rust-install-protoc }} use-sccache: ${{ inputs.rust-use-sccache }} + use-mold: ${{ inputs.rust-use-mold }} use-postgresql: ${{ inputs.rust-use-postgresql }} test-env-vars: ${{ inputs.rust-test-env-vars }} run-label: ${{ inputs.run-label }} From 88181667353fe2299de3dfb128be53b39ae57aaf Mon Sep 17 00:00:00 2001 From: geekbrother Date: Fri, 12 Jun 2026 19:25:08 +0200 Subject: [PATCH 3/3] ci: build the release image once, push per-env (no double compile) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release-app.yml previously published via a parallel matrix over publish-envs, with each env (Staging, Prod) running build-publish.yml — a full Docker compile. Because the legs ran in parallel they couldn't share the gha layer cache, so the identical image was compiled twice (~2x the slowest build). Restructure into build-once / push-per-env (same pattern as pay-core's canary image): - `build`: compiles the image once on the configurable run-label runner and exports it to a tarball artifact (gha layer cache retained). - `publish` (matrix per env): downloads the tarball, docker load, then tags + pushes to that env's ECR (its own role) and GHCR. No compile — runs on ubuntu-latest. Env config is injected at runtime in ECS, so the image is identical across envs. build-publish.yml is left in place for any direct callers. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-app.yml | 118 +++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-app.yml b/.github/workflows/release-app.yml index 3253f09..e358931 100644 --- a/.github/workflows/release-app.yml +++ b/.github/workflows/release-app.yml @@ -17,7 +17,7 @@ on: type: string default: ${{ vars.AWS_REGION }} run-label: - description: 'The run label to use for the actions' + description: 'The run label to use for the build job (the heavy compile). Push jobs always use ubuntu-latest.' type: string default: 'ubuntu-latest' secrets: @@ -44,7 +44,7 @@ jobs: submodules: recursive token: ${{ secrets.PRIVATE_SUBMODULE_ACCESS_TOKEN || github.token }} fetch-depth: 0 - # Don't persist the credentials because we are using the token to fetch the + # Don't persist the credentials because we are using the token to fetch the # private submodule and then we are using different token to create a release persist-credentials: false @@ -63,18 +63,114 @@ jobs: steps: - run: echo "Version = ${{ needs.update_version.outputs.version }}" + # Build the image ONCE (one compile) and export it as a tarball artifact. The + # per-env publish jobs below load this tarball and push to each environment's + # ECR, so staging + prod no longer each recompile the identical image. Same + # build-once / push-per-env pattern as pay-core's canary image. + build: + name: Build ${{ needs.update_version.outputs.version }} + needs: [ update_version ] + runs-on: ${{ inputs.run-label }} + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.update_version.outputs.version }} + token: ${{ secrets.PRIVATE_SUBMODULE_ACCESS_TOKEN || github.token }} + submodules: recursive + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image to tarball + uses: docker/build-push-action@v5 + with: + context: . + tags: release-image:${{ needs.update_version.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=docker,dest=/tmp/image.tar + + - name: Upload image artifact + uses: actions/upload-artifact@v7 + with: + name: release-image + path: /tmp/image.tar + retention-days: 1 + publish: name: Publish ${{ needs.update_version.outputs.version }} ❱❱ ${{ matrix.env.name }} - needs: [ update_version ] + needs: [ update_version, build ] strategy: fail-fast: false matrix: env: ${{ fromJson(inputs.publish-envs) }} - secrets: inherit - uses: ./.github/workflows/build-publish.yml - with: - version: ${{ needs.update_version.outputs.version }} - image-name: ${{ inputs.image-name }} - aws-region: ${{ inputs.aws-region }} - aws-role-arn: ${{ matrix.env.role }} - run-label: ${{ inputs.run-label }} + # Push-only: no compile here, so a small runner is enough regardless of the + # build runner. + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + packages: write + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ matrix.env.role }} + aws-region: ${{ inputs.aws-region }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + mask-password: 'true' + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + logout: false + + - name: Download image artifact + uses: actions/download-artifact@v8 + with: + name: release-image + path: /tmp + + - name: Load image + run: docker load -i /tmp/image.tar + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + ${{ steps.login-ecr.outputs.registry }}/${{ inputs.image-name }} + walletconnect/${{ inputs.image-name }},enable=false + flavor: | + latest=auto + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=${{ needs.update_version.outputs.version }} + # Tag immutability prevents usage of `latest` + # type=raw,value=latest,enable={{is_default_branch}} + + - name: Tag and push image + env: + SRC: release-image:${{ needs.update_version.outputs.version }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + set -euo pipefail + printf '%s\n' "$TAGS" | while IFS= read -r tag; do + [ -z "$tag" ] && continue + echo "Pushing $tag" + docker tag "$SRC" "$tag" + docker push "$tag" + done