Skip to content

Commit 3add7ef

Browse files
authored
Verify native installer candidates before draft release attachment (#186)
Bind macOS, Windows and Linux installer candidates to the exact tagged source, run identity, installer hashes and native acceptance receipts before attaching a five-installer draft release. Reject missing, stale, tampered, symlinked or failed inputs and preserve pending account, session and historical-migration limits. Build and workspace typechecks pass with 1,405 unit tests and 15 skips. Fresh native Windows/Linux CI passes; installer hashes and manifests match the installed-app reports. Source integration and artifact proof were independently reviewed. Signed macOS acceptance, approved tag creation and publication remain separate gates. Coven-Harness: Codex / gpt-6-astra
1 parent c738201 commit 3add7ef

10 files changed

Lines changed: 514 additions & 25 deletions

File tree

‎.github/workflows/platform-cli.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: Native platform builds
22

33
on:
4+
workflow_call:
45
workflow_dispatch:
56
pull_request:
67
paths:

‎.github/workflows/release.yml‎

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
name: Release
22

3-
# Builds, signs, notarizes and verifies the macOS artifacts for a pushed version tag and attaches
4-
# them to that tag's GitHub release (created as a draft if it does not exist). Fails closed: no
5-
# secret, no artifact. Nothing unsigned is ever uploaded. See docs/release-signing.md.
3+
# Signs and verifies macOS updates, builds unsigned Windows/Linux installers on native hosts,
4+
# then attaches exact accepted candidates to a draft release. Publication is separate.
5+
# macOS signing fails closed. See docs/release-signing.md.
66
on:
77
push:
88
tags: ["v*"]
@@ -53,12 +53,16 @@ jobs:
5353
git fetch --force origin "refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}"
5454
[ "$(git cat-file -t "${GITHUB_REF_NAME}")" = tag ] || { echo "::error::${GITHUB_REF_NAME} is not an annotated, signed tag"; exit 1; }
5555
git tag -v "${GITHUB_REF_NAME}"
56+
[ "$(git rev-parse "${GITHUB_REF_NAME}^{commit}")" = "$GITHUB_SHA" ] || { echo "::error::signed tag does not match workflow source"; exit 1; }
5657
5758
- uses: actions/setup-node@v5
5859
with:
5960
node-version: 24
6061
cache: npm
6162

63+
- name: Verify tag, package version and checkout identity
64+
run: node scripts/verify-release-candidate.mjs identity
65+
6266
- name: Install
6367
run: npm ci
6468

@@ -197,28 +201,67 @@ jobs:
197201
path: ${{ runner.temp }}/modex-update-acceptance-evidence/
198202
if-no-files-found: ignore
199203

204+
native-platforms:
205+
name: native Windows and Linux candidates
206+
uses: ./.github/workflows/platform-cli.yml
207+
permissions:
208+
contents: read
209+
200210
attach-release:
201211
name: attach verified release artifacts
202-
needs: [macos, native-update]
212+
needs: [macos, native-update, native-platforms]
203213
runs-on: ubuntu-latest
204214
permissions:
205215
contents: write
206216
steps:
217+
- uses: actions/checkout@v5
218+
- uses: actions/setup-node@v5
219+
with:
220+
node-version: 24
207221
- uses: actions/download-artifact@v4
208222
with:
209223
name: macos-release-candidate
210-
path: release
211-
- name: Verify candidate bytes and attach to the release (draft if new)
224+
path: release-candidates/macos
225+
- uses: actions/download-artifact@v4
226+
with:
227+
name: native-installers-win32
228+
path: release-candidates/win32
229+
- uses: actions/download-artifact@v4
230+
with:
231+
name: native-installers-linux
232+
path: release-candidates/linux
233+
- uses: actions/download-artifact@v4
234+
with:
235+
name: native-installed-win32
236+
path: release-candidates/installed-win32
237+
- uses: actions/download-artifact@v4
238+
with:
239+
name: native-installed-linux
240+
path: release-candidates/installed-linux
241+
- uses: actions/download-artifact@v4
242+
with:
243+
name: macos-native-update-upgrade
244+
path: release-candidates/macos-upgrade
245+
- uses: actions/download-artifact@v4
246+
with:
247+
name: macos-native-update-native-failure
248+
path: release-candidates/macos-native-failure
249+
- uses: actions/download-artifact@v4
250+
with:
251+
name: macos-native-update-interrupted
252+
path: release-candidates/macos-interrupted
253+
- name: Bind installer bytes and native receipts to this release
254+
run: node scripts/verify-release-candidate.mjs collect release-candidates release-candidates/verified
255+
- name: Attach verified files to a draft release
212256
env:
213257
GH_TOKEN: ${{ github.token }}
214258
GH_REPO: ${{ github.repository }}
215-
working-directory: release
259+
working-directory: release-candidates/verified
216260
run: |
217-
sha256sum --check SHA256SUMS.txt
218-
# Existing public releases and existing asset names are immutable here.
261+
# Existing public releases and assets are immutable here. No --clobber.
219262
if gh api "repos/$GH_REPO/releases/tags/$GITHUB_REF_NAME" > "$RUNNER_TEMP/release.json"; then
220-
jq -e '.draft == true' "$RUNNER_TEMP/release.json"
263+
jq -e '.draft == true and (.assets | length == 0)' "$RUNNER_TEMP/release.json"
221264
else
222-
gh api --method POST "repos/$GH_REPO/releases" -f tag_name="$GITHUB_REF_NAME" -f name="Modex $GITHUB_REF_NAME" -f body="Draft: notes pending." -F draft=true > "$RUNNER_TEMP/release.json"
265+
gh api --method POST "repos/$GH_REPO/releases" -f tag_name="$GITHUB_REF_NAME" -f name="Modex $GITHUB_REF_NAME" -f body="Draft: notes pending. Windows installers are unsigned. See RELEASE-MANIFEST.json for native acceptance limits." -F draft=true > "$RUNNER_TEMP/release.json"
223266
fi
224-
gh release upload "$GITHUB_REF_NAME" Modex-*-arm64.dmg Modex-*-arm64.zip SHA256SUMS.txt
267+
gh release upload "$GITHUB_REF_NAME" ./*

‎apps/desktop/scripts/accept-linux-package.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ fs.writeFileSync(path.join(home, "app", "state.json"), JSON.stringify({ version:
5858
const sentinel = path.join(root, "unrelated-data");
5959
const marker = randomUUID();
6060
fs.writeFileSync(sentinel, marker);
61-
const result = { schema: 1, sourceSha: manifest.sourceSha, runId: manifest.runId,
61+
const result = { schema: 1, candidate: manifest, sourceSha: manifest.sourceSha, runId: manifest.runId,
6262
platform: process.platform, arch: process.arch, launches: [], uninstall: "pending",
6363
installerAcceptance: "pending", credentials: "pending", remaining: ["KWallet credential persistence", "complete CLI/account acceptance"] };
6464
let installAttempted = false;
@@ -307,6 +307,7 @@ finally {
307307
} catch (error) { failure = failure ? new AggregateError([failure, error], "Acceptance and cleanup failed") : error; }
308308
try { await stopKeyring(); }
309309
catch (error) { failure = failure ? new AggregateError([failure, error], "Acceptance and keyring cleanup failed") : error; }
310+
result.nativeChecksPassed = !failure;
310311
result.error = failure ? String(failure) : undefined;
311312
fs.writeFileSync(path.join(evidence, "result.json"), JSON.stringify(result, null, 2) + "\n");
312313
}

‎apps/desktop/scripts/accept-windows-package.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const dataMarkers = defaultData.map(directory => {
8585
fs.writeFileSync(file, marker);
8686
return file;
8787
});
88-
const result = { schema: 1, sourceSha: manifest.sourceSha, runId: manifest.runId,
88+
const result = { schema: 1, candidate: manifest, sourceSha: manifest.sourceSha, runId: manifest.runId,
8989
platform: process.platform, arch: process.arch, terminalShell: commandShell, terminalEnvironmentKey: "ComSpec", launches: [], uninstall: "pending",
9090
installerAcceptance: "pending", credentials: "pending", remaining: ["complete CLI/account acceptance"] };
9191
let installAttempted = false;
@@ -247,6 +247,7 @@ finally {
247247
result.uninstall = "NSIS removed application and registration; recorded threads, probe ciphertext and unrelated data retained";
248248
}
249249
} catch (error) { failure = failure ? new AggregateError([failure, error], "Acceptance and cleanup failed") : error; }
250+
result.nativeChecksPassed = !failure;
250251
result.error = failure ? String(failure) : undefined;
251252
fs.writeFileSync(path.join(evidence, "result.json"), JSON.stringify(result, null, 2) + "\n");
252253
}

‎docs/native-platforms.md‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,28 @@ Fresh required macOS checks and native Windows/Linux jobs verify each submitted
185185
[PR #173](https://github.com/TypeSafeAI/modex/pull/173) records the exact hosted receipts.
186186

187187
Installer candidate implementation and native test artifacts are distinct from release
188-
publication. The protected release workflow still uploads only macOS assets; acceptance
189-
and publication of the exact tagged Windows/Linux installers remain pending, along with
188+
publication. The combined release workflow below is prepared, but acceptance and
189+
publication of the exact tagged Windows/Linux installers remain pending, along with
190190
the account/session and historical-migration limits recorded above.
191+
192+
## Tagged release candidates
193+
194+
The protected Release workflow calls the native platform workflow for the same tagged
195+
source. Each installed-app report includes its byte-verified candidate manifest and sets
196+
`nativeChecksPassed` only after acceptance and cleanup succeed. The attachment job requires
197+
matching source, run, version, architecture, file sizes and hashes, plus the signed macOS
198+
scenario receipts. It copies only the five verified installers and publishes their hashes
199+
and remaining acceptance limits into a draft release; private upgrade baselines are excluded.
200+
201+
This workflow change has not yet run on an approved signed tag. It does not change the native
202+
evidence or outstanding gates above. Windows installers remain unsigned, updates remain
203+
manual, and `installerAcceptance` stays `pending` until the remaining account/session checks
204+
have evidence. See [release signing](release-signing.md#candidate-attachment-checks).
205+
206+
The [release-patch native run](https://github.com/TypeSafeAI/modex/actions/runs/38230412498)
207+
passed at `5189b57`. Both reports contain the verified candidate manifest and
208+
`nativeChecksPassed: true`. The downloaded installer archives match GitHub's sizes and
209+
digests; their manifests exactly match the installed-app receipts, and all three installer
210+
byte sizes and SHA-256 values match. The tested merge tree equals that source head.
211+
This proves the new report fields on that revision; it does not complete the pending
212+
account/session checks or the first combined signed-tag execution.

‎docs/release-signing.md‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Signing and notarizing the macOS build
22

3-
From v0.0.5 every release artifact is signed with a Developer ID Application certificate,
3+
From v0.0.5 every macOS release artifact is signed with a Developer ID Application certificate,
44
hardened-runtime enabled, notarized by Apple and stapled, so Gatekeeper opens it without a
55
warning on a fresh Mac. v0.0.1–v0.0.4 were ad-hoc signed and needed a right-click → Open.
66

@@ -85,7 +85,7 @@ Rules that keep this safe:
8585
4. **The private key stays in the keychain.** Export a `.p12` only to populate CI, with a
8686
strong password, and delete the export afterwards. The workflow imports it into a
8787
temporary keychain on the runner and deletes that keychain at the end of the job.
88-
5. **CI refuses unsigned output.** The workflow checks every secret is present, verifies the
88+
5. **macOS CI refuses unsigned output.** The workflow checks every secret is present, verifies the
8989
tag's SSH signature against `TAG_ALLOWED_SIGNERS` before building, and uploads only after
9090
the same verification the local script runs. The `release-signing` environment should
9191
require a reviewer, so a tag push alone cannot mint artifacts.
@@ -168,9 +168,13 @@ rehearsal that gates the merge.
168168
the full e2e suite against the packaged app. It also signs a private updater-enabled
169169
`0.0.0` source fixture. Three fresh GitHub-hosted macOS runners use those exact source
170170
and candidate ZIPs for native upgrade, native download failure, and interrupted-download
171-
acceptance. Only after all jobs pass does it attach the DMG, ZIP, and `SHA256SUMS.txt`
172-
to a draft release. Existing public releases and asset names cannot be overwritten.
173-
The private source fixture is never a public release asset.
171+
acceptance. The same workflow also runs native Windows and Linux installer checks.
172+
After all jobs pass, it verifies the five candidate installers against their hashes,
173+
exact tag/package/source/run identity, and native receipts. It attaches the macOS
174+
DMG/ZIP, unsigned Windows NSIS installer, Linux AppImage/Debian packages,
175+
`SHA256SUMS.txt`, and `RELEASE-MANIFEST.json` to a draft release. Existing public
176+
releases and drafts containing assets are refused. The private source fixtures are
177+
never public release assets.
174178
4. Download the draft's assets and check them on a Mac as in
175179
[Verifying a download](#verifying-a-download): `shasum -c`, Gatekeeper, stapler and the
176180
signing certificate. Attach the verification log, write the notes, then publish the draft.
@@ -202,3 +206,26 @@ Keep the `macos-native-update-*` artifacts with the release review. Local harnes
202206
simulated controller tests pass, but the three hosted native scenarios have not run. Signed
203207
tag approval, the `release-signing` reviewer, and draft-asset verification remain required
204208
before publication. See [update recovery](update-recovery.md) for current local behavior.
209+
210+
## Candidate attachment checks
211+
212+
`node scripts/verify-release-candidate.mjs identity` requires a stable, nonzero desktop
213+
version matching the tag and a checkout matching the workflow commit. The signing job
214+
also verifies that the signed tag points to that commit. Version `0.0.0` is reserved for
215+
private upgrade fixtures; an existing published tag must not be reused.
216+
217+
The attachment job downloads candidates and acceptance reports from the same workflow run
218+
into separate directories. The verifier checks all five installer files, both native
219+
platform manifests, and all three signed macOS scenario receipts before copying an explicit
220+
allowlist into a fresh output directory. It checks the copied hashes again, then writes
221+
the combined checksum list and release manifest. Missing, extra, tampered, symlinked, stale,
222+
or failed inputs stop attachment. Windows and Linux checks use the reusable native workflow
223+
with read-only repository access and receive no Apple signing secrets.
224+
225+
`nativeChecksPassed: true` records that the installed-app harness and its cleanup passed.
226+
It does not complete `installerAcceptance`, which remains `pending` with the account/session
227+
limits in each report and the release manifest. Windows signing is deferred, Windows/Linux
228+
updates remain manual, and same-source `0.0.0` fixtures do not prove historical migration.
229+
The combined release workflow still needs its first approved tagged execution; helper tests
230+
verify the attachment checks, not installed-app behavior. Review the remaining gates in
231+
[native-platforms.md](native-platforms.md) before publication.

‎docs/status.md‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The living map of what has shipped, what is on `main` but unreleased, what is in
44
what is next. Update it in the same PR that changes any of those; a reader should be able to
55
plan the next session from this page alone. Release evidence lives in [`reviews/`](reviews/).
66

7-
_Last updated 2026-10-11._
7+
_Last updated 2026-10-12._
88

99
## Shipped
1010

@@ -480,6 +480,16 @@ seven site tests, with real OpenKnowledge enabled and no skips.
480480

481481
## In flight
482482

483+
- **Verified multiplatform release candidates.** The release workflow now joins native
484+
Windows/Linux checks with signed macOS acceptance, binds installer bytes and receipts
485+
to the exact tag/source/run, and prepares a draft with five installers, checksums and
486+
an acceptance manifest. The combined recovery source passed local build/typechecks and
487+
1,405 unit tests (15 skipped). Native reports and all three installer hashes were verified
488+
on [the release-patch run](https://github.com/TypeSafeAI/modex/actions/runs/38230412498).
489+
The combined release workflow has not yet run on an approved signed tag.
490+
Windows signing stays deferred; account/session and historical migration gaps remain.
491+
[Attachment checks](release-signing.md#candidate-attachment-checks).
492+
483493
- **Automatic updates and durable drafts.** The unreleased desktop code connects verified download,
484494
cancellation, restart preparation, and recovery for packaged Macs. Schema-2 draft storage
485495
preserves independent text and files, pending originals, and owner-removal recovery.
@@ -709,9 +719,8 @@ Roughly in the order they earn their place; none is scheduled.
709719

710720
1. **Ship automatic installation of updates.** Finish the in-flight acceptance and signed
711721
release gates above. Published builds still require manual installation.
712-
2. **Publish Windows and Linux installers.** Extend the protected release pipeline, which
713-
currently uploads macOS artifacts, to verify and publish the exact tagged native
714-
installers. Candidate Actions artifacts are not releases. Provider-account acceptance,
722+
2. **Publish Windows and Linux installers.** Run the prepared protected release pipeline
723+
on an approved new signed tag, verify its draft assets, and complete release review. Candidate Actions artifacts are not releases. Provider-account acceptance,
715724
KWallet6/full graphical sessions, and historical schema migration remain unproven;
716725
see [native-platforms.md](native-platforms.md) for candidate evidence and its limits.
717726
3. **Routines.** Read-only routines come next: Chat or Plan turns on a timer, with the

0 commit comments

Comments
 (0)