Skip to content

Commit bca2059

Browse files
ndemiancclaude
andcommitted
docs(release): clarify the .app.zip.sha256 is local-only; fix make-dmg header
Addresses the PR #22 review. The `.sha256` question (5 of 6 comments): every upload instruction listed the dmgs and zips but not the `.sha256` files make-dmg.sh writes, leaving it ambiguous whether S2 needs them published to resolve `sha256hash`. It does not. `EditorReleaseFeed#build_assets` reads the hash from GitHub's own API-computed asset `digest` ("sha256:<hex>"); no code path fetches a sidecar, and the exact-filename asset match means a `.sha256` asset would be ignored anyway. So the sidecars are a local verification convenience, and the fix is to say that explicitly rather than to start publishing them — stated now in make-dmg.sh's step 5, the script's own output, RELEASING.md §4/§5 and the hybrid flow, AUTO-UPDATE.md's gap list, and the CI draft-release instructions. RELEASING.md §5 also gains a verified command for using the file. Also in make-dmg.sh's header: - The step list ran 1-4 while the body is marked 0-5, and skipped the .dmg signing/notarizing step entirely. Renumbered to match the body one-for-one. - "The result is UNNOTARIZED" was stated unconditionally, but on the Developer ID path the script notarizes and staples both the app and the dmg. Scoped to the ad-hoc path, with the signed path's behaviour spelled out. Verified: bash -n passes, release.yml still parses (3 jobs), the header's 0-5 now matches the body's markers exactly, and the sidecar claim was re-checked against editor_release_feed.rb. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 54e364a commit bca2059

4 files changed

Lines changed: 47 additions & 18 deletions

File tree

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ jobs:
109109
(signs → notarizes → staples → `LevelCode-<arch>.dmg` **and** `LevelCode-<arch>.app.zip`).
110110
3. `gh release upload ${{ github.ref_name }} LevelCode-arm64.dmg LevelCode-x64.dmg LevelCode-arm64.app.zip LevelCode-x64.app.zip`
111111
— the `.dmg`s are for humans, the `.app.zip`s are the auto-update feed assets (`docs/AUTO-UPDATE.md`).
112+
Upload exactly these four; the `.app.zip.sha256` files `make-dmg.sh` writes stay **local**
113+
(the feed reads GitHub's own asset `digest`, never a sidecar).
112114
4. **Delete the `UNSIGNED-*.app.zip` assets**, add real notes, and publish.
113115
114116
Full runbook: `docs/RELEASING.md`.

docs/AUTO-UPDATE.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ then lift the guard.
3232
## The gap
3333

3434
1. **Release artifact.** Produce `LevelCode-<arch>.app.zip` from the **signed + notarized + stapled**
35-
`.app` (a `ditto` after `make-dmg.sh`'s signing step) and publish it on the release, plus its
36-
`shasum -a 256`. The `.dmg` stays — it remains the fresh-install path; the `.zip` is update-only.
35+
`.app` (a `ditto` after `make-dmg.sh`'s signing step) and publish it on the release. The `.dmg` stays —
36+
it remains the fresh-install path; the `.zip` is update-only.
37+
*The zip is the only new release asset.* `make-dmg.sh` also writes a `.app.zip.sha256` beside it, but
38+
that stays **local**: the feed's `sha256hash` comes from GitHub's API-computed asset `digest`
39+
(`"sha256:<hex>"`), so no sidecar is ever fetched. The file is for verifying by hand that the zip you
40+
published is the zip you built.
3741
2. **Feed asset resolution.** `EditorReleaseFeed#fetch_release` currently returns `url: rel["html_url"]`
3842
(the release page) and `sha256hash: nil`. It must select the **right asset** from `rel["assets"]` by
39-
arch and return its `browser_download_url` + the real hash.
43+
arch and return its `browser_download_url` plus the hash from that asset's own `digest` field.
4044
3. **Arch mapping.** Feed targets are `darwin-arm64` and `darwin` (Intel). Map to the arm64 / x64 zips
4145
respectively — **never serve a cross-arch zip**.
4246
4. **Lift the guard.** Set `LEVELCODE_UPDATE_FEED_SIGNED=1` on Elastic Beanstalk — **only after 1–3**.

docs/RELEASING.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ gh release create v0.1.0 --title "LevelCode v0.1.0" --notes "First public build.
8585
LevelCode-arm64.app.zip LevelCode-x64.app.zip
8686
```
8787
Upload **both kinds**: the `.dmg` is what humans install; the `.app.zip` is what the built-in updater
88-
installs (Squirrel takes a zip, never a dmg). See §5.
88+
installs (Squirrel takes a zip, never a dmg). `make-dmg.sh` also writes a `.app.zip.sha256` next to each
89+
zip — that one is **not** a release asset; it stays local. See §5.
8990
Stable URL: `https://github.com/levelcodeai/levelcode/releases/latest/download/LevelCode-arm64.dmg` — link it
9091
from **levelcode.ai/download**.
9192

@@ -102,9 +103,18 @@ Two assets, **not** interchangeable:
102103
| `LevelCode-<arch>.dmg` | Humans — fresh install, drag to Applications. |
103104
| `LevelCode-<arch>.app.zip` | The built-in **Squirrel** updater — it installs from a zip, never a dmg. |
104105

105-
`make-dmg.sh` emits the `.app.zip` (+ a `.sha256`) **only on the Developer-ID path**, because Squirrel
106-
refuses an update whose signing identity doesn't match the running app — an ad-hoc build must never be
107-
served as an update.
106+
`make-dmg.sh` emits the `.app.zip` **only on the Developer-ID path**, because Squirrel refuses an update
107+
whose signing identity doesn't match the running app — an ad-hoc build must never be served as an update.
108+
109+
**Only those two files get uploaded per arch.** `make-dmg.sh` also writes `LevelCode-<arch>.app.zip.sha256`,
110+
but that is a **local verification convenience, not a release asset**: the feed reads `sha256hash` from
111+
GitHub's own API-computed asset `digest` (`"sha256:<hex>"`), so nothing ever fetches a sidecar file.
112+
Uploading one is harmless — `EditorReleaseFeed` matches assets by exact filename and ignores anything
113+
else — just unnecessary. Use it to confirm the zip you published is the zip you built:
114+
```bash
115+
Z=LevelCode-arm64.app.zip
116+
[ "$(shasum -a 256 "$Z" | cut -d' ' -f1)" = "$(cat "$Z.sha256")" ] && echo "$Z OK" || echo "$Z MISMATCH"
117+
```
108118

109119
`LEVELCODE_UPDATE_FEED` (env JSON on the server) overrides the GitHub lookup and is the **rollback pin**:
110120
point it at the previous commit to stop a bad release propagating. Note it can't un-update anyone who
@@ -153,7 +163,8 @@ for A in arm64 x64; do
153163
NOTARY_PROFILE=levelcode-notary ./scripts/make-dmg.sh "$A" # → LevelCode-$A.dmg + LevelCode-$A.app.zip
154164
done
155165

156-
# 3. Verify (§3), then attach the dmgs AND the update zips, drop the unsigned zips, and publish
166+
# 3. Verify (§3), then attach the dmgs AND the update zips, drop the unsigned zips, and publish.
167+
# The .app.zip.sha256 files stay local on purpose — the feed uses GitHub's own asset digest (§5).
157168
gh release upload v0.1.0 LevelCode-arm64.dmg LevelCode-x64.dmg \
158169
LevelCode-arm64.app.zip LevelCode-x64.app.zip
159170
# `gh release delete-asset` takes ONE asset per call — drop each unsigned zip separately (`-y` skips the prompt).

scripts/make-dmg.sh

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,24 @@
66
# loose files and it can't be "installed". A .dmg wraps the whole app into one
77
# compressed file you can upload, download, and drag-to-install.
88
#
9-
# This script:
10-
# 1. Locates VSCode-darwin-<arch>/LevelCode.app (run ./scripts/build-macos.sh first).
11-
# 2. Ad-hoc code-signs it. arm64 macOS refuses to launch *unsigned* binaries at all,
12-
# so even for personal use the app must carry at least an ad-hoc signature.
13-
# 3. Builds a compressed .dmg containing the app + an /Applications symlink.
14-
# 4. On the Developer ID path only, also emits LevelCode-<arch>.app.zip (+ .sha256) — the
15-
# auto-update feed asset the built-in Squirrel updater installs. See docs/AUTO-UPDATE.md.
9+
# It operates on VSCode-darwin-<arch>/LevelCode.app (run ./scripts/build-macos.sh first).
10+
# Steps below are numbered to match the `# N.` markers in the script body:
11+
# 0. De-Microsoft the bundle + hide not-yet-ready features, before signing covers the result.
12+
# 1. Sign the app. Ad-hoc by default (arm64 macOS refuses to launch *unsigned* binaries at all);
13+
# with CODESIGN_IDENTITY set, real Developer ID signing + notarize + staple.
14+
# 2. Stage a clean folder: the app + an /Applications symlink.
15+
# 3. Build the compressed .dmg from it.
16+
# 4. Developer ID path only — sign, notarize + staple the .dmg itself.
17+
# 5. Developer ID path only — emit LevelCode-<arch>.app.zip, the auto-update feed asset the
18+
# built-in Squirrel updater installs. See docs/AUTO-UPDATE.md.
1619
#
17-
# The result is UNNOTARIZED. On another Mac, Gatekeeper will still warn on first launch:
20+
# Gatekeeper: on the DEFAULT (ad-hoc) path the result is UNNOTARIZED, so on another Mac Gatekeeper
21+
# warns on first launch:
1822
# - Right-click the app > Open (once), OR
1923
# - clear quarantine after copying out of the dmg:
2024
# xattr -dr com.apple.quarantine "/Applications/LevelCode.app"
25+
# With CODESIGN_IDENTITY set, steps 1 + 4 notarize and staple both the app and the dmg — a plain
26+
# double-click then works on any Mac, offline, with no warning.
2127
#
2228
set -euo pipefail
2329
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -105,6 +111,11 @@ fi
105111
# Developer ID doesn't match the running app — so this is produced ONLY on the signed path; an ad-hoc
106112
# build must never masquerade as an update asset. Use `ditto` (not `zip`): it preserves the stapled
107113
# notarization ticket, so the updated app still validates offline. Feed + rollout: docs/AUTO-UPDATE.md.
114+
#
115+
# The `.sha256` sidecar is LOCAL ONLY — do not upload it as a release asset. The update feed reads the
116+
# hash from GitHub's own API-computed asset `digest` ("sha256:<hex>"), not from a sidecar file; see
117+
# Levelcode::EditorReleaseFeed#build_assets in thin.ly. It exists so a releaser can verify by hand
118+
# that the zip they published is the zip they built.
108119
ZIP_OUT=""
109120
if [ "$NOTARIZE" = "1" ]; then
110121
ZIP_OUT="$ROOT_DIR/LevelCode-$ARCH.app.zip"
@@ -120,8 +131,9 @@ echo "[make-dmg] Output: $DMG_OUT ($SIZE)"
120131
if [ "$NOTARIZE" = "1" ]; then
121132
echo "[make-dmg] Signed + notarized — upload it; users just open the dmg and drag to Applications."
122133
echo "[make-dmg] Update asset: $ZIP_OUT ($(du -sh "$ZIP_OUT" | cut -f1))"
123-
echo "[make-dmg] sha256: $(cat "$ZIP_OUT.sha256")"
124-
echo "[make-dmg] Upload BOTH: the .dmg (fresh installs) and the .app.zip (auto-update feed)."
134+
echo "[make-dmg] sha256: $(cat "$ZIP_OUT.sha256") (local check only — the feed uses GitHub's own digest)"
135+
echo "[make-dmg] Upload EXACTLY these two: the .dmg (fresh installs) and the .app.zip (auto-update"
136+
echo "[make-dmg] feed). The .sha256 stays local — uploading it is harmless but pointless."
125137
else
126138
echo "[make-dmg] Upload this single file. On the other Mac: open the dmg, drag LevelCode to"
127139
echo "[make-dmg] Applications, then right-click > Open the first time (it's unnotarized)."

0 commit comments

Comments
 (0)