Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6a537ac
docs(windows): port release and proof guidance from #999
nsxdavid Aug 1, 2026
47ca19f
feat(windows): add exact-SHA release proof gate
arul28 Aug 2, 2026
4da126b
fix(windows): bind standalone runtime proof
arul28 Aug 2, 2026
98d7d65
fix(windows): promote immutable runtime proof
arul28 Aug 2, 2026
e95578d
fix(windows): enforce exact release artifact identity
arul28 Aug 2, 2026
0a15782
fix(release): restore platform-neutral prepare-release dry run
arul28 Aug 2, 2026
24b3334
fix(release): pin and check approved Windows proof artifact retention
arul28 Aug 2, 2026
cd21e62
fix(release): publish runtime assets from the run that built them
arul28 Aug 2, 2026
8c41ebf
docs(release): document every required release gate and setting
arul28 Aug 2, 2026
df6f954
feat(release): build and publish Windows on the release tag
arul28 Aug 2, 2026
96b24c5
docs(release): make the release skill and AGENTS Windows-aware
arul28 Aug 2, 2026
786cb73
docs(release): describe the in-run Windows release model
arul28 Aug 2, 2026
8624167
test(release): pin the Windows standalone checksum normalization
arul28 Aug 2, 2026
2cc9c34
docs(release): drop deleted Windows gate variables from supporting docs
arul28 Aug 2, 2026
12071e5
docs(release): rewrite the signing playbook for the in-run Windows build
arul28 Aug 2, 2026
0df3f1d
docs(sync): state that Linux is a runtime target, not a sync peer
arul28 Aug 2, 2026
199324a
fix(release): sign Windows through Azure Artifact Signing
arul28 Aug 2, 2026
e6e7a5d
fix(release): sign the standalone Windows runtime through Azure Artif…
arul28 Aug 2, 2026
d76c310
fix(release): require Azure signing credentials instead of PFX secrets
arul28 Aug 2, 2026
1017abb
docs(release): rewrite the Windows signing playbook for Azure Artifac…
arul28 Aug 2, 2026
561e607
fix(release): move publishing into its own reusable workflow
arul28 Aug 3, 2026
39fb091
fix(release): let windows_proof validate a pre-merge commit
arulsharma Aug 3, 2026
9b9988c
ci: allow CI to be dispatched for stacked branches
arulsharma Aug 3, 2026
06d09e0
Merge branch 'codex/windows-packaging-updates' into codex/windows-rel…
arulsharma Aug 3, 2026
1ad268d
ci: scope the dispatched secret scan to this branch's commits
arulsharma Aug 3, 2026
a9ba354
ci: invoke gitleaks directly for dispatched runs
arulsharma Aug 3, 2026
4708ea6
fix(release): let windows_proof build without a ci-pass check run
arulsharma Aug 3, 2026
6ea1dd5
Merge branch 'codex/windows-packaging-updates' into codex/windows-rel…
arul28 Aug 3, 2026
c239304
fix(windows): strip the vendor signature before postject injects the SEA
arulsharma Aug 3, 2026
90a7c7c
Merge branch 'codex/windows-packaging-updates' into codex/windows-rel…
arul28 Aug 3, 2026
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
122 changes: 113 additions & 9 deletions .agents/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ ship a TestFlight build.

This is a **GitHub desktop + local ASC iOS release flow**. Desktop releases
must use the repository GitHub Actions release workflow so macOS updater assets
are produced reproducibly as per-arch ZIP/DMG artifacts. This Mac may still run
checks, create release docs/tags, monitor and recover the workflow, and build
and upload iOS/TestFlight releases through ASC.
are produced reproducibly as per-arch ZIP/DMG artifacts, and so the signed
Windows installer is produced on a Windows runner this Mac cannot provide. This
Mac may still run checks, create release docs/tags, monitor and recover the
workflow, and build and upload iOS/TestFlight releases through ASC.

A **preflight** is a cheap check that runs before expensive build/upload work.
Use preflights to catch release blockers while fixes can still be committed
Expand Down Expand Up @@ -42,7 +43,13 @@ without burning a notarization, TestFlight upload, or build number.
crash Squirrel.Mac during in-app update.
- **Do not publish broken updater metadata.** Before making a desktop release
public/latest, verify `latest-mac.yml` references assets that exist and that
the expected arm64/x64 DMGs and ZIPs are present.
the expected arm64/x64 DMGs and ZIPs are present. When Windows is enabled,
apply the same rule to `latest.yml` and the Windows installer.
- **Do not publish a half-platform release.** The Windows gate and the Windows
assets must agree. If `ADE_WINDOWS_PUBLIC_RELEASE_ENABLED` is `1` the draft
must carry Windows assets; if it is not `1` the draft must carry none. Either
mismatch means the workflow did not do what you think it did, so keep the
release draft/private and investigate before publishing.
- **Do not discover obvious release blockers after upload.** Preflight iOS App
Clip packaging metadata before starting the expensive mobile phase.
- **Do not wait forever.** If GitHub notarization or TestFlight processing
Expand All @@ -55,13 +62,24 @@ This release lane runs on an Apple Silicon Mac (`arm64`), but desktop release
artifacts are produced remotely by GitHub Actions. Treat local desktop packaging
scripts as diagnostic/recovery tools only.

Desktop updater correctness requires:
Desktop updater correctness requires, on macOS:

- `latest-mac.yml`
- one arm64 ZIP and one x64 ZIP referenced by that file
- one arm64 DMG and one x64 DMG
- no universal ZIP in the updater feed

and, when `ADE_WINDOWS_PUBLIC_RELEASE_ENABLED` is `1`, additionally on Windows:

- `latest.yml`
- one `ADE-<VERSION>-win-x64.exe` installer referenced by that file
- the matching `ADE-<VERSION>-win-x64.exe.blockmap`

Windows builds fresh on the tag alongside macOS. There is one repository
variable, `ADE_WINDOWS_PUBLIC_RELEASE_ENABLED`; it decides whether the release
carries Windows at all. Read it before verifying assets, because it determines
which of the two asset matrices below is correct.

## State and Locking

Create a state file before mutating release state:
Expand All @@ -80,7 +98,7 @@ Track:

```json
{
"desktop": { "needed": false, "version": null, "tag": null, "lastTag": null },
"desktop": { "needed": false, "version": null, "tag": null, "lastTag": null, "platforms": null },
"ios": { "needed": false, "marketingVersion": null, "buildNumber": null, "lastTag": null },
"phase": "detect|docs|desktop|ios|verify|done|blocked",
"notes": []
Expand Down Expand Up @@ -124,6 +142,7 @@ relevant preflights pass.
```bash
test -f .github/workflows/release.yml
test -f .github/workflows/release-core.yml
test -f .github/workflows/release-publish.yml
gh workflow view release.yml --repo arul28/ADE
```

Expand All @@ -132,12 +151,31 @@ relevant preflights pass.

- `.github/workflows/release-core.yml` builds `dist:mac:arm64:signed`.
- `.github/workflows/release-core.yml` builds `dist:mac:x64:signed`.
- `.github/workflows/release-core.yml` builds `dist:win:signed` in
`build-win-release`.
- The publish job merges per-arch manifests into one `latest-mac.yml`.
- The publish job attaches the Windows installer, its `.blockmap`, and
`latest.yml` when the Windows gate is on.

If the workflow has been changed to publish universal updater ZIPs, stop and
fix the workflow before releasing.

7. For iOS releases, preflight App Clip packaging metadata before archiving:
7. For desktop releases, resolve the expected platform matrix before tagging.
This decides what the draft must contain in Phase 4:

```bash
gh variable get ADE_WINDOWS_PUBLIC_RELEASE_ENABLED --repo arul28/ADE 2>/dev/null || echo "unset"
```

- `1` means the release must carry macOS **and** Windows assets. Record
`platforms=mac,win`.
- Anything else, including unset, means macOS only. Record `platforms=mac`.

Windows signing is fail-closed: if the gate is `1` and the signing secrets
are missing, the `verify` job stops the run in about a minute. Do not
"fix" that by clearing the gate mid-release; fix the secrets or stop.

8. For iOS releases, preflight App Clip packaging metadata before archiving:

```bash
xcodebuild -showBuildSettings \
Expand Down Expand Up @@ -333,9 +371,18 @@ Expected shape:

- runtime/resource jobs run first
- `arm64 mac release` and `x64 mac release` build/sign/notarize independently
- `publish-release` merges the per-arch updater manifests and creates the draft
- `build-win-release` builds/signs/validates Windows independently, in parallel
with the mac jobs, when `platforms` includes `win`. With the gate off it is
skipped, and a skipped Windows job does not block the mac release.
- `publish-release` (in `release-publish.yml`, called by `release.yml` after
`run-release` succeeds) merges the per-arch updater manifests and creates the
draft
- `update-brew-tap` runs after publication

If `platforms=mac,win` and `build-win-release` did not run, stop. The gate and
the run disagree, and publishing would ship a macOS-only release under a
version that is supposed to carry Windows.

### Retry policy

Do not start duplicate full release workflows.
Expand Down Expand Up @@ -376,13 +423,51 @@ gh release download "v<VERSION>" --repo arul28/ADE \
cat ".ade/tmp/release-v<VERSION>-verify/latest-mac.yml"
```

Required assets:
When `platforms` includes `win`, also pull the Windows updater feed:

```bash
gh release download "v<VERSION>" --repo arul28/ADE \
--pattern latest.yml \
--dir ".ade/tmp/release-v<VERSION>-verify" \
--clobber
cat ".ade/tmp/release-v<VERSION>-verify/latest.yml"
```

Required assets, always:

- `ADE-<VERSION>-arm64.dmg`
- `ADE-<VERSION>-arm64.zip`
- `ADE-<VERSION>-x64.dmg`
- `ADE-<VERSION>-x64.zip`
- `latest-mac.yml`
- `install.sh`
- `SHA256SUMS`
- `ade-darwin-arm64`, `ade-darwin-x64`, `ade-linux-arm64`, `ade-linux-x64`, and
the matching `.native.tar.gz` for each

Required additionally when `platforms` includes `win`:

- `ADE-<VERSION>-win-x64.exe`
- `ADE-<VERSION>-win-x64.exe.blockmap`
- `latest.yml`
- `install.ps1`
- `ade-win32-x64.exe`
- `ade-win32-x64.native.tar.gz`

Gate/asset agreement is a hard check, in both directions:

```bash
WINDOWS_GATE="$(gh variable get ADE_WINDOWS_PUBLIC_RELEASE_ENABLED --repo arul28/ADE 2>/dev/null || echo unset)"
WINDOWS_ASSETS="$(gh release view "v<VERSION>" --repo arul28/ADE --json assets \
--jq '[.assets[].name | select(test("win-x64|win32-x64|^latest\\.yml$|^install\\.ps1$"))] | length')"
echo "gate=$WINDOWS_GATE windows_assets=$WINDOWS_ASSETS"
```

- `gate=1` and `windows_assets=0` means the Windows build silently did not
contribute. Stop; keep the release draft/private.
- `gate` not `1` and `windows_assets` greater than `0` means Windows assets
reached a release that was not supposed to carry them. Stop; keep the release
draft/private.

Also verify:

Expand All @@ -391,6 +476,12 @@ Also verify:
- no updater ZIP is suspiciously huge; a ZIP over about 900 MB needs human
review because Squirrel.Mac can crash while handling oversized updater ZIPs.
- every `latest-mac.yml` referenced ZIP exists in the release assets.
- when Windows is in scope, `latest.yml` references the uploaded
`ADE-<VERSION>-win-x64.exe`, and that installer and its `.blockmap` both
exist in the release assets.
- `SHA256SUMS` lists every published standalone runtime asset, including the
`ade-win32-x64` entries when Windows is in scope, and lists nothing that is
not published.

### Publish public/latest

Expand Down Expand Up @@ -597,6 +688,15 @@ Desktop:
until fixed.
- If `latest-mac.yml` references a universal ZIP, keep the release draft/private
and fix the GitHub workflow. Do not publish the release.
- If `latest.yml` is missing, or references an installer that is not in the
release assets, keep the release draft/private. Windows in-app update reads
that file; a broken feed strands installed Windows users.
- If the Windows build fails, the draft is not created at all while the gate is
on, by design. Fix the failure and rerun; do not clear
`ADE_WINDOWS_PUBLIC_RELEASE_ENABLED` to force a macOS-only draft under a
version that was announced as carrying Windows.
- If the Windows gate and the published Windows assets disagree in either
direction, keep the release draft/private and reconcile before publishing.

iOS:

Expand All @@ -613,8 +713,12 @@ iOS:
Report:

- desktop scope decision and tag
- the resolved desktop platform matrix (`mac` or `mac,win`) and the
`ADE_WINDOWS_PUBLIC_RELEASE_ENABLED` value it came from
- GitHub Release URL and asset count
- whether `latest-mac.yml` references only present assets
- when Windows is in scope, whether `latest.yml` references only present assets
and whether the gate and the published Windows assets agreed
- iOS marketing/build number
- TestFlight build ID
- group membership verification
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ on:
branches: [main]
pull_request:
branches: [main]
# Stacked PRs target their parent branch, not main, so the filter above
# never fires for them and they accumulate no ci-pass check run. That
# matters because release-core.yml's verify job requires ci-pass on the
# exact SHA it builds, which made a signed proof build of a stacked
# branch impossible. Dispatching by ref produces the same check runs on
# the same SHA, so the release gate stays honest rather than relaxed.
workflow_dispatch:

permissions:
contents: read
Expand Down Expand Up @@ -62,7 +69,27 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# On pull_request the action scans only the PR range. On workflow_dispatch
# it scans the entire history instead, which re-reports every already
# accepted false positive: gitleaks fingerprints are commit-scoped, so a
# finding waived in .gitleaksignore reappears under the SHA of every later
# commit that touched the same line. Scanning main..HEAD gives a dispatched
# run exactly the scope a PR run has, so the same code is judged the same
# way however CI was started.
# gitleaks-action exposes no way to narrow its commit range, so a
# dispatched run invokes gitleaks directly with the same pinned version
# the action uses. --log-opts limits the walk to this branch's own
# commits, matching what a pull_request run would scan.
- name: Scan this branch's own commits
if: github.event_name == 'workflow_dispatch'
run: |
set -euo pipefail
git fetch --no-tags origin main
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v8.24.3/gitleaks_8.24.3_linux_x64.tar.gz" \
| tar -xz gitleaks
./gitleaks detect --redact -v --exit-code=2 --log-opts="origin/main..HEAD"
- uses: gitleaks/gitleaks-action@v2
if: github.event_name != 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand Down
43 changes: 42 additions & 1 deletion .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: Prepare release

# Platform-neutral, non-publishing validation run. It never creates or updates a
# GitHub Release. Set windows_proof when the run is being used to collect the
# signed Windows exact-SHA proof; the Windows-specific preconditions are then
# asserted before anything is built.

on:
workflow_dispatch:
inputs:
Expand All @@ -11,6 +16,15 @@ on:
description: Exact 40-character commit SHA on main to validate.
required: true
type: string
windows_proof:
description: >-
Collect clean-host Windows proof evidence. Builds and signs Windows
even while ADE_WINDOWS_PUBLIC_RELEASE_ENABLED is off, and emits the
exact-SHA proof bundle. Leave false for the ordinary dry run, which
still builds Windows whenever the publication gate is on.
required: false
default: false
type: boolean

permissions:
actions: read
Expand All @@ -24,6 +38,33 @@ jobs:
tag_name: ${{ steps.resolve.outputs.tag_name }}
target_sha: ${{ steps.resolve.outputs.target_sha }}
steps:
# Opt-in evidence collection. It never depends on a repository variable
# state, so proof can be collected before Windows is enabled and again as
# a regression check after it is. This workflow never publishes either way.
- name: Prepare signed Windows proof mode
if: ${{ inputs.windows_proof }}
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
WINDOWS_SIGNING_EXPECTED_SUBJECT: ${{ secrets.WINDOWS_SIGNING_EXPECTED_SUBJECT }}
WINDOWS_SIGNING_EXPECTED_THUMBPRINT: ${{ secrets.WINDOWS_SIGNING_EXPECTED_THUMBPRINT }}
run: |
set -euo pipefail

if [ -z "$AZURE_TENANT_ID" ] || [ -z "$AZURE_CLIENT_ID" ] || [ -z "$AZURE_CLIENT_SECRET" ]; then
echo "::error::Signed Windows proof requires the AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET secrets."
exit 1
fi
if [ -z "$WINDOWS_SIGNING_EXPECTED_SUBJECT" ]; then
echo "::error::Signed Windows proof requires the WINDOWS_SIGNING_EXPECTED_SUBJECT secret."
exit 1
fi
if [ -n "$WINDOWS_SIGNING_EXPECTED_THUMBPRINT" ]; then
echo "::error::WINDOWS_SIGNING_EXPECTED_THUMBPRINT is not supported by the Azure Artifact Signing pipeline. Delete the secret and pin WINDOWS_SIGNING_EXPECTED_SUBJECT instead."
exit 1
fi

- uses: actions/checkout@v4
with:
ref: ${{ inputs.target_sha }}
Expand Down Expand Up @@ -74,5 +115,5 @@ jobs:
with:
release_tag: ${{ needs.resolve.outputs.tag_name }}
target_ref: ${{ needs.resolve.outputs.target_sha }}
publish: false
windows_proof: ${{ inputs.windows_proof }}
secrets: inherit
Loading
Loading