From 096059957ae5efb0429ec382c9e5ada0054749d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 16:15:35 +0000 Subject: [PATCH 1/5] ci: run on Node 24 via .nvmrc, declare engines floor - Pin the Node version in a single source (.nvmrc = 24) and have every CI job read it via setup-node's node-version-file, instead of hardcoding '20' in four places. Bumping Node later is now a one-line change. - Add an engines floor of node >=22 to package.json. esbuild 0.24 and @playwright/test 1.58 both support Node 22/24; the full build and the 51-test E2E suite pass on Node 22 locally. --- .github/workflows/ci.yml | 8 ++++---- .nvmrc | 1 + package.json | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .nvmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88fe7bf..d44b1fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - run: npm run build @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version-file: '.nvmrc' - run: bash scripts/check-version.sh "${GITHUB_REF_NAME}" test: @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - run: npm run build @@ -74,7 +74,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - name: Assemble Pages site (release + preview) diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/package.json b/package.json index 17a3a2f..24cd36f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "url": "https://github.com/opencoachingformat/editor/issues" }, "license": "MIT", + "engines": { + "node": ">=22" + }, "scripts": { "build": "esbuild src/main.js --bundle --outfile=ocf-bundle.js --format=iife --global-name=OCFEditor --sourcemap", "watch": "esbuild src/main.js --bundle --outfile=ocf-bundle.js --format=iife --global-name=OCFEditor --sourcemap --watch", From f679d5a4bdc8523559e067300c058a99dc7556fa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 16:19:11 +0000 Subject: [PATCH 2/5] =?UTF-8?q?docs:=20fix=20repo=20URLs=20(opencoachingfo?= =?UTF-8?q?rmat/editor=20=E2=86=92=20ocf-editor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The homepage, repository, bugs, CI badge and clone URLs pointed at the non-existent "opencoachingformat/editor" repo. Correct them to "ocf-editor" and fix the matching `cd editor` → `cd ocf-editor` in the setup steps. --- CONTRIBUTING.md | 4 ++-- README.md | 6 +++--- package.json | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f92fcc..1f3aade 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,8 +3,8 @@ ## Getting Started ```bash -git clone https://github.com/opencoachingformat/editor.git -cd editor +git clone https://github.com/opencoachingformat/ocf-editor.git +cd ocf-editor npm install npm run build open index.html diff --git a/README.md b/README.md index 1bd3b01..31e80bb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Schema](https://img.shields.io/badge/Schema-v1.0.0-green.svg)](opencoachingformat-v1.schema.json) -[![CI](https://github.com/opencoachingformat/editor/actions/workflows/ci.yml/badge.svg)](https://github.com/opencoachingformat/editor/actions/workflows/ci.yml) +[![CI](https://github.com/opencoachingformat/ocf-editor/actions/workflows/ci.yml/badge.svg)](https://github.com/opencoachingformat/ocf-editor/actions/workflows/ci.yml) Reference web editor for the [Open Coaching Format](https://github.com/opencoachingformat/spec) — an open standard for basketball drill diagrams and animations. @@ -29,8 +29,8 @@ Reference web editor for the [Open Coaching Format](https://github.com/opencoach ## Quick Start ```bash -git clone https://github.com/opencoachingformat/editor.git -cd editor +git clone https://github.com/opencoachingformat/ocf-editor.git +cd ocf-editor npm install npm run build # Open index.html in your browser diff --git a/package.json b/package.json index 24cd36f..fe1f1ca 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,13 @@ "version": "0.2.0", "description": "Reference web editor for the Open Coaching Format", "private": true, - "homepage": "https://opencoachingformat.github.io/editor", + "homepage": "https://opencoachingformat.github.io/ocf-editor", "repository": { "type": "git", - "url": "https://github.com/opencoachingformat/editor.git" + "url": "https://github.com/opencoachingformat/ocf-editor.git" }, "bugs": { - "url": "https://github.com/opencoachingformat/editor/issues" + "url": "https://github.com/opencoachingformat/ocf-editor/issues" }, "license": "MIT", "engines": { From 5044edee703c5245893409694f4d630f5725a9c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 16:21:37 +0000 Subject: [PATCH 3/5] chore: bump version to 0.2.1 Update package.json, the in-app version badge, the version E2E assertion, and the arc42 version note. --- doc/arc42.md | 2 +- index.html | 2 +- package.json | 2 +- tests/editor.spec.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/arc42.md b/doc/arc42.md index f8b8d09..2dc389b 100644 --- a/doc/arc42.md +++ b/doc/arc42.md @@ -7,7 +7,7 @@ animations. > Status: living document. Last substantive update reflects work up to and > including the mobile-touch / rendering fixes and the release/preview CI -> pipeline (app version `0.2.0`). +> pipeline (app version `0.2.1`). --- diff --git a/index.html b/index.html index 921ebfb..d3c53dd 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

OCFeditor

- v0.2.0 + v0.2.1
diff --git a/package.json b/package.json index fe1f1ca..3375a03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocf-editor", - "version": "0.2.0", + "version": "0.2.1", "description": "Reference web editor for the Open Coaching Format", "private": true, "homepage": "https://opencoachingformat.github.io/ocf-editor", diff --git a/tests/editor.spec.js b/tests/editor.spec.js index 9e81e01..0af141c 100644 --- a/tests/editor.spec.js +++ b/tests/editor.spec.js @@ -31,7 +31,7 @@ test.describe('App loads', () => { test('page title and version are correct', async ({ page }) => { await openEditor(page); await expect(page).toHaveTitle(/OCF Editor/); - await expect(page.locator('.app-version')).toHaveText('v0.2.0'); + await expect(page.locator('.app-version')).toHaveText('v0.2.1'); }); test('court SVG is rendered (no 404 on bundle)', async ({ page }) => { From 07a10d19f0f9c88915947adce3fa65c0980787bc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 16:35:58 +0000 Subject: [PATCH 4/5] refactor: make package.json the single source for the app version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version previously had to be bumped by hand in package.json, the index.html badge, and the E2E test. Now it lives only in package.json: - scripts/build.js (new) replaces the inline esbuild command and injects the version at build time via esbuild `define` (__APP_VERSION__), read from $npm_package_version (falling back to the package.json in cwd). An optional APP_VERSION env var overrides the display string. - index.html ships an empty .app-version badge; main.js fills it from the injected constant on init. - The version E2E assertion derives the expected value from package.json. - build-pages.sh builds every variant through build.js instead of a raw esbuild call + sed stamp. The /preview build is labelled via APP_VERSION; release builds derive "v" from the tag's package.json. Older release refs (whose main.js predates injection) keep their static badge, which for a tag already equals the tag version — so the fallback stays correct. Bumping the version is now a one-line change to package.json. Verified: build injects the badge, all 51 E2E tests pass, and build-pages produces correct badges for new (injected) and old (static fallback) refs. --- README.md | 3 ++- doc/arc42.md | 10 ++++++--- index.html | 2 +- package.json | 4 ++-- scripts/build-pages.sh | 30 ++++++++++++------------- scripts/build.js | 50 ++++++++++++++++++++++++++++++++++++++++++ src/main.js | 5 +++++ tests/editor.spec.js | 7 +++++- 8 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 scripts/build.js diff --git a/README.md b/README.md index 31e80bb..b0ebcb9 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ Until the first release tag exists, the root mirrors the preview build. To cut a release: -1. Bump `version` in `package.json` (and the `.app-version` badge in `index.html`). +1. Bump `version` in `package.json` — the only place it lives. The build injects + it into the in-app badge (`scripts/build.js`), and the E2E test derives it. 2. Commit and merge to `main`. 3. Tag the merge commit and push the tag: ```bash diff --git a/doc/arc42.md b/doc/arc42.md index 2dc389b..57f5487 100644 --- a/doc/arc42.md +++ b/doc/arc42.md @@ -300,7 +300,8 @@ flowchart TD ### Release process -1. Bump `version` in `package.json` and the `.app-version` badge in `index.html`. +1. Bump `version` in `package.json` (single source — the build injects it into + the badge and the E2E test derives it). 2. Merge to `main` (updates `/preview`). 3. `git tag vX.Y.Z && git push origin vX.Y.Z` → version-check gate → deploy to root. @@ -321,8 +322,11 @@ flowchart TD notifications; the singleton is reset in tests for order-independence. - **Validation.** Structural validator with no external deps; CI validates all `examples/*.ocf.json`. -- **Versioning.** Version lives in `package.json`, the in-app badge, and the - version test; the CI gate keeps a release tag consistent with `package.json`. +- **Versioning.** The version lives solely in `package.json`. `scripts/build.js` + injects it into the bundle at build time (esbuild `define` → `__APP_VERSION__`, + read from `$npm_package_version`), `main.js` writes it into the badge, and the + E2E test derives the expected value from `package.json`. The CI gate keeps a + release tag consistent with `package.json`. --- diff --git a/index.html b/index.html index d3c53dd..527126f 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

OCFeditor

- v0.2.1 +
diff --git a/package.json b/package.json index 3375a03..7274b92 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "node": ">=22" }, "scripts": { - "build": "esbuild src/main.js --bundle --outfile=ocf-bundle.js --format=iife --global-name=OCFEditor --sourcemap", - "watch": "esbuild src/main.js --bundle --outfile=ocf-bundle.js --format=iife --global-name=OCFEditor --sourcemap --watch", + "build": "node scripts/build.js", + "watch": "node scripts/build.js --watch", "validate-examples": "node scripts/validate-examples.js", "test": "npx playwright test", "test:ui": "npx playwright test --ui" diff --git a/scripts/build-pages.sh b/scripts/build-pages.sh index 4271cc4..ec70065 100755 --- a/scripts/build-pages.sh +++ b/scripts/build-pages.sh @@ -15,36 +15,35 @@ set -euo pipefail OUT="${1:-_site}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -ESBUILD="$ROOT/node_modules/.bin/esbuild" # Files that make up a deployable site (everything index.html references). STATIC_FILES=(style.css opencoachingformat-v1.schema.json) -# Build one variant from a git ref into a target directory, stamping the version. -# $1 = git ref (tag or branch), $2 = target dir, $3 = version label for the badge +# Build one variant from a git ref into a target directory. +# $1 = git ref (tag or branch), $2 = target dir, $3 = version label (optional; +# empty → scripts/build.js derives "v" from that ref's package.json). build_variant() { - local ref="$1" dest="$2" version="$3" + local ref="$1" dest="$2" label="$3" local src src="$(mktemp -d)" - echo "→ building '$ref' as version '$version' into $dest" + echo "→ building '$ref' (version: ${label:-from package.json}) into $dest" git archive "$ref" | tar -x -C "$src" - mkdir -p "$dest" - "$ESBUILD" "$src/src/main.js" --bundle \ - --outfile="$dest/ocf-bundle.js" --format=iife \ - --global-name=OCFEditor --sourcemap + # Build with this checkout's scripts/build.js (it has esbuild via node_modules) + # against the extracted ref as the working directory, so the version is taken + # from the ref's own package.json — or from APP_VERSION when we pass a label. + # Older refs whose main.js predates build-time injection simply keep their + # static badge, which for a release tag already equals the tag version. + ( cd "$src" && APP_VERSION="$label" node "$ROOT/scripts/build.js" ) + mkdir -p "$dest" + cp "$src/ocf-bundle.js" "$src/ocf-bundle.js.map" "$dest"/ cp "$src/index.html" "$dest/index.html" for f in "${STATIC_FILES[@]}"; do [ -e "$src/$f" ] && cp "$src/$f" "$dest/" done [ -d "$src/examples" ] && cp -r "$src/examples" "$dest/examples" - # Stamp the in-app version badge so the deployed build self-reports its version. - if [ -n "$version" ]; then - sed -i "s#[^<]*#${version}#" "$dest/index.html" - fi - rm -rf "$src" } @@ -65,7 +64,8 @@ fi LATEST_TAG="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)" if [ -n "$LATEST_TAG" ]; then - build_variant "$LATEST_TAG" "$OUT" "$LATEST_TAG" + # Empty label → build.js stamps "v" from the tag's package.json. + build_variant "$LATEST_TAG" "$OUT" "" else # No release yet → root shows the preview build so the site is never empty. echo "→ no v* tag found; root will mirror the preview build" diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..7668cce --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,50 @@ +#!/usr/bin/env node +// +// Build the OCF Editor bundle with esbuild. +// +// The app version is injected at build time from a single source of truth — +// package.json — so it never has to be kept in sync by hand. npm exposes the +// version as $npm_package_version when this runs via `npm run build`; when the +// script is invoked directly (e.g. by build-pages.sh against a checked-out +// release) it falls back to the package.json in the current working directory. +// +// The display string can be overridden with the APP_VERSION env var (used by +// build-pages.sh to label the /preview build), otherwise it is `v`. +// +// Usage: node scripts/build.js [--watch] +'use strict'; + +const path = require('path'); +const esbuild = require('esbuild'); + +// Resolve project files relative to the working directory, so this script can +// build either the repo root (npm run build) or an extracted release tree. +const cwd = process.cwd(); +const pkg = require(path.join(cwd, 'package.json')); + +const version = + process.env.APP_VERSION || `v${process.env.npm_package_version || pkg.version}`; + +const options = { + entryPoints: [path.join(cwd, 'src/main.js')], + bundle: true, + outfile: path.join(cwd, 'ocf-bundle.js'), + format: 'iife', + globalName: 'OCFEditor', + sourcemap: true, + define: { __APP_VERSION__: JSON.stringify(version) }, +}; + +(async () => { + if (process.argv.includes('--watch')) { + const ctx = await esbuild.context(options); + await ctx.watch(); + console.log(`esbuild: watching for changes (version ${version})…`); + } else { + await esbuild.build(options); + console.log(`esbuild: built ocf-bundle.js (version ${version})`); + } +})().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/main.js b/src/main.js index 8e8ea08..3e15e60 100644 --- a/src/main.js +++ b/src/main.js @@ -58,6 +58,11 @@ export function init() { playerControlsEl = document.getElementById('player-controls'); modeToggle = document.getElementById('mode-toggle'); + // Show the app version, injected at build time from package.json (see + // scripts/build.js). Single source of truth — no hand-maintained copies. + const versionEl = document.querySelector('.app-version'); + if (versionEl) versionEl.textContent = __APP_VERSION__; + // Init UI components new Palette(paletteEl, editorState); new Toolbar(toolbarEl, editorState); diff --git a/tests/editor.spec.js b/tests/editor.spec.js index 0af141c..67e467e 100644 --- a/tests/editor.spec.js +++ b/tests/editor.spec.js @@ -4,6 +4,11 @@ */ import { test, expect } from '@playwright/test'; +import { readFileSync } from 'node:fs'; + +// Single source of truth: the version the app should display comes from +// package.json (injected into the bundle at build time by scripts/build.js). +const { version } = JSON.parse(readFileSync('package.json', 'utf8')); // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -31,7 +36,7 @@ test.describe('App loads', () => { test('page title and version are correct', async ({ page }) => { await openEditor(page); await expect(page).toHaveTitle(/OCF Editor/); - await expect(page.locator('.app-version')).toHaveText('v0.2.1'); + await expect(page.locator('.app-version')).toHaveText(`v${version}`); }); test('court SVG is rendered (no 404 on bundle)', async ({ page }) => { From 0f058a0d8fc4e369ad9e9e510181eaa6d5994c77 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 16:46:55 +0000 Subject: [PATCH 5/5] ci: harden against stuck/slow E2E runs A transient stall in `playwright install chromium --with-deps` left the E2E job hanging with no timeout, and the workflow-level `concurrency: pages` group meant that one hung run blocked every other run (PRs and deploys) from even starting. - Add `timeout-minutes` to every job so a hang fails fast instead of running to GitHub's 6-hour default. - Move the single-slot `pages` concurrency from the workflow level onto the `deploy` job only, so a slow/stuck test run no longer blocks the pipeline. - Cache the Playwright browser by version; only re-download Chromium on a cache miss (the download/apt step is the slowest, most stall-prone part). --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d44b1fe..cd03377 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,15 +7,11 @@ on: pull_request: branches: [main] -# Allow one concurrent deployment; newer runs supersede in-progress ones. -concurrency: - group: pages - cancel-in-progress: false - jobs: build: name: Build & Validate runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -31,6 +27,7 @@ jobs: name: Version matches tag if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -42,6 +39,7 @@ jobs: name: E2E Tests runs-on: ubuntu-latest needs: build + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -50,7 +48,24 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build - - run: npx playwright install chromium --with-deps + # Cache the Playwright browser by its version so we don't re-download + # Chromium on every run (the download/apt step is the slowest and most + # stall-prone part of CI). On a cache hit we only install the system deps. + - name: Resolve Playwright version + id: pw + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" + - name: Cache Playwright browsers + id: pw-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.pw.outputs.version }} + - name: Install Playwright (browser + system deps) + if: steps.pw-cache.outputs.cache-hit != 'true' + run: npx playwright install chromium --with-deps + - name: Install Playwright system deps (cached browser) + if: steps.pw-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium - run: npm test # Assemble the full Pages site (root = newest release tag, /preview = main) @@ -60,6 +75,13 @@ jobs: needs: [build, test] if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) runs-on: ubuntu-latest + timeout-minutes: 15 + # Serialize deployments only (one Pages deploy at a time); queued deploys + # wait rather than cancel. Scoped to this job so a slow/stuck test run never + # blocks the whole CI pipeline. + concurrency: + group: pages + cancel-in-progress: false permissions: contents: read pages: write