Skip to content

Commit cf629c8

Browse files
ndemiancclaude
andcommitted
Atom++ release CI: hybrid build workflow (build in CI, sign locally)
.github/workflows/release.yml — on a v* tag: run the extension unit tests, then build BOTH macOS arches on their NATIVE runners (macos-14 = arm64, macos-13 = x64) via bootstrap.sh + build-macos.sh, and attach the unsigned .app bundles to a DRAFT release. No signing secrets in CI — the Developer ID cert never leaves the local machine. Building x64 natively on macos-13 is the point: it can't easily be cross-built from Apple silicon. docs/RELEASING.md §7 — the concrete hybrid runbook (tag -> CI builds -> gh release download -> make-dmg.sh signs+notarizes+staples each arch locally -> upload dmgs -> drop the unsigned zips -> publish), plus the fully-automated (sign-in-CI) alternative and why we don't use it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0dc8ebd commit cf629c8

2 files changed

Lines changed: 141 additions & 5 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Build release apps
2+
3+
# Hybrid release model (see docs/RELEASING.md): CI does the heavy, awkward part — building BOTH
4+
# macOS arches on their NATIVE runners (you can't easily build x64 on an Apple-silicon Mac) — with
5+
# NO signing secrets. It produces unsigned .app bundles and attaches them to a DRAFT release. You
6+
# then sign + notarize + staple LOCALLY (your Developer ID cert never leaves your machine), attach
7+
# the notarized dmgs, delete the UNSIGNED-*.app.zip assets, and publish.
8+
9+
on:
10+
push:
11+
tags: ["v*"] # e.g. git tag v0.1.0 && git push --tags
12+
workflow_dispatch:
13+
inputs:
14+
vscode_tag:
15+
description: "Override the pinned VS Code tag (blank = scripts/bootstrap.sh default)"
16+
required: false
17+
default: ""
18+
19+
permissions:
20+
contents: write # create/update the draft release
21+
22+
jobs:
23+
# Cheap gate: run the extension unit tests before spending ~1h of macOS build minutes.
24+
test:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: actions/setup-node@v4
29+
with:
30+
node-version: "24"
31+
- name: Extension unit tests
32+
run: |
33+
cd extensions/atom-ai
34+
for t in test/*.test.js; do node "$t"; done
35+
36+
build:
37+
name: Build ${{ matrix.arch }}
38+
needs: test
39+
strategy:
40+
fail-fast: false
41+
matrix:
42+
include:
43+
- { runner: macos-14, arch: arm64 } # Apple Silicon (native)
44+
- { runner: macos-13, arch: x64 } # Intel (native)
45+
runs-on: ${{ matrix.runner }}
46+
steps:
47+
- uses: actions/checkout@v4
48+
- uses: actions/setup-node@v4
49+
with:
50+
node-version: "24" # bootstrap.sh checks the .nvmrc major; 24.x satisfies the 1.126 pin
51+
- name: Cache npm downloads
52+
uses: actions/cache@v4
53+
with:
54+
path: ~/.npm
55+
key: npm-${{ matrix.arch }}-${{ hashFiles('scripts/bootstrap.sh') }}
56+
restore-keys: npm-${{ matrix.arch }}-
57+
- name: Bootstrap Code-OSS + branding + deps
58+
run: ./scripts/bootstrap.sh
59+
env:
60+
VSCODE_TAG: ${{ github.event.inputs.vscode_tag }}
61+
- name: Build Atom++.app (${{ matrix.arch }}, proprietary stripped)
62+
run: ./scripts/build-macos.sh ${{ matrix.arch }}
63+
- name: Zip the unsigned app
64+
run: ditto -c -k --sequesterRsrc --keepParent "VSCode-darwin-${{ matrix.arch }}/Atom++.app" "UNSIGNED-Atom++-${{ matrix.arch }}.app.zip"
65+
- name: Upload app artifact
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: UNSIGNED-Atom++-${{ matrix.arch }}
69+
path: UNSIGNED-Atom++-${{ matrix.arch }}.app.zip
70+
if-no-files-found: error
71+
retention-days: 14
72+
73+
draft-release:
74+
name: Draft release
75+
needs: build
76+
if: startsWith(github.ref, 'refs/tags/')
77+
runs-on: ubuntu-latest
78+
steps:
79+
- uses: actions/download-artifact@v4
80+
with:
81+
path: apps
82+
merge-multiple: true
83+
- name: Create / refresh the draft release with the UNSIGNED apps
84+
uses: softprops/action-gh-release@v2
85+
with:
86+
draft: true
87+
name: Atom++ ${{ github.ref_name }}
88+
tag_name: ${{ github.ref_name }}
89+
files: apps/*.zip
90+
fail_on_unmatched_files: true
91+
body: |
92+
**Draft — not for release as-is.** The attached `UNSIGNED-Atom++-<arch>.app.zip`
93+
files are CI build artifacts with **no Developer ID signature or notarization**.
94+
95+
To finish the release **locally** (your signing cert never touches CI):
96+
97+
1. `gh release download ${{ github.ref_name }} --pattern 'UNSIGNED-*.app.zip'`
98+
2. For each arch — unzip into `VSCode-darwin-<arch>/`, then
99+
`CODESIGN_IDENTITY="Developer ID Application: …" NOTARY_PROFILE=atompp-notary ./scripts/make-dmg.sh <arch>`
100+
(signs → dmg → notarizes → staples → `Atom++-<arch>.dmg`).
101+
3. `gh release upload ${{ github.ref_name }} Atom++-arm64.dmg Atom++-x64.dmg`
102+
4. **Delete the `UNSIGNED-*.app.zip` assets**, add real notes, and publish.
103+
104+
Full runbook: `docs/RELEASING.md`.

‎docs/RELEASING.md‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,41 @@ see the new version. Keep the feed version in lockstep with the tag.
105105
| Gatekeeper still warns after notarizing | Forgot to **staple**, or stapled the app but not the dmg. |
106106
| Chat won't open / shortcut dead in a build | `atompp.ai.focus` is `Ctrl+Cmd+I` (moved off the `Cmd+Alt+I` DevTools collision); the chat also auto-reveals until the first message is sent. |
107107

108-
## 7. CI (optional)
108+
## 7. CI build — hybrid model (`.github/workflows/release.yml`)
109+
110+
**CI builds both arches; you sign locally.** Your Developer ID cert never touches GitHub. CI exists to
111+
solve the awkward part — building the **Intel (x64)** dmg, which you can't easily do on an Apple-silicon
112+
Mac — by building each arch on its **native** runner (`macos-14` = arm64, `macos-13` = x64). It produces
113+
**unsigned** `.app` bundles; you do the fast, sensitive sign + notarize + staple on your machine.
114+
115+
The whole release becomes:
116+
117+
```sh
118+
# 1. Kick off CI (builds both arches, ~30–60 min/arch; free on public repos, 10× minutes while private)
119+
git tag v0.1.0 && git push --tags
120+
# → workflow builds → creates a DRAFT release with UNSIGNED-Atom++-<arch>.app.zip attached
121+
122+
# 2. Sign + notarize LOCALLY (needs the one-time setup from §1)
123+
gh release download v0.1.0 --pattern 'UNSIGNED-*.app.zip'
124+
for A in arm64 x64; do
125+
rm -rf "VSCode-darwin-$A" && ditto -x -k "UNSIGNED-Atom++-$A.app.zip" "VSCode-darwin-$A"
126+
CODESIGN_IDENTITY="Developer ID Application: SERGII DEMIANCHUK (AJ27Y4Z2HS)" \
127+
NOTARY_PROFILE=atompp-notary ./scripts/make-dmg.sh "$A" # → Atom++-$A.dmg (signed+notarized+stapled)
128+
done
129+
130+
# 3. Verify (§3), then attach the dmgs, drop the unsigned zips, and publish
131+
gh release upload v0.1.0 Atom++-arm64.dmg Atom++-x64.dmg
132+
gh release delete-asset v0.1.0 UNSIGNED-Atom++-arm64.app.zip UNSIGNED-Atom++-x64.app.zip
133+
gh release edit v0.1.0 --draft=false --notes-file RELEASE-NOTES.md
134+
```
109135

110-
On a `macos` runner: base64-encode the `.p12` Developer ID export + store it (plus cert password,
111-
`APPLE_ID`, `TEAM_ID`, `APP_SPECIFIC_PASSWORD`) as repo secrets; import the cert into a temp keychain at
112-
job start, then run `build-macos.sh` → `make-dmg.sh` with `APPLE_ID`/`TEAM_ID`/`APP_SPECIFIC_PASSWORD`
113-
set (make-dmg.sh uses those when `NOTARY_PROFILE` is absent).
136+
Notes:
137+
- **No secrets required** — the workflow is credential-free by design (that's the whole point of hybrid).
138+
- The dmg names (`Atom++-arm64.dmg` / `Atom++-x64.dmg`) are exactly what the download funnel at
139+
`atompp.ai/download/<arch>` expects — don't rename them.
140+
- `releases/latest` only resolves once this is a **published, non-prerelease** release with both dmgs.
141+
- **Fully-automated alternative** (signing in CI) if you ever want zero local steps: base64 the `.p12`
142+
Developer ID export + store it and an App Store Connect API key as secrets behind a *protected
143+
Environment*, import into a temp keychain at job start, and run `make-dmg.sh` with `CODESIGN_IDENTITY`
144+
set. Standard (VSCodium does this) but puts your signing identity in the cloud — the hybrid flow above
145+
deliberately doesn't.

0 commit comments

Comments
 (0)