Skip to content

Release

Release #109

Workflow file for this run

name: Release
# Tag it, and the tap updates itself:
# pnpm version patch && git push --follow-tags
#
# Without this the formula's sha256 is hand-maintained, which is the step
# everyone forgets — and a stale sha fails the install with a checksum
# mismatch that reads like a security warning.
on:
push:
tags: ["v*"]
# And by hand, against a tag, because the push trigger cannot be relied on.
#
# push.yml tags as github-actions[bot] with the default GITHUB_TOKEN, and
# GitHub deliberately does not start workflows from a GITHUB_TOKEN push — so
# this never fired for a bot-cut tag. Sixty-two tags between v0.1.4 and v0.1.66
# produced no release and no DMGs, and nothing said so: the tap kept installing
# the last build that happened to be cut by a person.
#
# The real fix is a PAT with `contents: write` on that push step, which needs a
# secret nobody can add from here. Until then this makes it recoverable without
# deleting and re-cutting a tag, which is the one thing update-cask treats as a
# reason to fail a build.
workflow_dispatch:
inputs:
why:
description: "Why this is being run by hand"
required: false
default: "the bot-cut tag did not start this workflow"
permissions:
contents: write
packages: read
jobs:
verify:
runs-on: ubuntu-latest
env:
# Verification has to use the fork: it owns the document-open integration
# and Studio host files that the toolkit actually targets. Keep the ref
# stable so a tag cannot turn red because the fork's main moved later.
OPENSCREEN_REF: v0.0.2
# setup-node records this placeholder in the npm config. It must outlive
# install because every later pnpm command parses the same config.
NODE_AUTH_TOKEN: ${{ secrets.ROLEMODEL_PACKAGE_REPO_READ_TOKEN || github.token }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with: { version: 11.20.0 }
- uses: actions/setup-node@v7
with:
# The same Node the push workflow checks on. A release built on an
# older runtime than the one the code is verified against is a release
# nobody tested.
node-version: "24"
cache: "pnpm"
scope: "@rolemodel"
registry-url: "https://npm.pkg.github.com"
# The generated Optics files are checked below, so the private package
# that generated them must be available to the release runner too.
- run: pnpm install --frozen-lockfile
# The presets are a patch onto OpenScreen's typed document. Check them
# against the real thing before shipping a tag — a drifted field name
# does not error, it silently renders stock.
- name: Check out OpenScreen for schema verification
run: git clone --depth 1 --branch "$OPENSCREEN_REF" https://github.com/RoleModel/openscreen.git /tmp/openscreen
# The token stylesheet is generated into OpenScreen rather than this
# toolkit. Build it in the pinned checkout so a tag tests the exact app
# token layer it was made for.
- run: node lib/optics-tokens.mjs --openscreen /tmp/openscreen
- run: node lib/optics-tokens.mjs --check --openscreen /tmp/openscreen
- run: node lib/verify.mjs --openscreen /tmp/openscreen
# The Figma exports ship with this toolkit; the OpenScreen token layer was
# generated in its pinned checkout above.
- run: node lib/sync-brand.mjs --check
- run: node lib/optics-css.mjs --check
- name: Smoke-test the CLI
run: |
node bin/rm-video.mjs presets
node bin/rm-video.mjs root
cat > /tmp/t.openscreen <<'JSON'
{"schemaVersion":7,"project":{"id":"p","title":"t","createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"},"assets":[],"timeline":{"clips":[],"gaps":[],"trimRanges":[],"muteRanges":[],"speedRanges":[],"captionRanges":[]},"annotations":[],"zoomRanges":[],"legacyEditor":{}}
JSON
node bin/rm-video.mjs brand /tmp/t.openscreen --preset academy --unit rails --title "CI" --eyebrow "smoke"
# readFileSync + JSON.parse, not require(). `require` only parses JSON
# for a `.json` extension; for anything else Node treats the file as
# CommonJS JavaScript, so this tried to EXECUTE the document and died
# on `"schemaVersion": 7` with a SyntaxError. It has never worked —
# v0.0.1 failed here in 16s too, which is why no release has ever been
# produced and the tap still points at a tarball nobody built.
node -e 'const d=JSON.parse(require("node:fs").readFileSync("/tmp/t.openscreen","utf8"));if(!d.annotations.length)throw new Error("no annotations written");if(!d.legacyEditor.wallpaper)throw new Error("no wallpaper set");console.log("ok")'
release:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# The tarball Homebrew installs is built here, not taken from GitHub's
# source archive, because it carries one file the repo does not:
# lib/deployment.json — where the team's database is, and which GitHub
# app to sign in through. This repo is public and the credential is not,
# so it cannot be committed; it comes from the repo's secrets at release
# time instead. Anyone who can install the release can reach the tables —
# that is the deliberate trade, and sql/studio.sql limits what the role in
# that string can do to three tables.
#
# `git archive` with a prefix, so the tarball unpacks into one directory
# the way Homebrew expects; the generated file is appended under the same
# prefix before compressing.
- name: Build the release tarball, with the deployment baked in
id: build
env:
RM_DATABASE_URL: ${{ secrets.RM_DATABASE_URL }}
RM_GITHUB_CLIENT_ID: ${{ secrets.RM_GITHUB_CLIENT_ID }}
run: |
set -euo pipefail
NAME="rolemodel-openscreen-${GITHUB_REF_NAME}"
if [ -z "$RM_DATABASE_URL" ] || [ -z "$RM_GITHUB_CLIENT_ID" ]; then
echo "::error::RM_DATABASE_URL and RM_GITHUB_CLIENT_ID must be set as repository secrets — a release without them cannot share storyboards"
exit 1
fi
case "$RM_DATABASE_URL" in
*neondb_owner*) echo "::error::RM_DATABASE_URL is the OWNER's connection string; bake the studio_app role from sql/studio.sql instead"; exit 1;;
esac
mkdir -p /tmp/stage/"$NAME"/lib
node -e '
const fs = require("node:fs");
fs.writeFileSync(process.argv[1], JSON.stringify({
databaseUrl: process.env.RM_DATABASE_URL,
githubClientId: process.env.RM_GITHUB_CLIENT_ID,
}, null, 2) + "\n");
' /tmp/stage/"$NAME"/lib/deployment.json
git archive --format=tar --prefix="$NAME/" "$GITHUB_REF_NAME" -o /tmp/"$NAME".tar
tar -rf /tmp/"$NAME".tar -C /tmp/stage "$NAME/lib/deployment.json"
gzip -n -9 /tmp/"$NAME".tar
echo "asset=/tmp/$NAME.tar.gz" >> "$GITHUB_OUTPUT"
echo "name=$NAME.tar.gz" >> "$GITHUB_OUTPUT"
# Re-runnable on a tag that already shipped.
#
# `gh release create` fails outright when the release exists, and every
# step after it — the tap bump included — is skipped. That is exactly the
# shape of a re-run: the first attempt cut the release and then died at
# the tap for want of a token, and the retry could never get past here.
- name: Create or update the GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
echo "Release ${GITHUB_REF_NAME} exists — replacing its asset."
gh release upload "${GITHUB_REF_NAME}" "${{ steps.build.outputs.asset }}" --clobber
else
gh release create "${GITHUB_REF_NAME}" --generate-notes "${{ steps.build.outputs.asset }}"
fi
- name: Compute the tarball checksum
id: sum
run: |
URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/${{ steps.build.outputs.name }}"
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "sha=$(sha256sum "${{ steps.build.outputs.asset }}" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
# Cross-repo push needs a PAT with `contents: write` on the tap.
# Store it as the TAP_TOKEN secret on this repo.
- name: Update the tap
env:
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
run: |
if [ -z "$TAP_TOKEN" ]; then
echo "::warning::TAP_TOKEN not set — formula not updated. Bump Formula/rm-video.rb in the tap by hand."
exit 0
fi
git clone "https://x-access-token:${TAP_TOKEN}@github.com/rolemodel/homebrew-tap.git" /tmp/tap
node lib/sync-tap.mjs --tap /tmp/tap
sed -i -E "s|^ url \".*\"| url \"${{ steps.sum.outputs.url }}\"|" /tmp/tap/Formula/rm-video.rb
sed -i -E "s|^ sha256 \".*\"| sha256 \"${{ steps.sum.outputs.sha }}\"|" /tmp/tap/Formula/rm-video.rb
cd /tmp/tap
git config user.name "rolemodel-bot"
git config user.email "bot@rolemodelsoftware.com"
git add Formula/rm-video.rb
git diff --staged --quiet && { echo "formula unchanged"; exit 0; }
git commit -m "rm-video ${GITHUB_REF_NAME}"
git push