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
76 changes: 63 additions & 13 deletions .ai/contexts/build-release.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
# Build and release workflow

`.github/workflows/build.yml` uses one event-dependent `build` matrix. PRs
include Windows, Linux x64 and Linux arm64; version-tag pushes also include
macOS. The JSON arrays in `matrix.include` must keep the non-mac entries in
sync. Each matrix job has `timeout-minutes: 60`, including macOS.
`.github/workflows/build.yml` runs on version-tag pushes, manual dispatches
and packaging-related PRs to `main`. PRs and dispatches include Windows,
Linux x64 and Linux arm64. Only a `push` whose ref starts with `refs/tags/v`
adds macOS; dispatching on a tag ref still uses the three-job matrix. The
JSON arrays in `matrix.include` must keep the non-mac entries in sync.

`publish` keeps `needs: build` and the default success gate. A failed or
cancelled macOS tag build blocks automatic publication; no partial-publish or
The PR path filter covers `package.json`, `package-lock.json`, the requested
root `postinstall.js` path, `scripts/**` and the build workflow itself. The
actual postinstall is `scripts/postinstall.js`; `scripts/**` also covers the
`scripts/after-pack.js` hook and icon generators. `build/**` covers icons, macOS
entitlements and the DMG background. The builder configuration is inline in
`package.json`; there is no separate electron-builder config. Dependencies are
covered by the lockfile. The filter is packaging only: application source
(`*.js`, `src/**`, `workers/**`, `public/**`, `harnesses/**`) and `CHANGELOG.md`
are deliberately absent, so a code PR builds no installers. A packaging break
caused by source surfaces at the tag, where strict publish blocks it (accepted
risk, maintainer decision).

Each matrix job retains `timeout-minutes: 60`. These are parallel jobs, not
dependent stages: the budget must fit installation/native rebuilds, both
viewer bundles and packaging, including macOS arm64+x64 DMG/ZIP and Windows
x64+arm64 NSIS. Retaining the existing hour avoids cutting the tag-build
budget without measured runner timings; it is not a measured duration.
`publish`, the only downstream job (`needs: build`), has a 10-minute bound.

`publish` requires a version-tag **push** and keeps `needs: build` and the
default success gate. A manual dispatch never publishes, even on a tag ref.
A failed or cancelled macOS tag build blocks automatic publication; no partial-publish or
`continue-on-error` path exists. Recovery and manifest consistency requirements
are in [releasing.md](../../docs/releasing.md).

Expand All @@ -17,13 +38,42 @@ module rebuild between architectures applies only to the first target type;
multiple targets in one multi-architecture invocation can reuse modules from
the wrong architecture (issue #18).

`test/build-workflow.test.js` parses the actual workflow using `js-yaml` and
evaluates its expression for a PR and a tag push. It checks the exact matrices,
timeout, triggers, failure propagation configuration and strict publish
dependency. It evaluates the expression as JavaScript, not with the Actions
expression engine, so it is a configuration guard only: the proof that Actions
accepts the expression is a PR run's job list (no mac job), and the tag branch
is first seen at the next tag. It does not verify runner queue behavior either;
Both workflows group PR runs per PR number (`pr-<number>`) and give every other
run its own group (`github.run_id`), and cancel older runs only for `pull_request`.
Tag pushes, main pushes and dispatches never cancel in-progress runs. Every
upload explicitly retains PR artifacts for 3 days and other artifacts for
14 days. Tag artifacts feed `publish` through `actions/download-artifact` in
the same run, so the 14-day retention does not change that dependency.

`test.yml` has no path filter. PRs run Node 20/Linux and Node 22/Linux+Windows;
main pushes run Node 22/Linux+Windows. Matrix keys remain `node-version`, then
`os`, with no job-name override, retaining contexts such as
`test (22, ubuntu-latest)`. No step depends on a Node 20 or Windows/Node 20
cell. Linux real-bwrap enforcement stays on every Linux test cell; patch
coverage stays on PR Node 22/Linux only. Lint and e2e still run for every PR
and main push; changelog stays PR-only. Test jobs have 25 minutes; lint,
changelog and e2e have 10 each.

The required-check list is documented in [releasing.md](../../docs/releasing.md).
Build is not a required check. JB owns the external settings update that removes
`test (20, windows-2022)` from both ruleset 17240725 (`main-protection`) and the
classic branch protection on `main` (`required_status_checks.contexts`); both
must be updated together whenever the test matrix changes. Changing these
workflows does not update those settings.
This CI-only change takes the `no-changelog` PR label.

To obtain an installer for any branch, dispatch the build and download that
run's artifacts; see [testing-a-pr.md](../../docs/testing-a-pr.md#obtaining-a-pr-installer).
`task test-pr` runs sources and has no artifact dependency.

`test/build-workflow.test.js` parses both actual workflows using `js-yaml`
and evaluates their JSON matrix literals for PRs, pushes and dispatches,
including dispatch on a tag. It checks matrices, timeouts, triggers, retention,
concurrency, unchanged Test gates, failure propagation and strict publish
dependency. It evaluates expressions as JavaScript, not with the Actions
expression engine, so it is a configuration guard only. An Actions run's job
list must verify expression acceptance and context names; the full release
remains a tag-run check. It does not verify runner queue behavior either;
`timeout-minutes` bounds a hung build on a runner, not time spent queued.

Trade-off: the macOS-only "Test postinstall helpers" step now runs on tags
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@ name: Build & Release
on:
push:
tags: ['v*']
workflow_dispatch:
pull_request:
branches: [main]
paths:
- package.json
- package-lock.json
- postinstall.js
- scripts/**
- .github/workflows/build.yml
- build/**

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
Expand All @@ -17,7 +29,7 @@ jobs:
# see .ai/contexts/build-release.md
matrix:
include: >-
${{ fromJSON(github.event_name != 'pull_request' &&
${{ fromJSON(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') &&
'[{"os":"macos-14","platform":"mac"},{"os":"windows-2022","platform":"win"},{"os":"ubuntu-latest","platform":"linux","arch":"x64"},{"os":"ubuntu-24.04-arm","platform":"linux","arch":"arm64"}]' ||
'[{"os":"windows-2022","platform":"win"},{"os":"ubuntu-latest","platform":"linux","arch":"x64"},{"os":"ubuntu-24.04-arm","platform":"linux","arch":"arm64"}]') }}

Expand Down Expand Up @@ -84,11 +96,13 @@ jobs:
dist/*.pacman
dist/latest*.yml
if-no-files-found: ignore
retention-days: ${{ github.event_name == 'pull_request' && 3 || 14 }}

publish:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
Expand Down
17 changes: 13 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ on:
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
lint:
# Separate job, not an npm pre-hook: `npm run coverage` (below) has no
# `precoverage`, so `pretest` never fired for it and lint silently never
# ran in CI. A dedicated job can't be skipped by a future script rename —
# it shows up as its own entry in the checks list.
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
Expand All @@ -34,6 +39,7 @@ jobs:
# see docs/changelog.md ("The CI check")
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
Expand All @@ -49,7 +55,7 @@ jobs:
e2e:
# see docs/e2e.md
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -90,16 +96,19 @@ jobs:
path: |
${{ runner.temp }}/switchboard-e2e-results/
e2e/playwright-report/
retention-days: 14
retention-days: ${{ github.event_name == 'pull_request' && 3 || 14 }}

test:
runs-on: ${{ matrix.os }}
timeout-minutes: 25

strategy:
fail-fast: false
matrix:
node-version: [20, 22]
os: [ubuntu-latest, windows-2022]
include: >-
${{ fromJSON(github.event_name == 'pull_request' &&
'[{"node-version":20,"os":"ubuntu-latest"},{"node-version":22,"os":"ubuntu-latest"},{"node-version":22,"os":"windows-2022"}]' ||
'[{"node-version":22,"os":"ubuntu-latest"},{"node-version":22,"os":"windows-2022"}]') }}

steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 2 additions & 1 deletion docs/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Electron's ABI. `postinstall` swallows a failed rebuild, and without that check
a failed rebuild would show up as an app that does not start. When a journey
fails, the job uploads `switchboard-e2e-results/` under the runner's temporary
directory (a `trace.zip` and a `failure.png` for each failed journey) and the
HTML report as the `e2e-results` artifact.
HTML report as the `e2e-results` artifact, retained for 3 days on PRs and
14 days on main pushes. The job has a 10-minute timeout.
Open a trace with `npx playwright show-trace trace.zip`. It has the DOM and a
screenshot for each step.

Expand Down
26 changes: 20 additions & 6 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ request first, because `main` is protected.
`test/changelog.test.js` fails when `CHANGELOG.md` has no section for the
version in `package.json`, so a bump without its section does not pass CI.

The `main-protection` ruleset requires the checks `lint`,
`test (20, ubuntu-latest)`, `test (20, windows-2022)`, `test (22, ubuntu-latest)`,
`test (22, windows-2022)`, `changelog` and `e2e`, and no approving review, so
`gh pr merge --auto --squash` can be armed as soon as the checks run.
The required PR checks for this workflow are `lint`,
`test (20, ubuntu-latest)`, `test (22, ubuntu-latest)`,
`test (22, windows-2022)`, `changelog` and `e2e`. Build is not required.
JB must remove the obsolete `test (20, windows-2022)` context from both
places that require it: ruleset 17240725 (`main-protection`) and the classic
branch protection on `main` (`required_status_checks.contexts`). Both must
be updated together whenever the test matrix changes; workflow changes do
not change repository settings. The documented ruleset requires
no approving review, so `gh pr merge --auto --squash` can be armed once the
required checks run. CI-only PRs use the `no-changelog` label.

2. **Run the app before tagging.** The tag is what publishes, and nothing
between the merge and the tag looks at the assembled whole; the test suite
Expand All @@ -45,12 +51,20 @@ request first, because `main` is protected.

4. **Wait for the build.** The tag starts `.github/workflows/build.yml`: macOS
(macos-14), Windows (windows-2022), and Linux x64 and arm64 on separate
runners. Pull requests build only Windows and both Linux architectures;
macOS remains part of every tag build. Each build job has a 60-minute timeout.
runners. Packaging-related pull requests and manual dispatches build only
Windows and both Linux architectures; macOS remains part of every tag push
build. A dispatch never publishes, even on a tag ref. For a PR installer,
see [Testing a PR live](testing-a-pr.md#obtaining-a-pr-installer).
Each build job retains its 60-minute timeout for native rebuilds and
multi-architecture packaging; publish has a 10-minute timeout.
The publish job keeps `needs: build`: all matrix entries must succeed before
it runs. If macOS fails, is cancelled or times out, nothing is published
automatically, even when Windows and Linux succeed.

Tag artifacts are downloaded by publish in the same run and retained for
14 days (PR artifacts: 3 days). Only older runs of the same PR are cancelled
by its newer runs; tag and main pushes never cancel in-progress runs.

To recover, re-run the failed macOS job in the tag's Actions run (or re-run
failed jobs) and let the dependent publish job run once every build succeeds.
Alternatively, create the release draft by hand using only artifacts from
Expand Down
24 changes: 23 additions & 1 deletion docs/testing-a-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ The default mode keeps your real HOME and transcript history. `ISOLATED` takes
`1` for fixture mode or `0` for the default mode; unset or empty also selects
the default. Other values produce a usage error before any git command.

## Obtaining a PR installer

PR installers are built only when a PR to `main` changes packaging files:
`package.json`, `package-lock.json`, `postinstall.js`, `scripts/**`, `build/**`
or `.github/workflows/build.yml`. Shipped source such as `main.js` or `public/`
does not trigger a build. Other PRs have no automatic build artifacts. Build is
not a required check. To build a PR branch on demand:

```bash
gh workflow run build.yml --repo devsuitup/switchboard --ref <branch>
gh run list --repo devsuitup/switchboard --workflow build.yml --branch <branch> --event workflow_dispatch --limit 5
gh run watch <run-id> --repo devsuitup/switchboard --exit-status
gh run download <run-id> --repo devsuitup/switchboard --dir .work-files/pr-installer
```

Select the dispatched run for the intended commit from the list. Dispatches
build Windows, Linux x64 and Linux arm64, and never publish a release, even
when dispatched on a tag ref. macOS installers are built on version-tag pushes.
PR artifacts expire after 3 days; dispatch and tag artifacts after 14 days.
Dispatch again if an artifact has expired. Installing a downloaded installer
can replace the installed app; the source task below runs an isolated instance.

## From source, not a build

Electron receives the absolute worktree path and `--no-sandbox` to run the
Expand All @@ -29,7 +51,7 @@ the launch with an error. It rebuilds even when a bundle already exists,
so the file viewers use the PR's current inputs. A failed bundle build stops
the launch and reports the worktree path and build error.

The task does not package the app or rebuild native modules, which other
The task does not download CI artifacts, package the app or rebuild native modules, which other
instances may have loaded (see
[Development](development.md#building-and-replacing-while-an-installed-copy-runs)).

Expand Down
Loading
Loading