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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,36 @@ jobs:
- run: npm run lint

- run: npm test

# A CORE CHANGE NEEDS A NEW CORE VERSION (#23). core-release.yml tags
# core-v<version> on merge and never moves a tag, so a PR that changes core/
# under a version that is already released would fail AFTER the merge, on
# main. It fails here instead, where it can still be fixed.
core-version:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
set -euo pipefail
base="origin/${{ github.base_ref }}"
if git diff --quiet "$base"...HEAD -- core/; then
echo "core/ is untouched."
exit 0
fi
version=$(node -p "require('./core/package.json').version")
tag="core-v$version"
git fetch -q --tags origin
if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
echo "core/ changed and $tag is a new version. Good."
exit 0
fi
git subtree split --prefix=core -b core-check >/dev/null
if [ "$(git rev-parse "$tag^{tree}")" = "$(git rev-parse 'core-check^{tree}')" ]; then
echo "core/ matches the released $tag."
exit 0
fi
echo "::error::core/ changed, but core/package.json still says $version and $tag is already released with different contents. Bump the version in core/package.json."
exit 1
184 changes: 184 additions & 0 deletions .github/workflows/core-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# THE CORE RELEASES ITSELF, AND TELLS PRISM (#23).
#
# core/ is the terminal of TWO apps. Prism builds against a pinned tag of the
# `core-dist` branch, so a core change reaches Prism only when (1) core-dist is
# re-split, (2) a core-v<version> tag is pushed, and (3) Prism's pin is bumped.
# All three were done by hand, which is how an app falls silently behind.
# Owner, 2026-09-19: "that compiled copy needs to be auto bumped when a new
# Prism Terminal release or merge to main happens."
#
# So, on every push to main that touches core/:
# 1. split core/ into core-dist and tag it core-v<core/package.json version>;
# 2. open a PR in Maxaubert/Prism that bumps the pin to that tag.
#
# THE BUMP PROVES ITSELF (#23, and Prism #164). Prism's terminal gate used to
# run only on the owner's machine, so a bump could not merge without a person.
# It now runs on a GitHub runner (`terminal-gate.yml` in Prism; MEASURED green
# 3 runs of 3, real dictation included), beside Prism's typecheck, lint and unit
# tests. So the bump PR carries its own version bump, this job WAITS for every
# check on it, and then:
# - repo variable PRISM_AUTO_MERGE = 'true': it squash-merges the PR, Prism's
# release.yml publishes the new version, and Prism's users see the update
# chip. No person in the loop. This is a STANDING EXCEPTION to the owner's
# "never merge without my word" rule, and only the owner switches it on.
# - anything else (the default): the PR stays open, green, for a person to merge.
# A red check never merges, in either mode: the PR stays open and says why.
#
# Step 2 needs a token that can write to the Prism repo, since this repo's own
# GITHUB_TOKEN cannot: a fine-grained token on Maxaubert/Prism with Contents and
# Pull requests read/write, stored here as the secret PRISM_BUMP_TOKEN. Without
# it step 2 says so and is skipped; step 1 still happens.
name: core-release

on:
push:
branches: [main]
paths: ['core/**']
workflow_dispatch:

permissions:
contents: write

concurrency:
group: core-release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
fresh: ${{ steps.tag.outputs.fresh }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Split core/ and tag it
id: tag
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
version=$(node -p "require('./core/package.json').version")
tag="core-v$version"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
git subtree split --prefix=core -b core-dist-new
tree=$(git rev-parse 'core-dist-new^{tree}')
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
if [ "$(git rev-parse "$tag^{tree}")" = "$tree" ]; then
echo "::notice::$tag already holds exactly this core. Nothing to release."
echo "fresh=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# A tag is a promise to everyone who pinned it. It is never moved.
echo "::error::core/ changed but core/package.json still says $version, and $tag already exists with different contents. Bump the version in core/package.json (ci.yml checks this on the PR)."
exit 1
fi
git push -f origin core-dist-new:core-dist
git tag "$tag" core-dist-new
git push origin "$tag"
echo "fresh=true" >> "$GITHUB_OUTPUT"
echo "::notice::Released $tag"

bump-prism:
needs: release
if: needs.release.outputs.fresh == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
BUMP_TOKEN: ${{ secrets.PRISM_BUMP_TOKEN }}
AUTO_MERGE: ${{ vars.PRISM_AUTO_MERGE }}
TAG: ${{ needs.release.outputs.tag }}
steps:
- name: No token, no bump
if: env.BUMP_TOKEN == ''
run: |
echo "::warning::$TAG is released, but the secret PRISM_BUMP_TOKEN is not set, so no PR was opened in Maxaubert/Prism. Bump Prism's pin by hand, or add the secret (see the top of core-release.yml) and re-run this workflow."

- uses: actions/checkout@v4
if: env.BUMP_TOKEN != ''
with:
repository: Maxaubert/Prism
token: ${{ secrets.PRISM_BUMP_TOKEN }}

- uses: actions/setup-node@v4
if: env.BUMP_TOKEN != ''
with:
node-version: 22

- name: Bump the pin and the version, open the PR
id: pr
if: env.BUMP_TOKEN != ''
env:
GH_TOKEN: ${{ secrets.PRISM_BUMP_TOKEN }}
run: |
set -euo pipefail
branch="chore/$TAG"
if [ -n "$(git ls-remote --heads origin "$branch")" ]; then
echo "::notice::$branch already exists in Prism. Leaving it alone."
exit 0
fi
current=$(node -p "require('./package.json').devDependencies['prism-term-core']")
want="github:Maxaubert/PrismTerminal#$TAG"
if [ "$current" = "$want" ]; then
echo "::notice::Prism already pins $TAG."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "$branch"
npm pkg set "devDependencies.prism-term-core=$want"
# A NEW Prism version, or release.yml refuses to publish and nobody's
# update chip fires. A core FEATURE (its major.minor moved) is a minor
# bump of Prism, anything else a patch: the owner's own rule.
kind=$(node -e "
const v = (s) => (String(s).match(/core-v(\d+)\.(\d+)\./) || []).slice(1, 3).join('.')
process.stdout.write(v(process.argv[1]) && v(process.argv[1]) !== v(process.argv[2]) ? 'minor' : 'patch')
" "$current" "$TAG")
npm version "$kind" --no-git-tag-version >/dev/null
version=$(node -p "require('./package.json').version")
# The lockfile only: nothing is installed or run on this runner.
npm install --package-lock-only --ignore-scripts --no-audit --no-fund
git add package.json package-lock.json
git commit -m "build(terminal): pin prism-term-core to $TAG, $version"
git push origin "$branch"
url=$(gh pr create --repo Maxaubert/Prism --base main --head "$branch" \
--title "Terminal core: prism-term-core $TAG (Prism $version)" \
--body "Opened automatically by PrismTerminal's \`core-release\` workflow: the shared terminal core released **$TAG**, and Prism pinned \`$current\`. This bumps the pin and makes Prism **$version** ($kind).

What changed in the core: https://github.com/Maxaubert/PrismTerminal/commits/main/core

The checks on this PR are the proof: typecheck, lint, the unit suite, and **terminal-gate**, which builds the app on a Windows runner and drives every scenario the terminal can break, real dictation included.

If the repo variable \`PRISM_AUTO_MERGE\` in PrismTerminal is \`true\`, this merges itself once every check is green and Prism releases $version. Otherwise it waits here for a person. A red check never merges.")
echo "url=$url" >> "$GITHUB_OUTPUT"
echo "::notice::Opened $url"

- name: Wait for Prism's checks, then merge if the owner switched that on
if: env.BUMP_TOKEN != '' && steps.pr.outputs.url != ''
env:
GH_TOKEN: ${{ secrets.PRISM_BUMP_TOKEN }}
URL: ${{ steps.pr.outputs.url }}
run: |
set -uo pipefail
# Checks take a moment to be registered; "no checks" is not "green".
for i in $(seq 1 20); do
n=$(gh pr checks "$URL" --json name --jq 'length' 2>/dev/null || echo 0)
[ "$n" -ge 3 ] && break
sleep 15
done
if [ "${n:-0}" -lt 3 ]; then
echo "::error::Prism reported fewer than three checks on $URL (expected ci's two jobs and terminal-gate). Not merging."
exit 1
fi
if ! gh pr checks "$URL" --watch --interval 30 --fail-fast; then
echo "::error::A check failed on $URL. The bump stays open; it is NOT merged."
exit 1
fi
if [ "${AUTO_MERGE:-}" = "true" ]; then
gh pr merge "$URL" --squash --delete-branch
echo "::notice::Merged $URL. Prism's release workflow publishes it now."
else
echo "::notice::$URL is green and waiting for a person (PRISM_AUTO_MERGE is not 'true')."
fi
94 changes: 94 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# EVERY MERGE TO MAIN IS A RELEASE, when it carries a new version (#23).
#
# The app already checks GitHub Releases for a newer `v<version>` and shows an
# update chip in its title bar (src/main/update.ts). Until now nothing PUBLISHED
# a release: the chip had never had anything to find. Owner, 2026-09-19: "users
# of the app can get an update available banner in their app title bar."
#
# Same gates as ci.yml, then the installer, then `v<version>` as the latest
# release. A push whose version is ALREADY released publishes nothing and says
# so, without failing: a docs-only or CI-only merge is not an error. Bump
# package.json in the PR when a release is meant (patch for fixes, minor for
# features).
#
# The app's tags are `v*`. The shared terminal core's are `core-v*`, plain git
# tags with no GitHub Release behind them (core-release.yml), so "latest
# release" is always an app release.
#
# Unsigned, like Prism, until a certificate is configured.
name: release

on:
push:
branches: [main]

concurrency:
group: release-main
cancel-in-progress: false

jobs:
release:
runs-on: windows-latest
timeout-minutes: 40
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Is this version already released?
id: ver
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$v = (Get-Content package.json -Raw | ConvertFrom-Json).version
"version=$v" >> $env:GITHUB_OUTPUT
gh release view "v$v" *> $null
if ($LASTEXITCODE -eq 0) {
"fresh=false" >> $env:GITHUB_OUTPUT
Write-Host "::notice::v$v is already released. Nothing to publish (bump package.json in a PR to release)."
} else {
"fresh=true" >> $env:GITHUB_OUTPUT
}
$global:LASTEXITCODE = 0

- uses: actions/setup-node@v4
if: steps.ver.outputs.fresh == 'true'
with:
node-version: 22
cache: npm

- run: npm ci
if: steps.ver.outputs.fresh == 'true'

- run: npm run typecheck
if: steps.ver.outputs.fresh == 'true'

- run: npm run lint
if: steps.ver.outputs.fresh == 'true'

- run: npm test
if: steps.ver.outputs.fresh == 'true'

# The speech engine for dictation, pinned by SHA-256. `--if-present`: the
# script arrives with the dictation PR, and this file may land before it.
- run: npm run fetch:bin --if-present
if: steps.ver.outputs.fresh == 'true'

- run: npx electron-vite build
if: steps.ver.outputs.fresh == 'true'

- run: npx electron-builder --win --publish never
if: steps.ver.outputs.fresh == 'true'

- name: Publish
if: steps.ver.outputs.fresh == 'true'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$v = "${{ steps.ver.outputs.version }}"
$exe = "dist/PrismTerminal-Setup-x64-$v.exe"
if (-not (Test-Path $exe)) { throw "installer not found: $exe" }
gh release create "v$v" $exe --title "Prism Terminal $v" --generate-notes --latest
if ($LASTEXITCODE -ne 0) { throw "Release publication failed" }
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ dist
*.log
.e2e-profile
.e2e-shots/
vendor/
.e2e-cache/
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,37 @@ so an update never silently changes what an existing user sees; the bridge to ma
a ruined page, because they asserted that rows EXIST. The `options` scenario now MEASURES the
layout (card width, wall rows, row padding) and writes `.e2e-shots/settings-*.png`. After any change
that moves UI between `src/` and `core/`, LOOK at those screenshots before calling it done.
- **MERGING TO MAIN SHIPS, IN BOTH APPS** (#23; owner, 2026-09-19: "that compiled copy needs to be auto
bumped when a new Prism Terminal release or merge to main happens", and "users of the app can get
an update available banner"). Three workflows:
- `release.yml`: a push to main with a NEW `package.json` version builds the installer and publishes
`v<version>` as the latest release, which is what the in-app update chip looks for. A version
already released publishes nothing and does not fail. So: bump the version in the PR when a release
is meant (patch for fixes, minor for features).
- `core-release.yml`: a push to main that touches `core/` re-splits `core-dist`, tags
`core-v<core/package.json version>`, and opens a PR in Maxaubert/Prism that bumps the pin AND
Prism's version (minor when the core's major.minor moved, else patch). It then WAITS for that PR's
checks, which now include Prism's terminal gate on a runner (`terminal-gate.yml` there; MEASURED
green 3 of 3, real dictation included). Repo variable `PRISM_AUTO_MERGE = 'true'`: it merges the
PR and Prism releases itself. Anything else (the default): the green PR waits for a person. A red
check never merges. Auto-merge is a STANDING EXCEPTION to "never merge without the owner's word";
only the owner switches it on, and it covers these bot-made bump PRs and nothing else.
- `ci.yml`'s `core-version` job: **a PR that changes `core/` MUST bump `core/package.json`'s
version**, or it fails on the PR (a released tag is never moved).
**AUTO-MERGE IS ON** (owner, 2026-09-19: "we can say that they automerge"; `PRISM_AUTO_MERGE=true`).
**AND THE RATCHET THAT MAKES IT SAFE** (owner, the same message: "if it ever, and it probably will
at some point, create a bug in only one app, we'll make a test that it needs to pass, so the
automation gets more and more secure over time"). So: a bug that reaches EITHER app through a core
change is never just fixed. FIRST it becomes a scenario that fails on the broken build, in the suite
that guards the app it broke: Prism's `terminal-gate` (`tools/e2e/run.mjs` there, listed in
`e2e:terminal`, and RUNNER-SAFE so it runs in CI) or this repo's e2e. THEN the fix. A bump that
passed the gate and still broke something is a hole in the gate, and the hole is the first thing
to close. If a bump ever has to be undone: revert the bump PR in Prism (its pin goes back to the
previous `core-v*` tag, which still exists) and publish a patch; never move or delete a tag.
Never split, tag or push `core-dist` by hand on main any more; release candidates cut from an open
PR's branch (`core-v0.2.0-rc.N`) are the one exception. The bump needs the secret
`PRISM_BUMP_TOKEN` (fine-grained, Maxaubert/Prism, Contents + Pull requests read/write); without it
the workflow warns and only releases the core.
- **A terminal change goes in `core/`**, and is a change to Prism too: say so in the PR, and ask the
owner when it would conflict with how Prism works. App-shell changes (tabs, start screen, window)
stay in `src/`.
Expand Down
Loading