---
name: release-maple
description: Prepare, publish, monitor, and verify a Maple release from current master. Use when asked to bump a release version, cut or create a GitHub release, verify signed artifacts and updater metadata, monitor downstream publication, or prepare an explicitly authorized App Store, TestFlight, Google Play, or billing-API handoff.
---

# Release Maple

Treat every release step as a production action. Do not use this workflow for
routine validation. Before a write, state the exact repository, version, tag,
commit, external effect, and authority provided by the user.

## Know the triggers

- A push to `master` starts production-shaped desktop, Android, iOS, web,
  frontend, and Rust workflows. The iOS master workflow uploads its verified
  IPA to TestFlight automatically.
- `Mobile App CI` serializes production iOS build/export, verification, and
  upload so concurrent master pushes cannot reuse an export-assigned build
  number. Its master-only manual dispatch forces a fresh build even without
  app changes. After a duplicate-build rejection, dispatch this full workflow
  from master; retrying only submission reuses the rejected IPA's build number.
- The independent `Maple Dev TestFlight` workflow also builds every master
  push and uploads the distinct `cloud.opensecret.maple.dev` application for
  internal testing against the existing development services. Manual dispatch
  accepts master only. Its app identity, credentials, artifact verification,
  internal group setup, and retry contract are documented in
  [Maple Dev on TestFlight](../../../docs/ios-dev-testflight.md).
- Creating a GitHub Release starts the cross-platform release workflow. A
  successful release workflow starts separate updater-metadata, Pages, and
  best-effort Zapstore workflows. These siblings never gate or change the core
  Maple release. Pages has staged modes: `Promote Pages production` advances the
  branch for native Cloudflare builds by default; when repository variable
  `MAPLE_PAGES_PRODUCTION_ENABLED=true`, `Publish Pages` uploads the verified
  existing release web artifact instead. Read
  [the Pages publisher architecture](../../../docs/pages-deployments.md) for
  its source contract. Mode activation, environment administration, and
  Cloudflare build controls belong to a separately authorized operator procedure.
- The same Maple GitHub Release receives four native `maple-proxy` archives and
  their checksum manifest. Never create a separate proxy Release or proxy tag;
  `/releases/latest` must continue to identify the Maple application release.
- Maple GitHub Releases do not publish `maple-sdk` or `maple-proxy` to
  crates.io. A successful stable release starts a non-gating GHCR sibling that
  builds only for a new or missing non-baseline proxy version. It treats exact
  version tags as immutable, verifies release provenance and container inputs,
  and repairs minor, major, and `latest` aliases without rebuilding an existing
  exact image. Version `0.3.3` is the explicit unbackfilled migration baseline.
  The container remains separately versioned at
  `ghcr.io/mapleprivacylabs/maple-proxy`.
- GitHub Release creation does not itself submit the release IPA or AAB to
  Apple App Store review or Google Play.

Never push or merge `master`, create a release, retry a workflow, upload to a
store, submit for review, or alter a rollout merely to see whether it works.

## Repository-transfer checkpoint

Before creating a new release, verify that `repo.meta.json` identifies
`MaplePrivacyLabs/Maple` as the canonical repository. Keep the existing updater
Worker serving its deployed metadata until the first normal new-org release.
Do not manually republish retained v3.3.10 updater metadata: its asset URLs use
`OpenSecretCloud/Maple`, while the publisher and new Worker correctly require
the current canonical owner. The next release generates new-owner URLs without
changing the legacy updater fallback compiled into existing clients.

The first eligible proxy container publication initializes
`ghcr.io/mapleprivacylabs/maple-proxy` with a manual `Publish proxy container`
run from `master` and `bootstrap_only=true`. This validates the current release
and uploads only content-addressed images, without version or alias tags. It
does not treat failed package enumeration as an empty inventory. GitHub creates
new packages privately; make that package public in its settings, retain Maple
Actions write access, then rerun only the proxy publisher with both manual
inputs disabled. For read-only API diagnosis, use `diagnostics_only=true`;
its success reports completed observations, not publication readiness. Do
not create another release, change proxy versions, or overwrite exact tags to
repair package visibility. Existing old-namespace images receive no updates.

## Prepare the version

1. Prepare the bump on a clean focused branch based on current `origin/master`;
   do not switch another worktree to `master` or force its owning worktree off
   that branch. Compare the checked-in version with the latest release:

   ```bash
   git fetch origin master
   git merge-base --is-ancestor origin/master HEAD
   current_version="$(nix develop --no-update-lock-file .#ci -c just get-version | tail -n 1)"
   released_version="$(gh api repos/MaplePrivacyLabs/Maple/releases/latest --jq '.tag_name | ltrimstr("v")')"
   printf 'current=%s released=%s\n' "$current_version" "$released_version"
   ```

2. If `current_version` is newer, retain that pending version unless the user
   explicitly chooses a different target. For example, a requested `3.4.0`
   replaces a pending `3.3.11`; a generic request to release does not trigger
   another bump. The chosen target must still be newer than the published release.
3. If versions are equal, establish the intended next version. Proceed when
   the user names an exact version or patch/minor/major level. If the user
   delegates the choice, use patch; do not infer minor or major from commits.
4. On a focused branch, run exactly one repository helper:

   ```bash
   nix develop --no-update-lock-file .#ci -c just update-version X.Y.Z
   nix develop --no-update-lock-file .#ci -c just bump-patch
   nix develop --no-update-lock-file .#ci -c just bump-minor
   nix develop --no-update-lock-file .#ci -c just bump-major
   ```

5. Review all manifest, Apple project, Android version-code, and
   `apps/maple-research/frontend/src-tauri/Cargo.lock` changes. Run the applicable Maple validation
   gates and submit the isolated bump through normal review when authorized.
   Do not use `just release`; it creates a local tag before the reviewed GitHub
   flow.
6. After the bump merges, use or create a clean worktree on `master`. If another
   worktree already owns that branch, use its checkout instead of forcing or
   stealing it. Pull with `--ff-only` and wait for the required CI evidence.
   Release only current master; preflight verifies it again.

## Run preflight

Run the bundled fail-closed preflight from the repository root:

```bash
preflight="$(.agents/skills/release-maple/scripts/preflight.sh)"
printf '%s\n' "$preflight" | jq .
tag="$(printf '%s' "$preflight" | jq -r .tag)"
previous_tag="$(printf '%s' "$preflight" | jq -r .previous_tag)"
head_sha="$(printf '%s' "$preflight" | jq -r .head_sha)"
```

The script requires a clean current `master`, exact manifest version parity, a
newer version and unused tag, and successful executed master-push CI jobs.
CodeQL must run on the exact release commit. Research test/build evidence may
come from an ancestor only when the complete Git tree diff contains exclusively
independent Agent, documentation, agent guidance, or release-gate test-harness
changes. Unknown paths and changes to Research, SDK, proxy, shared scripts,
workflows, or Nix require fresh evidence. Skipped builds never count as build
proof; the helper searches a bounded workflow history and rejects newer failures,
pending runs, missing jobs, changed inputs, and non-ancestors. The returned
`ci_evidence` records each actual run, tested SHA, and whether it was reused.
Preflight also rechecks master after collecting evidence. Stop on any failure; correct it through the normal reviewed process.
Never overwrite or move a release tag.

Review the [SDK consumer version policy](../../../docs/sdk-publishing.md#consumer-version-policy)
for the clients being released. Record the frontend's selected SDK from its
manifest/lockfile and each Rust consumer's resolved version/source using the
policy's `cargo metadata --locked` command. Local links are allowed. If those
clients will ship unpublished SDK changes, recommend publishing the SDK and
pinning those consumers first; an intentional local-source release can proceed
with the exact monorepo commit recorded. Unrelated SDK source changes do not
require a pinned client to upgrade, and this preference adds no release gate.

When preparing an enclave trust or PCR rotation change, review both browser
consumers' embedded fallbacks against the approved development and production
histories: `apps/maple-research/frontend/src/config/openSecretClientConfig.ts`
and `apps/maple-auth/src/config/openSecretClientConfig.ts`. Verify each affected
app's combined app-provided and pinned-SDK roots support its intended approved
enclave measurements when signed-history fetching is unavailable, preserving
development/production separation. Record affected artifacts and any pending rollout
in the handoff. Auth owns a separate SDK pin and publisher: refreshing Research
does not update Auth, and an Auth publication remains a separately authorized
operation under [the Pages guide](../../../docs/pages-deployments.md#independent-auth-site).
This is a compatibility review of each consumer, not a requirement to keep their
lists byte-identical or release them together.

Record the proxy version and inspect its own runtime inputs since `previous_tag`:

```bash
proxy_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' proxy/Cargo.toml | head -n 1)"
git diff --name-only "$previous_tag".."$head_sha" -- proxy
printf 'proxy_version=%s\n' "$proxy_version"
```

Inspect the proxy's manifest and lockfile at both revisions. Include changes
under `sdk/rust` in its runtime comparison when either revision uses that local
SDK. With registry dependencies at both revisions, compare the selected SDK
versions/checksums in `proxy/Cargo.lock`; unrelated `sdk/rust` edits are not
proxy runtime changes. The embedding app's SDK selection is separate from the
standalone proxy's lockfile.

If consumed runtime inputs changed without a proxy version change, stop and
make the version decision explicit before publishing. A normal Maple Release
always builds the checked-in proxy version, but that does not implicitly
authorize a crates.io or GHCR publish.

Preview GitHub's generated notes:

```bash
gh api --method POST repos/MaplePrivacyLabs/Maple/releases/generate-notes \
  -f tag_name="$tag" \
  -f target_commitish="$head_sha" \
  -f previous_tag_name="$previous_tag" | jq -r '.name, .body'
```

Confirm the notes span the intended changes and recheck that `head_sha` is
still `origin/master`. GitHub's generated body is changelog input, not a
complete public release description. Draft a concise user-facing summary and
highlights from the exact release diff, place them above the generated notes,
and review the complete Markdown in a temporary `notes_file`. Do not publish a
PR-list-only description when the release has meaningful product changes.
Present the tag, commit, previous tag, and final notes to the user before
creating the release unless the current request already gives unambiguous
authority for that exact release.

## Publish once

Create the GitHub Release exactly once. This creates the tag in the same flow:

```bash
gh release create "$tag" \
  --repo MaplePrivacyLabs/Maple \
  --target "$head_sha" \
  --title "$tag" \
  --notes-file "$notes_file"
```

Do not create or push a local tag first. Record the release URL and confirm the
release and workflow resolve to `head_sha`.

## Monitor release CI

Find and watch the new `Release` run:

```bash
gh run list --repo MaplePrivacyLabs/Maple --workflow Release --event release \
  --commit "$head_sha" --limit 10 \
  --json databaseId,displayTitle,headSha,status,conclusion,url

gh run watch RELEASE_RUN_ID \
  --repo MaplePrivacyLabs/Maple --exit-status --compact
```

Stay with every platform build, signature/canonical proof, artifact upload,
the four native proxy builds and their published-asset verification, updater
`latest.json`, aggregate verification, and verification-guide step.
Packaging success alone is not runtime smoke; inspect the workflow's actual
verification and attestation results.

After `Release` succeeds, inspect the two required publication handoffs. Do
not rerun the core Release to repair either sibling:

```bash
gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish updater metadata' \
  --limit 10 \
  --json databaseId,status,conclusion,headSha,createdAt,url

pages_workflow='Promote Pages production'
pages_enabled="$(gh variable list --repo MaplePrivacyLabs/Maple --json name,value \
  --jq '.[] | select(.name == "MAPLE_PAGES_PRODUCTION_ENABLED") | .value')" || exit 1
if [ "$pages_enabled" = true ]; then
  pages_workflow='Publish Pages'
fi
gh run list --repo MaplePrivacyLabs/Maple --workflow "$pages_workflow" \
  --limit 10 \
  --json databaseId,status,conclusion,headSha,createdAt,url
```

Both publishers execute trusted master, which can be newer than the release.
Identify the updater run by its validated release tag and source, and the Pages
run by its production job's validated source SHA. Do not filter either by the
workflow checkout SHA; preview publication uses the same Pages workflow name.

Also inspect the independent proxy-container publisher. It should either prove
the expected immutable proxy version, public AMD64/ARM64 manifest, per-platform
provenance, and aliases; publish a missing eligible version; or explicitly skip
the unbackfilled `0.3.3` baseline. Retry it with manual dispatch; never create a
proxy tag or Release and never rerun the core Release to repair it:

```bash
gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish proxy container' \
  --limit 10 \
  --json databaseId,status,conclusion,headSha,createdAt,url
```

The updater workflow must publish the original verified `latest.json` and its
matching Research installer catalog before reporting distribution current.
Its public check proves byte-identical Tauri metadata and all six GET/HEAD
installer redirects; marketing follows the stable URLs without a redeploy.
Treat incomplete assets, provenance failures and blocked download routes as
publication failures, not reasons to alter updater metadata or weaken checks.
Use the [updates service guide](../../../services/updates/README.md) for its
one-time rollout and recovery boundaries. In legacy Pages mode, a successful
promoter proves only the `pages-production` ref mutation; verify Cloudflare's
separate build result. In owned-publisher mode, require the `production` job in
`Publish Pages` to succeed: it validates the release asset, uploads without a
rebuild, checks CF stage/commit/active canonical deployment, and then advances
the ref without force. Neither result proves browser login/chat or configuration.
Report and repair sibling failures without altering completed release artifacts.

Confirm the production ref in either mode:

```bash
pages_sha="$(gh api repos/MaplePrivacyLabs/Maple/git/ref/heads/pages-production --jq .object.sha)"
[[ "$pages_sha" == "$head_sha" ]]
```

In legacy mode only, inspect Cloudflare's exact-commit check:

```bash
gh api "repos/MaplePrivacyLabs/Maple/commits/$head_sha/check-runs" --jq '
  [.check_runs[]
   | select(.name == "Cloudflare Pages")
   | select(.app.name == "Cloudflare Workers and Pages")
   | {status, conclusion, started_at, completed_at, details_url}]'
```

Require the successful Cloudflare check for the production-branch promotion,
not an older preview check on the same commit. In owned-publisher mode, inspect
the `Publish Pages` production job summary and GitHub deployment instead; the
old Cloudflare App check is no longer produced by this path. The summary names
the deployment ID and SHA. A failed post-upload freshness/ref/status check can
leave a new CF deployment already active: inspect actual canonical state before
retrying, and follow the deployment guide's hold/rollback procedure.

A raw `curl` from an automated VM may be denied by edge policy. An allowed-browser
smoke is separate application evidence; do not turn an edge-policy 403 into a
release failure. Once owned publishing is enabled, manually dispatch
`Publish Pages` from master to retry the current verified stable release; never
recreate a Release or rerun the core release merely to repair Pages.

On failure, read the failed logs before acting:

```bash
gh run view RELEASE_RUN_ID --repo MaplePrivacyLabs/Maple --log-failed
```

Retry only a terminal failure proven to be transient infrastructure trouble:

```bash
gh run rerun RELEASE_RUN_ID --repo MaplePrivacyLabs/Maple --failed
```

Do not classify version/proof mismatches, deterministic builds, signing
failures, missing credentials, or integrity checks as transient. Do not delete
or recreate a published release without separate explicit direction.

## Verify the release and optionally inspect Zapstore

Verify the published release and its assets:

```bash
gh release view "$tag" --repo MaplePrivacyLabs/Maple \
  --json tagName,name,isDraft,isPrerelease,publishedAt,targetCommitish,url,assets

mkdir -p artifacts
gh release download "$tag" --repo MaplePrivacyLabs/Maple --dir artifacts
nix develop --no-update-lock-file .#ci -c \
  ./scripts/ci/verify-release-artifacts.sh artifacts proxy
```

Confirm the release contains all four stable proxy archives and
`maple-proxy-release-final.sha256`, and that their attestations and the
published-asset verification job succeeded. Report the embedded proxy version
separately from the Maple application version. Do not report crates.io as
updated unless its independent manual publisher was explicitly authorized and
verified. Report GHCR as published only after its sibling workflow and anonymous
manifest verification succeed; otherwise report the unchanged-version skip or
failure separately.

Verify that the hosted updater serves the same metadata as the GitHub Release:

```bash
updater_dir="$(mktemp -d)"

curl --fail --silent --show-error --location --max-time 20 \
  https://updates.trymaple.ai/latest.json >"$updater_dir/hosted.json"
curl --fail --silent --show-error --location --max-time 20 \
  https://github.com/OpenSecretCloud/Maple/releases/latest/download/latest.json \
  >"$updater_dir/github.json"

jq -e --arg version "$version" '.version == $version' \
  "$updater_dir/hosted.json" "$updater_dir/github.json"
jq -S . "$updater_dir/hosted.json" >"$updater_dir/hosted.canonical.json"
jq -S . "$updater_dir/github.json" >"$updater_dir/github.canonical.json"
cmp "$updater_dir/hosted.canonical.json" "$updater_dir/github.canonical.json"
```

Do not report updater publication complete from workflow status alone: require
the public endpoint to return the intended version and content.

Zapstore starts only after `Release` succeeds and is strictly best effort. Its
queued, running, skipped, or failed state must not delay release completion,
trigger a release retry, or be reported as a Maple release failure. Inspect it
only when Zapstore status is specifically useful:

```bash
gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish to Zapstore' \
  --commit "$head_sha" --limit 10 \
  --json databaseId,status,conclusion,headSha,createdAt,url
```

Do not retry or repair Zapstore as part of the Maple release flow. A separate
explicit request may authorize investigating or retrying Zapstore itself.

Do not call the core repository release complete while its required `Release`
workflow is queued or running. Report updater publication and Pages production
as separate downstream states. Zapstore is not a required release workflow.

## Store and API handoff

Apple and Google actions remain manual production operations. Do not open a
store console, choose a track, add testers, upload a build, answer compliance
questions, submit for review, release an approved version, or change a rollout
without explicit authorization for that exact action.

For an authorized handoff:

1. Identify the artifact from the exact tag and commit.
2. Verify its digest, platform signature, application/bundle ID, visible
   version, build/version code, and repository release proof.
3. Record the destination application, tester group or release track,
   countries/audience, rollout choice, and any review/compliance state before
   submission.
4. After the store reports a result, distinguish upload, processing, testing,
   review, approval, rollout, and public availability. Do not infer one state
   from another.
5. If the approved client version is gated by a configured billing API, verify
   that API recognizes the exact `vX.Y.Z` version. Any service-side version-gate
   change or deployment is outside this repository and requires its own
   reviewed workflow and authority.

Keep time-specific build numbers, review outcomes, blockers, and rollout facts
in the release handoff or issue that owns them, not in this evergreen skill.

## Report

Report the Maple version, proxy version, tag, exact commit, release URL, main
workflow URL and attempt count, application and four-proxy-archive verification
results, any required-workflow retry and supporting evidence, authorized
store/API actions, and every boundary that remains unverified. State crates.io
and GHCR status separately. If Zapstore was inspected, report its status as
non-gating. Separate repository release completion from store distribution and
live application availability.
