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
45 changes: 33 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,7 @@ jobs:
node-version: 22.22.3
cache: npm

- name: Install locked dependencies
run: npm ci

- name: Install Playwright Chromium for Generative UI containment
run: npx playwright install chromium

- name: Assign the main-push version
- name: Resolve the declared release version
id: version
shell: bash
run: |
Expand All @@ -52,46 +46,69 @@ jobs:
if [[ -n "$base_tag_match" ]]; then
base_tag_exists=true
fi
release_version="$(node scripts/prepare-ci-release.mjs "$GITHUB_RUN_NUMBER" "$base_tag_exists")"
npm version "$release_version" --no-git-tag-version --allow-same-version
selection="$(node scripts/prepare-ci-release.mjs "$base_tag_exists")"
release_version="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).version)' "$selection")"
release_tag="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).tag)' "$selection")"
should_publish="$(node -e 'process.stdout.write(String(JSON.parse(process.argv[1]).publish))' "$selection")"
echo "version=$release_version" >> "$GITHUB_OUTPUT"
echo "tag=v$release_version" >> "$GITHUB_OUTPUT"
echo "tag=$release_tag" >> "$GITHUB_OUTPUT"
echo "publish=$should_publish" >> "$GITHUB_OUTPUT"
if [[ "$should_publish" != true ]]; then
echo "$release_tag already exists; skipping release without changing its version."
fi

- name: Install locked dependencies
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm ci

- name: Install Playwright Chromium for Generative UI containment
if: ${{ steps.version.outputs.publish == 'true' }}
run: npx playwright install chromium

- name: Verify Homebrew and website release consumers
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run release:check-consumers

- name: Verify TypeScript
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run type-check

- name: Verify lint
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run lint

- name: Verify diagnostics privacy and storage contracts
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run test:diagnostics

- name: Verify branding, updater, and release policy
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run test:branding

- name: Run JavaScript, TypeScript, and Computer Use tests
if: ${{ steps.version.outputs.publish == 'true' }}
env:
AIDEN_SKIP_VIRTUALIZED_LAUNCH_CONSTRAINT_TESTS: "1"
run: npm test

- name: Run Apple Foundation Models tests
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run test:native

- name: Run deterministic Electron E2E gate
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run test:e2e

- name: Verify production-profile diagnostics support workflow
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run test:e2e:diagnostics:production

- name: Build sanitized E2E failure receipt
if: ${{ failure() }}
if: ${{ failure() && steps.version.outputs.publish == 'true' }}
run: npm run diagnostics:failure-receipt -- test-results/e2e-safe-receipt.json electron-e2e test-failed

- name: Upload sanitized E2E failure receipt
if: ${{ failure() }}
if: ${{ failure() && steps.version.outputs.publish == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: playwright-e2e-release-${{ github.run_id }}-${{ github.run_attempt }}
Expand All @@ -100,6 +117,7 @@ jobs:
retention-days: 7

- name: Prepare App Store Connect key
if: ${{ steps.version.outputs.publish == 'true' }}
shell: bash
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
Expand All @@ -114,6 +132,7 @@ jobs:
echo "APPLE_API_KEY=$key_path" >> "$GITHUB_ENV"

- name: Build, sign, notarize, and verify distribution
if: ${{ steps.version.outputs.publish == 'true' }}
env:
AIDEN_ENABLE_AUTO_UPDATES: "1"
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
Expand All @@ -123,9 +142,11 @@ jobs:
run: npm run dist

- name: Verify diagnostics in the signed packaged app
if: ${{ steps.version.outputs.publish == 'true' }}
run: npm run test:e2e:diagnostics:packaged

- name: Publish verified release assets
if: ${{ steps.version.outputs.publish == 'true' }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
Expand Down
21 changes: 11 additions & 10 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ visitors and installed apps can download GitHub Release assets without a GitHub
- `.github/workflows/ci.yml` verifies pull requests and pushes on GitHub's `macos-26` image.
- `.github/workflows/release.yml` is considered enabled only when the source repository variable
`RELEASES_ENABLED` is exactly `true`.
- Each enabled push to `main` derives a monotonically increasing version from the workflow run
number. The beta line starts at `0.27.0`; the base `package.json` version supplies that release
line, so `0.27.0` plus run `41` produces `0.27.41` without committing a version-bump loop to
`main`.
- A release uses the complete SemVer declared in `package.json` exactly. For example, `0.35.0`
publishes only as `v0.35.0`; workflow run numbers never become application versions.
- If the exact declared tag already exists, an enabled push to `main` completes the release job as
a green no-op. Publishing another build requires an explicit reviewed version change in both
`package.json` and `package-lock.json`.
- The release job runs the full TypeScript, lint, JavaScript/TypeScript, Rust, Swift, and build
gates before preparing signing material.
- GitHub-hosted macOS VMs do not enforce Aiden's live kernel launch constraint. CI still verifies
Expand Down Expand Up @@ -44,8 +45,9 @@ visitors and installed apps can download GitHub Release assets without a GitHub
retry, and ready states. A two-minute no-progress stall is cancelled and retried with bounded
backoff. Installation starts only after the package and macOS updater handoff are ready, and
never interrupts an open workspace or bypasses the existing quit barriers.
- A failed or rerun job refuses to overwrite an existing tag. Recovery is a new `main` commit,
which receives a higher version.
- A failed or rerun job refuses to overwrite an existing tag. If no tag or draft was created, rerun
the same declared version. If a partial draft exists, inspect and remove only that exact failed
draft before retrying. Never manufacture a version from a workflow run number.

Local `npm run dist` builds do not embed a feed or perform automatic update checks. The release
workflow opts in with `AIDEN_ENABLE_AUTO_UPDATES=1`.
Expand Down Expand Up @@ -113,7 +115,6 @@ Never place an Apple private key, certificate password, or notarization credenti

## Version-line changes

For a planned minor or major release, change only the major/minor line in `package.json` and
`package-lock.json` (for example, `0.27.0` to `0.28.0`). The next workflow run becomes
`0.28.<run number>`, which remains greater than every `0.27.x` build. Do not lower the
major/minor line or manually reuse a published version.
For every planned release, change the complete version in `package.json` and `package-lock.json`
(for example, `0.35.0` to `0.35.1`, or `0.35.0` to `0.36.0`). The next eligible workflow run uses
that exact version. Do not lower the version or reuse a published tag.
30 changes: 13 additions & 17 deletions scripts/prepare-ci-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,30 @@ import { fileURLToPath } from "node:url";
const modulePath = fileURLToPath(import.meta.url);
const repositoryRoot = path.resolve(path.dirname(modulePath), "..");

export function automaticReleaseVersion(baseVersion, runNumber) {
const match = /^(\d+)\.(\d+)\.\d+(?:-[0-9A-Za-z.-]+)?$/u.exec(baseVersion);
if (!match) throw new Error(`Invalid base release version: ${baseVersion}`);
if (!/^[1-9]\d*$/u.test(String(runNumber))) {
throw new Error(`GitHub run number must be a positive integer: ${runNumber}`);
export function mainPushReleaseSelection(baseVersion, baseTagExists) {
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$/u.test(baseVersion)) {
throw new Error(`Invalid declared release version: ${baseVersion}`);
}
return `${Number(match[1])}.${Number(match[2])}.${Number(runNumber)}`;
}

export function mainPushReleaseVersion(baseVersion, runNumber, baseTagExists) {
const incrementedVersion = automaticReleaseVersion(baseVersion, runNumber);
if (typeof baseTagExists !== "boolean") {
throw new Error("Base tag existence must be a boolean");
throw new Error("Declared tag existence must be a boolean");
}
return baseTagExists ? incrementedVersion : baseVersion;
return Object.freeze({
version: baseVersion,
tag: `v${baseVersion}`,
publish: !baseTagExists,
});
}

if (process.argv[1] && path.resolve(process.argv[1]) === modulePath) {
const packageJson = JSON.parse(await readFile(path.join(repositoryRoot, "package.json"), "utf8"));
const baseTagExistsArgument = process.argv[3];
const baseTagExistsArgument = process.argv[2];
if (baseTagExistsArgument !== "true" && baseTagExistsArgument !== "false") {
throw new Error("Base tag existence must be provided as true or false");
throw new Error("Declared tag existence must be provided as true or false");
}
console.log(
mainPushReleaseVersion(
JSON.stringify(mainPushReleaseSelection(
packageJson.version,
process.argv[2],
baseTagExistsArgument === "true",
),
)),
);
}
40 changes: 20 additions & 20 deletions scripts/prepare-ci-release.test.mjs
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import assert from "node:assert/strict";
import test from "node:test";

import {
automaticReleaseVersion,
mainPushReleaseVersion,
} from "./prepare-ci-release.mjs";
import { mainPushReleaseSelection } from "./prepare-ci-release.mjs";

test("main pushes receive a monotonic patch version inside the declared release line", () => {
assert.equal(automaticReleaseVersion("0.27.0", "41"), "0.27.41");
assert.equal(automaticReleaseVersion("1.0.0", "41"), "1.0.41");
assert.equal(automaticReleaseVersion("2.3.0", 42), "2.3.42");
test("an unpublished declared version is selected exactly", () => {
assert.deepEqual(mainPushReleaseSelection("0.35.0", false), {
version: "0.35.0",
tag: "v0.35.0",
publish: true,
});
});

test("automatic release versions reject ambiguous inputs", () => {
assert.throws(() => automaticReleaseVersion("1.0", 1), /Invalid base release version/u);
assert.throws(() => automaticReleaseVersion("1.0.0", 0), /positive integer/u);
assert.throws(() => automaticReleaseVersion("1.0.0", "01"), /positive integer/u);
test("an existing declared tag makes release publication a green no-op", () => {
assert.deepEqual(mainPushReleaseSelection("0.35.0", true), {
version: "0.35.0",
tag: "v0.35.0",
publish: false,
});
});

test("a declared version publishes exactly once before automatic build increments", () => {
assert.equal(mainPushReleaseVersion("0.31.0", 55, false), "0.31.0");
assert.equal(mainPushReleaseVersion("0.31.0", 56, true), "0.31.56");
});

test("main-push release selection requires an explicit tag state", () => {
test("declared release selection rejects ambiguous inputs", () => {
assert.throws(
() => mainPushReleaseSelection("1.0", false),
/Invalid declared release version/u,
);
assert.throws(
() => mainPushReleaseVersion("0.31.0", 55, undefined),
/Base tag existence must be a boolean/u,
() => mainPushReleaseSelection("1.0.0", undefined),
/Declared tag existence must be a boolean/u,
);
});
22 changes: 21 additions & 1 deletion scripts/run-macos-distribution.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,29 @@ test("release publication checks deployed consumers before building", async () =
);
const consumerCheck = workflow.indexOf("npm run release:check-consumers");
const distributionBuild = workflow.indexOf("npm run dist");
const versionResolution = workflow.indexOf("Resolve the declared release version");
const dependencyInstall = workflow.indexOf("Install locked dependencies");

assert.match(workflow, /git ls-remote --tags origin/u);
assert.match(workflow, /--allow-same-version/u);
assert.doesNotMatch(workflow, /GITHUB_RUN_NUMBER|--allow-same-version/u);
assert.match(workflow, /node scripts\/prepare-ci-release\.mjs "\$base_tag_exists"/u);
assert.match(workflow, /steps\.version\.outputs\.publish == 'true'/u);
assert.ok(versionResolution >= 0 && versionResolution < dependencyInstall);
for (const stepName of [
"Install locked dependencies",
"Build, sign, notarize, and verify distribution",
"Verify diagnostics in the signed packaged app",
"Publish verified release assets",
]) {
assert.match(
workflow,
new RegExp(
`- name: ${stepName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\n` +
" {8}if: \\$\\{\\{ steps\\.version\\.outputs\\.publish == 'true' \\}\\}",
"u",
),
);
}
assert.ok(consumerCheck >= 0, "the release workflow must check Homebrew and the website");
assert.ok(
distributionBuild > consumerCheck,
Expand Down