diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88fe7bf..cd03377 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,20 +7,16 @@ 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 with: - node-version: '20' + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - run: npm run build @@ -31,26 +27,45 @@ 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 with: - node-version: '20' + node-version-file: '.nvmrc' - run: bash scripts/check-version.sh "${GITHUB_REF_NAME}" test: name: E2E Tests runs-on: ubuntu-latest needs: build + timeout-minutes: 15 steps: - 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 - - 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 @@ -74,7 +96,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/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..b0ebcb9 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 @@ -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 f8b8d09..57f5487 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`). --- @@ -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 921ebfb..527126f 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

OCFeditor

- v0.2.0 +
diff --git a/package.json b/package.json index 17a3a2f..7274b92 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,23 @@ { "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/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": { + "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 9e81e01..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.0'); + await expect(page.locator('.app-version')).toHaveText(`v${version}`); }); test('court SVG is rendered (no 404 on bundle)', async ({ page }) => {