Repository navigation
Release #109
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |