diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 235e320f..437f3643 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -7,10 +7,13 @@ description: Cut a Shipyard release (vX.Y.Z) - version bump, changelog, tag, and The tag is the release. Pushing `vX.Y.Z` runs `.github/workflows/release.yml`: -1. **checks:** calls `ci.yml`: Go with `-race` and a real Chromium, shellcheck, the app's checks, tests and build, and the docs build. A tag skips the e2e suite. Nothing is built if any check fails. -2. **tmux:** static tmux for Linux boxes (Docker, on a hosted runner). It runs in parallel with checks. +1. **checks:** calls `ci.yml`: Go with `-race` and a real Chromium, shellcheck, the app's checks, tests and build, and the docs build. A tag skips the e2e suite. It runs alongside the builds; nothing is published if any check fails (the release stays a draft). +2. **tmux:** static tmux for Linux boxes (Docker, on a hosted runner). 3. **release:** builds `berth`/`berthd` archives for darwin and linux, amd64 and arm64, and makes a **draft** release. -4. **macos:** the universal app, signed, notarized and stapled. It uploads the dmg and updater archive, then `latest.json` last, then publishes the release as Latest. This takes about 30 minutes. +4. **macos** and **linux:** the universal Mac app (signed, notarized, stapled) and the Linux alpha. They upload their bundles and `latest.json` to the draft. +5. **feed:** once the apps are built and the checks pass, merges the Linux entries into `latest.json` and publishes the release as Latest. About 15 minutes from the tag when the caches are warm. + +The builds restore caches `release-cache.yml` keeps warm on `main` (a tag's run reads only `main`'s caches): the Mac's Go build cache and the Mac and Linux apps' Rust dependencies. It runs when Go or Rust dependencies change and twice a week; run it by hand (`gh workflow run release-cache.yml`) before a release if it hasn't run in a week. Only people the "Release tags" ruleset lets through can push `v*` tags. An agent usually can't push a tag; prepare everything and give the user the command. diff --git a/.github/workflows/release-cache.yml b/.github/workflows/release-cache.yml new file mode 100644 index 00000000..844e74d1 --- /dev/null +++ b/.github/workflows/release-cache.yml @@ -0,0 +1,119 @@ +name: release-cache + +# Keeps the caches a release builds from warm on main, where a release's run +# (release.yml, on a tag) finds them: a tag's run reads main's caches, not +# other tags', and CI runs on pull requests, whose caches no tag sees. +# +# mac: the Go build cache for the Mac's binaries (berth-cli, berthd, the +# Linux berthds it carries), and the Rust dependencies of the +# universal app (Apple silicon and Intel) +# linux: the Rust dependencies of the Linux app's release build +# +# Each builds what the release builds, the same way, without signing or +# bundling. It runs when the dependencies change, and twice a week: GitHub +# drops a cache nobody has read for seven days. +on: + push: + branches: [main] + paths: + - go.mod + - go.sum + - app/src-tauri/Cargo.toml + - app/src-tauri/Cargo.lock + - .github/workflows/release-cache.yml + schedule: + - cron: "17 4 * * 1,4" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-cache + cancel-in-progress: true + +jobs: + mac: + runs-on: macos-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + cache: false + # A new entry each run, so the newest code's packages are what a + # release restores (by the go-mac-release- prefix). + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: | + ~/Library/Caches/go-build + ~/go/pkg/mod + key: go-mac-release-${{ hashFiles('go.sum') }}-${{ github.run_id }} + restore-keys: | + go-mac-release-${{ hashFiles('go.sum') }}- + go-mac-release- + - name: Build the Go binaries the app carries + run: | + set -euo pipefail + # As make app-binaries does for universal-apple-darwin, and its + # daemons target; the version stamp only changes the link. + out="$RUNNER_TEMP/go" + make daemons BIN="$out" TMUX=0 + for arch in arm64 amd64; do + GOOS=darwin GOARCH=$arch go build -trimpath -o "$out/berth-cli-$arch" ./cmd/berth + GOOS=darwin GOARCH=$arch go build -trimpath -o "$out/berthd-$arch" ./cmd/berthd + done + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + with: + version: 11 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: app/pnpm-lock.yaml + - run: rustup target add aarch64-apple-darwin x86_64-apple-darwin + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: app/src-tauri + shared-key: mac-release + - run: pnpm install --frozen-lockfile + working-directory: app + # The release's build (scripts/mac-release.sh, make app-build), with + # its features, for both architectures; no bundle, no signing. + - name: Build the app's Rust + working-directory: app + run: pnpm tauri build --target universal-apple-darwin --no-bundle + + linux: + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + with: + version: 11 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: app/pnpm-lock.yaml + - name: Install the system libraries + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: app/src-tauri + shared-key: linux-release + - run: pnpm install --frozen-lockfile + working-directory: app + # The release build of the Linux app (scripts/linux-release.sh), no + # bundles. + - name: Build the app's Rust + working-directory: app + run: pnpm tauri build --no-bundle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07102e34..c6485fa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,15 @@ name: release # notarized and stapled (scripts/mac-release.sh), and adds the dmg, the # updater archive and latest.json, the feed installed apps update from. No # asset name carries a version, so releases/latest/download/ always -# finds the newest. The checks job (all of ci.yml) must pass first. The -# release job runs on omarchy, the owner's self-hosted -# runner (ci/runner/), when the repository variable SELF_HOSTED is true. +# finds the newest. The checks job (all of ci.yml) runs alongside the builds, +# and the feed job publishes nothing unless it passes: until then the release +# is a draft, which releases/latest/download/ and installed apps never see. +# The release job runs on omarchy, the owner's self-hosted runner +# (ci/runner/), when the repository variable SELF_HOSTED is true. +# +# The builds start from caches release-cache.yml keeps warm on main (a tag's +# run reads main's caches, not other tags'): the Mac's Go build cache, and +# the Rust dependencies of the Mac and Linux apps. # # The linux job builds the Linux desktop app, an alpha, for x86_64 on a # hosted Ubuntu runner (scripts/linux-release.sh): an AppImage, which updates @@ -34,8 +40,9 @@ concurrency: jobs: # All of CI (ci.yml: Go with -race and a real Chromium, shellcheck, the # app's checks, tests, build and Playwright suite, the docs build) on the - # tagged commit. Nothing is built or published unless it passes, so a - # release needs no test run on a laptop. + # tagged commit. It runs while the release is built; nothing is published + # unless it passes (the feed job), so a release needs no test run on a + # laptop. A failure leaves a draft, which nobody is offered. checks: uses: ./.github/workflows/ci.yml @@ -73,7 +80,7 @@ jobs: if-no-files-found: error release: - needs: [tmux, checks] + needs: [tmux] runs-on: ${{ (vars.SELF_HOSTED == 'true' && github.event_name == 'push' && github.actor == github.repository_owner) && fromJSON('["self-hosted","omarchy"]') || 'ubuntu-latest' }} permissions: contents: write @@ -173,6 +180,24 @@ jobs: node-version: 22 cache: pnpm cache-dependency-path: app/pnpm-lock.yaml + # main's warm caches (release-cache.yml): read here, never written, + # since a tag's caches serve no later tag. + - if: steps.published.outputs.published != 'true' + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: | + ~/Library/Caches/go-build + ~/go/pkg/mod + key: go-mac-release-${{ hashFiles('go.sum') }}- + restore-keys: | + go-mac-release-${{ hashFiles('go.sum') }}- + go-mac-release- + - if: steps.published.outputs.published != 'true' + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: app/src-tauri + shared-key: mac-release + save-if: false - name: Install tools if: steps.published.outputs.published != 'true' run: | @@ -314,6 +339,11 @@ jobs: tar -xzf "$RUNNER_TEMP/minisign.tar.gz" -C "$RUNNER_TEMP" minisign-linux/x86_64/minisign sudo install -m 0755 "$RUNNER_TEMP/minisign-linux/x86_64/minisign" /usr/local/bin/minisign minisign -v + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: app/src-tauri + shared-key: linux-release + save-if: false - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: tmux @@ -344,12 +374,13 @@ jobs: if-no-files-found: error feed: - # After the Mac and Linux jobs, whichever finished first: the Mac's - # latest.json gets the Linux entries, then the release becomes "latest". - # Runs when the Mac app is published even if the Linux build failed; then - # the feed stays the Mac's alone. - needs: [macos, linux] - if: ${{ !cancelled() && needs.macos.result == 'success' }} + # After the Mac and Linux jobs, whichever finished first, and the checks: + # the Mac's latest.json gets the Linux entries, then the release becomes + # "latest". Runs when the Mac app is built even if the Linux build + # failed; then the feed stays the Mac's alone. Never when a check failed: + # the release stays a draft. + needs: [macos, linux, checks] + if: ${{ !cancelled() && needs.macos.result == 'success' && needs.checks.result == 'success' }} runs-on: ubuntu-latest permissions: contents: write