Skip to content

Merge pull request #27 from levelcodeai/feat/json-beautify-on-paste #17

Merge pull request #27 from levelcodeai/feat/json-beautify-on-paste

Merge pull request #27 from levelcodeai/feat/json-beautify-on-paste #17

Workflow file for this run

name: Build release apps
# Hybrid release model (see docs/RELEASING.md): CI does the heavy, awkward part — building BOTH
# macOS arches on their NATIVE runners (you can't easily build x64 on an Apple-silicon Mac) — with
# NO signing secrets. It produces unsigned .app bundles and attaches them to a DRAFT release. You
# then sign + notarize + staple LOCALLY (your Developer ID cert never leaves your machine), attach
# the notarized dmgs, delete the UNSIGNED-*.app.zip assets, and publish.
on:
push:
tags: ["v*"] # e.g. git tag v0.1.0 && git push --tags
workflow_dispatch:
inputs:
vscode_tag:
description: "Override the pinned VS Code tag (blank = scripts/bootstrap.sh default)"
required: false
default: ""
permissions:
contents: write # create/update the draft release
jobs:
# Cheap gate: run the extension unit tests before spending ~1h of macOS build minutes.
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Extension unit tests
# `shopt` is a bash builtin, so pin the shell instead of relying on the runner default (bash on
# Linux/macOS, but pwsh on Windows — where this step would break if the job were ever copied).
# Explicit `shell: bash` also upgrades the default `bash -e` to
# `bash --noprofile --norc -eo pipefail`, so a stray profile file can't perturb the gate either.
shell: bash
# DISCOVERS suites — it used to `cd extensions/levelcode-ai`, so levelcode-updater's tests never
# ran here, including the one guarding the updater's Download button against serving a raw
# .app.zip. Globbing every extension means a new suite is gated the moment it is added, with no
# list here to keep in sync. Requires are file-relative, so running from the repo root is fine.
run: |
shopt -s nullglob
count=0
for t in extensions/*/test/*.test.js; do
echo "── $t"
node "$t" # `-e` (from `shell: bash` above) aborts the job on the first failure
count=$((count + 1))
done
# A zero-match glob would otherwise report success and gate nothing — the exact failure this
# step is fixing. Fail loudly instead.
if [ "$count" -eq 0 ]; then
echo "::error::No suites matched extensions/*/test/*.test.js — the gate would pass vacuously."
exit 1
fi
echo "──────── $count test files passed ────────"
build:
name: Build ${{ matrix.arch }}
needs: test
strategy:
fail-fast: false
matrix:
include:
- { runner: macos-14, arch: arm64 } # Apple Silicon (native)
# macos-15-intel is GitHub's last native x86_64 image (macos-13 was retired 2025-12-04;
# jobs still requesting it hang forever "Waiting for a runner…" until the 24h queue timeout).
# It's a premium/large runner (bills ~2× minutes) and Intel support ends Fall 2027 — after
# that the x64 build must cross-compile on an arm64 runner. See docs/RELEASING.md §7.
- { runner: macos-15-intel, arch: x64 } # Intel (native)
runs-on: ${{ matrix.runner }}
# Authenticate npm postinstall downloads (notably @vscode/ripgrep, which fetches a prebuilt binary
# from GitHub releases). Without a token they use GitHub's 60/hr ANONYMOUS limit and 403 on busy
# release days; GITHUB_TOKEN raises that to 5000/hr. Read-only default token — no extra perms needed.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24" # bootstrap.sh checks the .nvmrc major; 24.x satisfies the 1.126 pin
- name: Cache npm downloads
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ matrix.arch }}-${{ hashFiles('scripts/bootstrap.sh') }}
restore-keys: npm-${{ matrix.arch }}-
- name: Bootstrap Code-OSS + branding + deps
run: ./scripts/bootstrap.sh
env:
VSCODE_TAG: ${{ github.event.inputs.vscode_tag }}
- name: Build LevelCode.app (${{ matrix.arch }}, proprietary stripped)
run: ./scripts/build-macos.sh ${{ matrix.arch }}
- name: Zip the unsigned app
run: ditto -c -k --sequesterRsrc --keepParent "VSCode-darwin-${{ matrix.arch }}/LevelCode.app" "UNSIGNED-LevelCode-${{ matrix.arch }}.app.zip"
- name: Upload app artifact
uses: actions/upload-artifact@v4
with:
name: UNSIGNED-LevelCode-${{ matrix.arch }}
path: UNSIGNED-LevelCode-${{ matrix.arch }}.app.zip
if-no-files-found: error
retention-days: 14
draft-release:
name: Draft release
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: apps
merge-multiple: true
- name: Create / refresh the draft release with the UNSIGNED apps
uses: softprops/action-gh-release@v2
with:
draft: true
name: LevelCode ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
files: apps/*.zip
fail_on_unmatched_files: true
body: |
**Draft — not for release as-is.** The attached `UNSIGNED-LevelCode-<arch>.app.zip`
files are CI build artifacts with **no Developer ID signature or notarization**.
To finish the release **locally** (your signing cert never touches CI):
1. `gh release download ${{ github.ref_name }} --pattern 'UNSIGNED-*.app.zip'`
2. For each arch — unzip into `VSCode-darwin-<arch>/`, then
`CODESIGN_IDENTITY="Developer ID Application: …" NOTARY_PROFILE=levelcode-notary ./scripts/make-dmg.sh <arch>`
(signs → notarizes → staples → `LevelCode-<arch>.dmg` **and** `LevelCode-<arch>.app.zip`).
3. `gh release upload ${{ github.ref_name }} LevelCode-arm64.dmg LevelCode-x64.dmg LevelCode-arm64.app.zip LevelCode-x64.app.zip`
— the `.dmg`s are for humans, the `.app.zip`s are the auto-update feed assets (`docs/AUTO-UPDATE.md`).
Upload exactly these four; the `.app.zip.sha256` files `make-dmg.sh` writes stay **local**
(the feed reads GitHub's own asset `digest`, never a sidecar).
4. **Delete the `UNSIGNED-*.app.zip` assets**, add real notes, and publish.
Full runbook: `docs/RELEASING.md`.