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
58 changes: 50 additions & 8 deletions .github/workflows/j5-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ name: J5 Release
# fetches. packages/shared/src/cliRelease.ts, the installer, `j5 update`,
# the background service, and SSH remote launch all download from here.
# J5 no longer publishes to npm (FORK.md release-archive case).
#
# A build whose version is `<x.y.z>-preview.<yyyymmdd>.<n>` publishes a preview:
# a GitHub pre-release that never becomes "latest", so the stable installer
# URL, `j5 update`, and desktop updates never pick it up. Maintainers cut one
# from an unmerged branch to test installs and updates before a release; see
# docs/j5/runbooks/macos-packaging.md.

on:
workflow_dispatch:
Expand All @@ -34,6 +40,7 @@ jobs:
outputs:
ref: ${{ steps.source.outputs.ref }}
version: ${{ steps.source.outputs.version }}
preview: ${{ steps.source.outputs.preview }}
published: ${{ steps.source.outputs.published }}
steps:
- uses: actions/github-script@v8
Expand Down Expand Up @@ -61,14 +68,17 @@ jobs:
const { data } = await github.rest.repos.getContent({ ...context.repo, path, ref: run.head_sha });
return JSON.parse(Buffer.from(data.content, 'base64').toString()).version;
}));
if (!/^\d+\.\d+\.\d+$/.test(versions[0]) || versions.some(version => version !== versions[0])) {
throw new Error(`This workflow requires one stable version across ${paths.join(', ')}; found ${versions.join(', ')}.`);
// The preview pattern is the one packages/shared/src/cliRelease.ts and
// scripts/build-desktop-artifact.ts recognise.
const preview = /^\d+\.\d+\.\d+-preview\.\d{8}\.\d+$/.test(versions[0]);
if ((!preview && !/^\d+\.\d+\.\d+$/.test(versions[0])) || versions.some(version => version !== versions[0])) {
throw new Error(`This workflow requires one stable or preview version across ${paths.join(', ')}; found ${versions.join(', ')}.`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
const version = versions[0];
// Release archives replace the npm package from 0.0.44 on; an older
// version number would be offered as a downgrade to every installed
// J5 and would collide with the npm-era tags.
const [major, minor, patch] = version.split('.').map(Number);
const [major, minor, patch] = version.split('-', 1)[0].split('.').map(Number);
if (major === 0 && minor === 0 && patch < 44) {
throw new Error(`J5 release archives start at 0.0.44; ${version} belongs to the npm era.`);
}
Expand Down Expand Up @@ -101,10 +111,11 @@ jobs:
}
core.setOutput('ref', run.head_sha);
core.setOutput('version', version);
core.setOutput('preview', String(preview));
core.setOutput('published', String(published));
core.notice(published
? `J5 Code ${version} is already published; nothing to do.`
: `Publishing J5 Code ${version} from ${run.head_sha}.`);
: `Publishing J5 Code ${version}${preview ? ' as a preview' : ''} from ${run.head_sha}.`);

verify:
needs: resolve
Expand Down Expand Up @@ -306,6 +317,7 @@ jobs:
GH_REPO: ${{ github.repository }}
RELEASE_VERSION: ${{ needs.resolve.outputs.version }}
RELEASE_SHA: ${{ needs.resolve.outputs.ref }}
RELEASE_PREVIEW: ${{ needs.resolve.outputs.preview }}
steps:
- uses: actions/checkout@v6
with:
Expand All @@ -331,7 +343,12 @@ jobs:
set -euo pipefail
test -f "release-assets/J5-Code-$RELEASE_VERSION-arm64.dmg"
test -f "release-assets/J5-Code-$RELEASE_VERSION-arm64.zip"
grep -Fx "version: $RELEASE_VERSION" release-assets/latest-mac.yml
# A preview desktop build has no update feed, so it emits no metadata.
if [[ "$RELEASE_PREVIEW" == true ]]; then
test ! -e release-assets/latest-mac.yml
else
grep -Fx "version: $RELEASE_VERSION" release-assets/latest-mac.yml
fi
archives=(
"t3-$RELEASE_VERSION-darwin-arm64.tar.gz"
"t3-$RELEASE_VERSION-linux-x64.tar.gz"
Expand All @@ -352,7 +369,21 @@ jobs:
test "$tag_sha" = "$RELEASE_SHA"
fi
if ! gh release view "$tag" >/dev/null 2>&1; then
cat > "$RUNNER_TEMP/release-notes.md" <<EOF
if [[ "$RELEASE_PREVIEW" == true ]]; then
cat > "$RUNNER_TEMP/release-notes.md" <<EOF
J5 Code $RELEASE_VERSION (preview)

A maintainers' test build cut from an unreleased branch. It can be broken, gets no fixes, and is never offered as an update.

- Command-line server, fresh install:
\`curl -fsSL https://github.com/$GH_REPO/releases/download/$tag/install.sh | T3CODE_VERSION=$RELEASE_VERSION sh\`
- Command-line server, from an installed \`j5\`: \`j5 update $RELEASE_VERSION\` in a terminal.
- macOS app: the DMG below. It replaces an installed J5 Code and never updates itself; reinstall a stable DMG to go back.

Built from $RELEASE_SHA.
EOF
else
cat > "$RUNNER_TEMP/release-notes.md" <<EOF
J5 Code $RELEASE_VERSION

- macOS Apple Silicon app, Developer ID signed and notarized by Apple.
Expand All @@ -364,7 +395,18 @@ jobs:

Built from $RELEASE_SHA.
EOF
gh release create "$tag" --target "$RELEASE_SHA" --title "J5 Code $RELEASE_VERSION" --notes-file "$RUNNER_TEMP/release-notes.md" --draft
fi
# Marked from the start: a draft left behind by a failed run must not
# become "latest" if someone publishes it by hand.
prerelease=()
if [[ "$RELEASE_PREVIEW" == true ]]; then prerelease=(--prerelease); fi
gh release create "$tag" --target "$RELEASE_SHA" --title "J5 Code $RELEASE_VERSION" --notes-file "$RUNNER_TEMP/release-notes.md" --draft "${prerelease[@]}"
fi
gh release upload "$tag" release-assets/* --clobber
gh release edit "$tag" --draft=false --latest
# A preview must never become "latest": the stable installer URL and
# desktop updates resolve through it.
if [[ "$RELEASE_PREVIEW" == true ]]; then
gh release edit "$tag" --draft=false --prerelease --latest=false
else
gh release edit "$tag" --draft=false --latest
fi
2 changes: 1 addition & 1 deletion FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ On every advance, grep the files upstream added since the old pin, not only `BRA

Fork-owned workflows use the Node version in `.nvmrc`, pnpm `11.10.0`, the frozen pnpm lockfile, and
the repo-local Vite Plus binary. `J5 CI` is the push/PR gate for `j5/**`; `J5 Weekly Full Build` is the
scheduled and manually dispatchable pre-rebase suite plus Apple Silicon desktop build. `J5 Mobile CI` runs upstream's mobile native static analysis and an iOS simulator Debug build on macOS, each only when its native inputs change; both are advisory and outside the required check. `J5 Release` publishes CLI archives (darwin-arm64, linux-x64), SHA256SUMS, install.sh and the signed desktop build to GitHub Releases (case 40); npm publishing is retired. The CLI archive job switches the host to Node 26.8.2 only around `build-exe` (Node single-executables need `--build-sea`), runs every repo script through `pnpm exec` so the repo-local `vp` is on PATH, and applies production web icons before packing.
scheduled and manually dispatchable pre-rebase suite plus Apple Silicon desktop build. `J5 Mobile CI` runs upstream's mobile native static analysis and an iOS simulator Debug build on macOS, each only when its native inputs change; both are advisory and outside the required check. `J5 Release` publishes CLI archives (darwin-arm64, linux-x64), SHA256SUMS, install.sh and the signed desktop build to GitHub Releases (case 40), as the latest release for a stable version and as a pre-release that is never latest for a `-preview.<date>.<n>` version (procedure in `docs/j5/runbooks/macos-packaging.md`); npm publishing is retired. The CLI archive job switches the host to Node 26.8.2 only around `build-exe` (Node single-executables need `--build-sea`), runs every repo script through `pnpm exec` so the repo-local `vp` is on PATH, and applies production web icons before packing.

See [`docs/j5/runbooks/macos-packaging.md`](docs/j5/runbooks/macos-packaging.md) for the local build, signature
verification, install, Gatekeeper approval, and workflow runbook.
Expand Down
31 changes: 28 additions & 3 deletions docs/j5/runbooks/macos-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,42 @@ For a public release:
4. After publication, bring the version bump into `j5/main` with a PR so `j5/main` never reports an
older version than the latest release.

For a preview, to test installs and updates from a branch before it merges:

1. On a branch cut from the work under test, run
`node scripts/update-release-package-versions.ts <x.y.z>-preview.<yyyymmdd>.<n>`, using the next
release's `x.y.z`, today's date, and a counter that starts at 1. Commit and push it as
`j5/preview-<x.y.z>-<n>`. This branch is never merged.
2. Run `J5 Signed macOS Build` on that branch, then `J5 Release` with that build's run ID, as for a
public release. Keep the branch at that commit until the release is published.
3. Install it by naming the version; nothing offers a preview on its own:
- **Fresh server:** use the installer attached to that release, which is the one under test:
`curl -fsSL https://github.com/Jacksondr5/j5code/releases/download/v<version>/install.sh | T3CODE_VERSION=<version> sh`.
- **Installed server:** run `j5 update <version>` in a terminal and confirm the prompt.
- **Desktop app:** install the release's DMG. It replaces the installed app and has no update
feed, so reinstall a stable DMG to go back; its window title uses the nightly name. While it
is installed, it offers to update a connected server to its own version, which is how an
in-app server update is tested. The offer compares `x.y.z` only, so a server already on that
`x.y.z`, stable or an earlier preview of it, gets no offer: return it to an older stable
first, or use `j5 update <version>` on its machine.
4. A server on a preview follows previews. Return it to stable with
`j5 update --channel stable`, adding `--allow-downgrade` if the stable release is older.
5. Delete the preview release, its tag, and the branch once the work has shipped.

A preview is a GitHub pre-release and is never marked latest. The stable installer URL,
`j5 update`, and desktop updates ignore it.

`J5 Release` publishes one GitHub Release, tagged `v<version>` at the build's commit, containing:

- the signed and notarized Apple Silicon DMG and ZIP, their blockmaps, and `latest-mac.yml` from the
selected build run;
- the signed and notarized Apple Silicon DMG and ZIP from the selected build run, with their
blockmaps and `latest-mac.yml` (a preview build has no update feed and emits no `latest-mac.yml`);
- self-contained CLI archives `t3-<version>-darwin-arm64.tar.gz` (Developer ID signed and notarized)
and `t3-<version>-linux-x64.tar.gz`, built and smoke-tested in the release run from the same
commit;
- `install.sh` and `SHA256SUMS` over the archives and the installer.

Its resolve job reads the version from the build commit's four manifests, which must agree on one
stable version of at least 0.0.44 (0.0.43 and earlier were npm releases). If a release for that
stable or preview version of at least 0.0.44 (0.0.43 and earlier were npm releases). If a release for that
version already exists at a different commit, the run fails: bump the version and build again. If
it is already published from the same commit with `SHA256SUMS` attached, the run does nothing. The
workflow uses GitHub-hosted runners and does not deploy relay or Vercel services.
Expand Down
Loading