diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd1abb1..88fe7bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,15 @@ name: CI on: push: branches: [main] + tags: ['v*'] 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 @@ -20,19 +26,17 @@ jobs: - run: npm run build - run: node scripts/validate-examples.js - - name: Assemble Pages artifact - if: github.ref == 'refs/heads/main' - run: | - mkdir -p _site - cp index.html style.css ocf-bundle.js ocf-bundle.js.map _site/ - cp -r examples _site/examples - cp opencoachingformat-v1.schema.json _site/ - - - name: Upload Pages artifact - if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 + # On a release tag, package.json must match the tag, or we refuse to ship. + version-check: + name: Version matches tag + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - path: '_site' + node-version: '20' + - run: bash scripts/check-version.sh "${GITHUB_REF_NAME}" test: name: E2E Tests @@ -49,18 +53,35 @@ jobs: - run: npx playwright install chromium --with-deps - run: npm test + # Assemble the full Pages site (root = newest release tag, /preview = main) + # only after build + tests are green. Runs on main pushes and on tag pushes. deploy: name: Deploy to GitHub Pages needs: [build, test] - if: github.ref == 'refs/heads/main' + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) runs-on: ubuntu-latest permissions: + contents: read pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: + - uses: actions/checkout@v4 + with: + # Need full history + all tags so build-pages.sh can pick the newest. + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm ci + - name: Assemble Pages site (release + preview) + run: bash scripts/build-pages.sh _site + - uses: actions/upload-pages-artifact@v3 + with: + path: '_site' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 94dc222..5123711 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ocf-bundle.js.map .idea/ playwright-report/ test-results/ +_site/ diff --git a/README.md b/README.md index 888ea8f..1bd3b01 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,13 @@ Reference web editor for the [Open Coaching Format](https://github.com/opencoachingformat/spec) — an open standard for basketball drill diagrams and animations. -**Live demo:** https://opencoachingformat.github.io/ocf-editor +**Live demo (latest release):** https://opencoachingformat.github.io/ocf-editor +**Preview (current `main`):** https://opencoachingformat.github.io/ocf-editor/preview/ + +> The root URL serves the latest tagged release; `/preview/` always tracks the +> current `main` and is redeployed on every merge (after CI passes). See +> [Releases & Deployment](#releases--deployment). Architecture is documented in +> [doc/arc42.md](doc/arc42.md). --- @@ -56,6 +62,30 @@ OCF (Open Coaching Format) is a JSON-based standard for basketball drill diagram This editor intentionally deviates from the spec in two places for `ajv` strict-mode compatibility — see the comment block at the top of `src/schema.js` for details. +## Releases & Deployment + +GitHub Pages hosts two builds side by side: + +- **`/`** (root) — the latest **release**, built from the newest `v*` tag. +- **`/preview/`** — the current **`main`**, redeployed on every merge (after CI passes). + +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`). +2. Commit and merge to `main`. +3. Tag the merge commit and push the tag: + ```bash + git tag v0.3.0 + git push origin v0.3.0 + ``` + +CI refuses to deploy a tag whose `package.json` version doesn't match it +(`scripts/check-version.sh`). The Pages site is assembled by +`scripts/build-pages.sh`, which you can run locally (`bash scripts/build-pages.sh`) +to preview the exact deployed layout in `./_site`. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/doc/arc42.md b/doc/arc42.md new file mode 100644 index 0000000..f8b8d09 --- /dev/null +++ b/doc/arc42.md @@ -0,0 +1,385 @@ +# OCF Editor — Architecture (arc42) + +This document follows the [arc42](https://arc42.org/) template. It describes the +architecture of the OCF Editor, a reference web editor for the Open Coaching +Format (OCF) — an open JSON standard for basketball drill diagrams and +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`). + +--- + +## 1. Introduction and Goals + +The OCF Editor lets coaches build basketball drills visually on a court, export +them as OCF-compliant `.ocf.json`, and play them back frame by frame. It is the +reference implementation of the OCF format. + +### Requirements overview + +- Visual half-court / full-court editor for FIBA, NBA, NCAA and NFHS courts. +- Place and drag entities: offense, defense, coach, cone, station, ball. +- Draw movement, passing, dribbling, screen and plain lines (straight or curved). +- Multi-frame drills using per-frame entity-state deltas. +- Live JSON view, import/export, and structural validation against OCF v1.0.0. +- A viewer mode that animates through frames. + +### Quality goals + +| # | Quality | Motivation | +|---|---------|------------| +| 1 | **Correctness of rendering** | Diagrams must read like standard coaching notation (court lines per ruleset, FIBA defender horseshoe, curve direction). | +| 2 | **Usability on touch devices** | Coaches use phones/tablets courtside; drag and draw must work with a finger. | +| 3 | **Portability / zero-backend** | Pure static site; deployable to GitHub Pages with relative asset paths. | +| 4 | **Format fidelity** | Output validates against the OCF schema. | +| 5 | **Testability** | Behaviour and geometry are covered by automated tests. | + +### Stakeholders + +- **Coaches** — end users authoring and viewing drills. +- **OCF maintainers** — own the spec; the editor is its reference UI. +- **Contributors** — extend rendering/interaction. + +--- + +## 2. Architecture Constraints + +- **No backend.** Everything runs in the browser; persistence is file + import/export. The site must work as static files from any sub-path. +- **No runtime dependencies.** All `import`s are relative; the app is bundled + with esbuild into a single IIFE (`ocf-bundle.js`). This is what lets CI build + any git ref from a throwaY checkout (see §7). +- **Schema compatibility.** The embedded schema deviates from the published spec + in two spots for `ajv` strict-mode compatibility (unknown `format` keywords); + see the comment block atop `src/schema.js`. +- **Coordinate system.** Origin = midcourt center, `y+` = frontcourt (toward the + offense basket). FIBA stores court coords in metres; NBA/NCAA/NFHS in feet. +- **Build tooling.** `esbuild` (bundling) and `@playwright/test` (E2E) are the + only dev dependencies. + +--- + +## 3. Context and Scope + +![Context diagram: Coach uses the OCF Editor SPA, which imports/exports .ocf.json files and validates against the embedded OCF schema](diagrams/context.png) + +
Diagram source (Mermaid) + +```mermaid +flowchart LR + coach(["Coach
(browser)"]) + editor["OCF Editor (SPA)
court rendering · editing · playback"] + file[/".ocf.json file"/] + schema[["OCF v1.0.0 JSON Schema
(embedded copy)"]] + + coach -->|edits / views| editor + editor -->|export| file + file -->|import| editor + editor -.->|validates against| schema +``` + +
+ +- **In scope:** the browser application, its rendering/editing/playback logic, + the embedded schema and validator, examples, tests, and the CI/deploy pipeline. +- **Out of scope:** the OCF specification itself (lives in + `opencoachingformat/spec`), any server, accounts, or cloud storage. + +--- + +## 4. Solution Strategy + +| Goal | Approach | +|------|----------| +| Correct rendering | A pure, side-effect-free renderer turns an OCF document + frame index into an SVG string. Geometry is unit-tested on the emitted path data. | +| Touch usability | A single **Pointer Events** code path (mouse + touch + pen), `touch-action: none` on the court, and a larger hit radius for coarse pointers. | +| Portability | Static files only; **relative** asset paths so the app runs from `/` or `/preview/`. | +| Format fidelity | A dependency-free structural validator plus CI validation of all examples. | +| Testability | Pure modules (renderer, curves, editor state) are tested directly; UI flows are driven through the real bundle via `window.OCFEditor`. | + +--- + +## 5. Building Block View + +![Building block view: main.js wires the editor UI, the court rendering modules, the player, export, the embedded schema, and EditorState as the single source of truth](diagrams/building-blocks.png) + +
Diagram source (Mermaid) + +```mermaid +flowchart TD + main["main.js
entry point · app mode · OCFEditor global"] + + subgraph ui["editor/ — UI"] + palette["palette.js"] + properties["properties.js"] + frames["frames.js"] + toolbar["toolbar.js"] + ctxmenu["context-menu.js"] + interaction["interaction.js
pointer drag · hit-test · draw"] + end + + state["editor.js
EditorState
(single source of truth + undo/redo)"] + + subgraph court["court/ — rendering"] + renderer["renderer.js
renderOCF()"] + courtsvg["court-svg.js
createTransform()"] + curves["curves.js"] + positions["positions.js"] + end + + player["player/player.js
FramePlayer"] + schema["schema.js
embedded schema"] + + subgraph exp["export/"] + validate["validate.js"] + json["json.js"] + end + + main --> ui + main --> state + main --> player + main --> renderer + ui --> state + state -.->|notify| ui + state --> schema + interaction --> renderer + renderer --> courtsvg + renderer --> curves + renderer --> positions + main --> exp + exp --> validate + exp --> json +``` + +
+ +### Level 1 — modules (`src/`) + +| Module | Responsibility | +|--------|----------------| +| `main.js` | Entry point. Wires UI components, owns app mode (editor/viewer) and the current transform. Exposes `editorState`, `player`, `getCurrentTransform()` on the `OCFEditor` global (used by tests). | +| `schema.js` | Embedded OCF v1.0.0 schema + `createBlankDocument()`. | +| `court/court-svg.js` | Court background per ruleset, and `createTransform()` (court coords ↔ viewBox coords). | +| `court/renderer.js` | `renderOCF(doc, frameIndex, w, h)` → `{ svgContent, transform }`. Resolves entity positions, draws entities/lines/areas/labels. | +| `court/curves.js` | Path geometry: `curvedPath`, `straightPath`, `dribblingPath`. | +| `court/positions.js` | Named positions per ruleset, coordinate resolution, snapping. | +| `editor/editor.js` | `EditorState`: the central store with undo/redo, entity/line/frame ops, tool state, ball assignment, `startLineTool`. | +| `editor/interaction.js` | `InteractionManager`: pointer drag, hit-testing, line drawing, snapping, keyboard. | +| `editor/context-menu.js` | Floating per-entity action menu (draw tools, ball, delete). | +| `editor/palette.js`, `properties.js`, `frames.js`, `toolbar.js` | Sidebar UI: add entities, edit properties, manage frames, keyboard shortcuts. | +| `player/player.js` | `FramePlayer`: frame navigation and timed playback. | +| `export/validate.js` | Dependency-free structural OCF validator. | +| `export/json.js` | JSON serialization + file import/export. | + +### Level 2 — key abstractions + +- **Pure renderer.** `renderOCF` has no DOM side effects; `main.js` assigns its + string to `svg.innerHTML`. This makes rendering snapshot/geometry testable. +- **Single source of truth.** `EditorState` holds the document and notifies + subscribers (`subscribe`/`notify`); UI components re-render on change. +- **Transform object.** `createTransform` returns `toSvg` / `toCourt` plus a + scale helper, derived from the ruleset dimensions and padding. + +--- + +## 6. Runtime View + +### Editing an entity (drag) + +![Sequence diagram of an entity drag: pointerdown hit-tests and captures, each pointermove maps client to viewBox via the screen CTM, snaps, and moves the entity which notifies a re-render; pointerup releases and swallows the synthesized click](diagrams/runtime-drag.png) + +
Diagram source (Mermaid) + +```mermaid +sequenceDiagram + actor U as Coach + participant IM as InteractionManager + participant ES as EditorState + participant M as main.js (render) + + U->>IM: pointerdown + IM->>IM: hit-test (radius by pointer type) + IM->>IM: setPointerCapture + IM->>ES: select(entity) + loop pointermove + IM->>IM: client→viewBox via screen CTM + IM->>IM: snap to named position? + IM->>ES: moveEntity(x, y) + ES-->>M: notify + M->>M: re-render SVG (+ re-append snap indicator) + end + U->>IM: pointerup + IM->>IM: releasePointerCapture + Note over IM: swallow synthesized click after a real drag +``` + +
+ +1. `pointerdown` on the court → `InteractionManager` hit-tests entities (radius + depends on pointer type) and starts a drag, capturing the pointer. +2. `pointermove` → convert client → viewBox coords via the SVG **screen CTM**, + apply optional snap to a named position, call `editorState.moveEntity`. +3. `EditorState` updates the base position (frame 0) or writes a frame + `entity_states` delta (frame > 0) and notifies. +4. `main.js` re-renders the SVG; the snap indicator is re-appended (innerHTML + reset detaches it). +5. `pointerup` releases capture; a synthesized click after a real drag is + swallowed so it can't deselect. + +### Drawing a line from a player + +1. Player selected → context menu shown. A draw tool button calls + `editorState.startLineTool(tool, key)`, which seeds the player's position as + waypoint 1 and sets `from_entity`. +2. Court clicks append waypoints; double-click / Enter / ✓ commits the line into + the current frame. + +### Playback (viewer mode) + +`FramePlayer.load(doc)` then `play()` schedules frame advances using each +frame's `duration_ms` (default 1500 ms), looping at the end; `onFrameChange` +re-renders. + +--- + +## 7. Deployment View + +Static site on **GitHub Pages**, assembled by `scripts/build-pages.sh`: + +![Deployment: the newest v* tag is built to the site root and current main is built to /preview, both published to one GitHub Pages site](diagrams/deployment.png) + +
Diagram source (Mermaid) + +```mermaid +flowchart LR + tag["newest v* tag"] -->|git archive → esbuild| root["/ (release build)"] + main["current main"] -->|git archive → esbuild| preview["/preview/ (test build)"] + root --> pages[["GitHub Pages site"]] + preview --> pages +``` + +
+ +- Both variants are rebuilt from their **own** git ref on every deploy + (`git archive ` → esbuild → copy static files → stamp version badge). + This is possible only because the app has no runtime deps and uses relative + asset paths. +- Until the first `v*` tag exists, the root mirrors the preview build. + +### CI pipeline (`.github/workflows/ci.yml`) + +![CI pipeline: a trigger runs build then test; tags also run version-check; deploy runs after tests on main pushes or v* tags](diagrams/ci-pipeline.png) + +
Diagram source (Mermaid) + +```mermaid +flowchart TD + trigger{{"trigger: PR · main push · v* tag"}} + build["build
npm ci → build → validate examples"] + test["test
Playwright E2E (chromium)"] + vcheck["version-check
(tag only) package.json == tag"] + deploy["deploy
assemble _site → GitHub Pages"] + + trigger --> build + build --> test + trigger -->|tag only| vcheck + test -->|main push or v* tag| deploy + vcheck --> deploy +``` + +
+ +| Job | Runs on | Purpose | +|-----|---------|---------| +| `build` | PR, main push, tag push | `npm ci` → build → validate examples | +| `test` | needs `build` | Playwright E2E (chromium) | +| `version-check` | tag push only | `scripts/check-version.sh` fails unless `package.json` == tag | +| `deploy` | main push or `v*` tag, after build+test | assemble `_site` and publish to Pages | + +### Release process + +1. Bump `version` in `package.json` and the `.app-version` badge in `index.html`. +2. Merge to `main` (updates `/preview`). +3. `git tag vX.Y.Z && git push origin vX.Y.Z` → version-check gate → deploy to root. + +> Note: the repo's *Settings → Pages → Source* must be **GitHub Actions** for the +> Actions-based deploy to take effect. + +--- + +## 8. Cross-cutting Concepts + +- **Coordinate transforms.** Court units ↔ viewBox via `createTransform`; + client pixels ↔ viewBox via the SVG **screen CTM** (`getScreenCTM`). The court + scales to 100% width, so client pixels never equal viewBox units — using the + CTM is mandatory for correct hit-testing on any screen size. +- **Input.** Unified Pointer Events; `touch-action: none` prevents the browser + from claiming finger drags as scroll/pan. +- **State & undo.** `EditorState` snapshots JSON for undo/redo and emits change + 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`. + +--- + +## 9. Architecture Decisions + +| ADR | Decision | Rationale | +|-----|----------|-----------| +| Pure string renderer | `renderOCF` returns an SVG string, no DOM writes | Testable geometry; trivial re-render via `innerHTML`. | +| Pointer Events over mouse events | One handler path for mouse/touch/pen | Mouse-only handlers never fired for touch — entities couldn't be dragged on phones. | +| Screen-CTM coordinate mapping | Map client→viewBox via `getScreenCTM`, not `clientX - rect.left` | The 1:1 assumption was wrong whenever the SVG was scaled (always on mobile), mis-placing hits/drags. | +| Midpoint-quadratic curves | Interior waypoints are control points; spans join at segment midpoints | The old angle-bisector approach bowed curves *into* bends (wrong direction) and left half the line straight. | +| Stroked-arc defender | FIBA defender = single open stroked arc | The old filled self-intersecting Bézier collapsed into two crescents. | +| Possession via ball position | Ball ownership modeled by moving the ball entity onto a player | OCF has a single ball; no schema change, honors frame deltas. | +| Release/preview on one Pages site | Root = tag, `/preview` = main, rebuilt each deploy | Coexisting stable + test builds without a `gh-pages` branch. | + +--- + +## 10. Quality Requirements + +Automated tests (Playwright; pure modules tested without a browser): + +| Suite | Focus | +|-------|-------| +| `tests/editor.spec.js` | App load, palette, settings, frames, undo/redo, JSON panel, viewer toggle, example import, version badge. | +| `tests/curves.spec.js` | Curve geometry: single quadratic with waypoint as control, apex on the same side as the waypoint, endpoints preserved; dribble waves alternate sides and follow bends. | +| `tests/renderer.spec.js` | Entity symbols (defender = open stroked arc), court surface/rim, line styling, and the half-court center-circle arc bulging into the court. | +| `tests/regressions.spec.js` | Ball possession, line selection/deletion, snap indicator persistence, right-click-selects, numbering cap, touch dragging (touch-action + generous radius), line-from-player. | + +Practice: each bug fix ships with a test verified to **fail on the pre-fix code** +and pass after. Full suite at the time of writing: **51 tests**. + +--- + +## 11. Risks and Technical Debt + +- **Viewer/animation path** is the least test-covered area (frame deltas, + playback timing) — next candidate for a rendered review + tests. +- **Court dimensions** are validated visually for plausibility, not numerically + against rulebooks (deliberate — drills, not pixel-perfect designs). +- **Schema deviations** from the published spec exist for ajv strict mode; a + drift risk if the spec changes (documented in `src/schema.js`). +- **Single-file bundle / global `OCFEditor`** is convenient for tests but is a + global; acceptable for a small reference app. +- **External fonts** (Google Fonts) are loaded from a CDN; offline/blocked + networks fall back to system fonts (cosmetic only). + +--- + +## 12. Glossary + +| Term | Meaning | +|------|---------| +| OCF | Open Coaching Format — the JSON standard this editor targets. | +| Entity | A court object: offense, defense, coach, cone, station, ball. | +| Frame | One step of a drill; carries lines and entity-state deltas. | +| Entity-state delta | Per-frame position override for an entity, layered on the base. | +| Named position | A ruleset-specific court location (e.g. `top_of_the_key`) used for snapping/coordinates. | +| Transform | The mapping between court coordinates and SVG viewBox coordinates. | +| Preview build | The deployment of the current `main` at `/preview/`. | +| Release build | The deployment of the newest `v*` tag at the site root. | diff --git a/doc/diagrams/building-blocks.png b/doc/diagrams/building-blocks.png new file mode 100644 index 0000000..efd284b Binary files /dev/null and b/doc/diagrams/building-blocks.png differ diff --git a/doc/diagrams/ci-pipeline.png b/doc/diagrams/ci-pipeline.png new file mode 100644 index 0000000..923aa99 Binary files /dev/null and b/doc/diagrams/ci-pipeline.png differ diff --git a/doc/diagrams/context.png b/doc/diagrams/context.png new file mode 100644 index 0000000..ba8cb37 Binary files /dev/null and b/doc/diagrams/context.png differ diff --git a/doc/diagrams/deployment.png b/doc/diagrams/deployment.png new file mode 100644 index 0000000..55b53d2 Binary files /dev/null and b/doc/diagrams/deployment.png differ diff --git a/doc/diagrams/runtime-drag.png b/doc/diagrams/runtime-drag.png new file mode 100644 index 0000000..25d9f8c Binary files /dev/null and b/doc/diagrams/runtime-drag.png differ diff --git a/index.html b/index.html index ade3154..921ebfb 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

OCFeditor

- v0.1.0 + v0.2.0
diff --git a/package.json b/package.json index a3ea8e9..17a3a2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocf-editor", - "version": "0.1.0", + "version": "0.2.0", "description": "Reference web editor for the Open Coaching Format", "private": true, "homepage": "https://opencoachingformat.github.io/editor", diff --git a/scripts/build-pages.sh b/scripts/build-pages.sh new file mode 100755 index 0000000..4271cc4 --- /dev/null +++ b/scripts/build-pages.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Build the full GitHub Pages site into ./_site: +# / → newest v* release tag (the "final" build) +# /preview/ → current main (the "test" build) +# +# Both variants are rebuilt from their own source tree on every deploy, so the +# Pages site always contains both the latest release and the latest preview. +# The app uses only relative asset paths, so it works from any sub-path. +# +# Requires: git, node + npm deps installed (esbuild) in the current checkout. +# +# Usage: scripts/build-pages.sh [output_dir] +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_variant() { + local ref="$1" dest="$2" version="$3" + local src + src="$(mktemp -d)" + echo "→ building '$ref' as version '$version' 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 + + 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" +} + +rm -rf "$OUT" +mkdir -p "$OUT" + +# Resolve "main" robustly: when CI checks out a tag, main may only exist as a +# remote-tracking ref. +if git rev-parse --verify --quiet main >/dev/null; then + MAIN_REF="main" +elif git rev-parse --verify --quiet origin/main >/dev/null; then + MAIN_REF="origin/main" +else + MAIN_REF="HEAD" +fi + +# Newest release tag, if any (semantic sort). +LATEST_TAG="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)" + +if [ -n "$LATEST_TAG" ]; then + build_variant "$LATEST_TAG" "$OUT" "$LATEST_TAG" +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" + build_variant "$MAIN_REF" "$OUT" "main-preview" +fi + +# Always (re)build the preview from main into /preview. +build_variant "$MAIN_REF" "$OUT/preview" "main-preview" + +echo "✓ Pages site assembled in $OUT" +ls -la "$OUT" diff --git a/scripts/check-version.sh b/scripts/check-version.sh new file mode 100755 index 0000000..01d9849 --- /dev/null +++ b/scripts/check-version.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# +# Verify that package.json's version matches a release tag. +# Used by CI on tag pushes so a release can never ship a mismatched version. +# +# Usage: scripts/check-version.sh e.g. check-version.sh v0.2.0 +set -euo pipefail + +TAG="${1:?usage: check-version.sh }" +TAG_VERSION="${TAG#v}" # strip leading "v" + +PKG_VERSION="$(node -p "require('./package.json').version")" + +if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::Version mismatch — tag is '$TAG' (=$TAG_VERSION) but package.json is '$PKG_VERSION'." + echo "Update package.json to $TAG_VERSION (and the version badge) before tagging, or retag to match." + exit 1 +fi + +echo "✓ Version OK: tag $TAG matches package.json $PKG_VERSION" diff --git a/tests/editor.spec.js b/tests/editor.spec.js index 4bd3d45..9e81e01 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.1.0'); + await expect(page.locator('.app-version')).toHaveText('v0.2.0'); }); test('court SVG is rendered (no 404 on bundle)', async ({ page }) => {