Skip to content

chore(release): 0.1.5 #6

chore(release): 0.1.5

chore(release): 0.1.5 #6

Workflow file for this run

name: Release
# Signed, published release build for all three platforms, triggered by a
# version tag (e.g. `v0.1.0`). Artifacts are published to GitHub Releases as a
# DRAFT (see `releaseType: draft` in packages/noodl-editor/package.json), so a
# human confirms and publishes before anything reaches users or the auto-update
# feed. See dev-docs/guidelines/RELEASE-PROCESS.md.
#
# Signing/notarisation are gated on repository secrets. If a secret is absent the
# build still succeeds but that platform's artifact is UNSIGNED (macOS: not
# notarised → Gatekeeper warns; Windows: SmartScreen blocks). The draft gate
# means an unsigned artifact can never be shipped by accident — a human sees it
# first. Required secrets are documented in RELEASE-PROCESS.md.
on:
push:
tags:
- 'v*.*.*'
# Manual dispatch for dry-runs. Publishes a draft from whatever ref is chosen;
# the release is named from package.json's version, not the ref.
workflow_dispatch:
concurrency:
# One release run at a time; do NOT cancel in progress — a half-published
# release is worse than a slow one.
group: release
cancel-in-progress: false
permissions:
contents: write # electron-builder needs this to create/update the GitHub Release.
jobs:
release:
name: ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: linux-x64
- os: windows-latest
platform: win32-x64
- os: macos-latest
platform: darwin-arm64
# macos-13, the previous Intel image, was retired by GitHub (Dec 2025);
# jobs targeting it queue for 24h and are cancelled — that is what
# "hung" the v0.1.0 darwin-x64 leg. macos-15-intel is the supported
# Intel continuation, keeping natives (dugite git, trampoline) x64.
- os: macos-15-intel
platform: darwin-x64
steps:
- uses: actions/checkout@v4
# electron-builder shells out to python on some paths.
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: ./.github/actions/setup
- name: Build viewer bundles
run: npm run build:editor:_viewer
env:
WORKSPACE_PATH: .
# Everything electron-builder copies via `extraResources`: the backend
# service and the two MCP servers. This path does NOT go through
# scripts/build-editor.ts (which is where those builds used to live), and
# electron-builder only *warns* when an extraResources source is missing —
# so without this step the published app shipped without them and the
# build was green. `build:sidecars` ends in `check-build-artefacts.js
# --built`, which fails the release instead. (MCP-002)
- name: Build packaged sidecars
run: npm run build:sidecars
- name: Build, sign, and publish the editor
run: npm run build:editor:_editor
env:
WORKSPACE_PATH: .
TARGET_PLATFORM: ${{ matrix.platform }}
# Turn signing ON and publishing ON for the release path.
DISABLE_SIGNING: false
PUBLISH_RELEASE: true
# Token electron-builder uses to create/update the draft GitHub Release.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# --- macOS signing (Developer ID) + notarisation ---
# CSC_LINK: base64-encoded .p12, CSC_KEY_PASSWORD: its password.
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# --- Windows signing ---
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
# The two macOS legs each publish a `latest-mac.yml`; whichever uploads
# last wins, so the auto-update feed would only serve one architecture
# (REV-007 known issue; DEBT-007). Preserve each leg's feed file as a
# build artifact so the merge job below can combine them.
# `if-no-files-found: error` is deliberate and costs no false reds: if the
# build step above failed, this step is skipped entirely (the job is
# already red). It fires only when the build *succeeded* and produced no
# feed — which the merge job below would then quietly paper over.
- name: Save per-arch mac update feed
if: startsWith(matrix.platform, 'darwin-')
uses: actions/upload-artifact@v4
with:
name: mac-feed-${{ matrix.platform }}
path: packages/noodl-editor/dist/latest-mac.yml
if-no-files-found: error
# Rebuild latest-mac.yml so its `files` list covers BOTH architectures —
# electron-updater picks the entry matching the running arch. Only meaningful
# on tag pushes (dispatch dry-runs may have no release to upload to).
merge-mac-update-feed:
name: merge mac update feed
needs: release
if: always() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download per-arch feeds
uses: actions/download-artifact@v4
with:
pattern: mac-feed-*
path: feeds
- name: Merge feeds
id: merge
run: |
python3 - <<'EOF'
import glob, sys, yaml
paths = sorted(glob.glob('feeds/*/latest-mac.yml'))
if len(paths) < 2:
# This used to exit 0. A green "nothing to merge" on a tag build is
# the quietest possible way to ship a single-architecture update
# feed: every Mac on the other arch stops receiving updates, with
# no symptom on the machine that cut the release.
print(f'ERROR: only {len(paths)} per-arch mac feed(s) found; both are required.')
print('One of the darwin legs did not produce a latest-mac.yml. Read its log before retagging.')
sys.exit(1)
feeds = [yaml.safe_load(open(p)) for p in paths]
merged = feeds[0]
seen = {f['url'] for f in merged.get('files', [])}
for other in feeds[1:]:
for f in other.get('files', []):
if f['url'] not in seen:
merged['files'].append(f)
seen.add(f['url'])
with open('latest-mac.yml', 'w') as out:
yaml.safe_dump(merged, out, sort_keys=False)
print(open('latest-mac.yml').read())
open('merged.flag', 'w').write('yes')
EOF
- name: Upload merged feed to the draft release
run: |
if [ -f merged.flag ]; then
gh release upload "${GITHUB_REF_NAME}" latest-mac.yml --clobber --repo "${GITHUB_REPOSITORY}"
else
echo "Skipping upload - merged feed not produced."
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# F73's general lesson, made into a job: **electron-builder uploads as it goes,
# so a draft with files in it is not evidence of a green run.** The v0.1.0
# Linux leg uploaded its AppImage and then aborted on the `.deb`; the release
# was read as "succeeded, no update feed" for over a week. `fail-fast: false`
# means a failed leg does go red, and that is the only signal there was — one
# nobody looks at when the draft in front of them has files in it.
#
# `if: always()` is load-bearing: the point is to run precisely when something
# above failed, and name what is missing from the draft rather than leaving it
# to be discovered on a tester's machine.
verify-release-assets:
name: verify draft release is complete
needs: [release, merge-mac-update-feed]
if: always() && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Fetch the draft's asset list and update feed
run: |
gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --json assets > assets.json
# Best-effort: a missing feed is one of the things being asserted, so
# a failed download must not fail the step before the check runs.
gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \
--pattern 'latest-mac.yml' --clobber || echo "latest-mac.yml not downloadable"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Assert every expected artifact is on the draft
run: node scripts/check-release-assets.js --assets assets.json --mac-feed latest-mac.yml