Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
119 changes: 119 additions & 0 deletions .github/workflows/release-cache.yml
Original file line number Diff line number Diff line change
@@ -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
55 changes: 43 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name> 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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading