From 46f2f69d22c60742ea0bde4e041c6cf062fe7654 Mon Sep 17 00:00:00 2001 From: Jonas Jensen Date: Sun, 8 Nov 2026 21:49:49 +0100 Subject: [PATCH 1/3] Support classroom Windows development Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 + .github/rulesets/README.md | 10 ++- .github/workflows/ci.yml | 61 ++++++++++++++++++ README.md | 108 ++++++++++++++++---------------- docs/code.md | 34 ++++++++-- scripts/chrome.mjs | 38 ++++++++--- scripts/shot-sheet.mjs | 4 +- scripts/shot.mjs | 8 ++- scripts/tools.mjs | 25 ++++++-- tests/platform-tools.test.mjs | 86 +++++++++++++++++++++++++ 10 files changed, 297 insertions(+), 79 deletions(-) create mode 100644 tests/platform-tools.test.mjs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2645805..e49c8d7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,6 +13,8 @@ product requirements. - Preserve browser zoom, semantic HTML, and accessible responsive behavior. - Do not introduce runtime dependencies or external network services without an explicit product requirement. +- Local development supports Windows PowerShell and Linux; CI runs on Ubuntu. + Keep shell-specific setup in [`docs/code.md`](../docs/code.md). - Nothing in `docs/` is loaded automatically. Read only the topics the task needs. diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md index ee98d54..966fdd9 100644 --- a/.github/rulesets/README.md +++ b/.github/rulesets/README.md @@ -15,18 +15,16 @@ maintainer can supply independent human approval. See 3. Update that ruleset from the checked-in JSON. 4. Fetch the live ruleset and compare it with the intended change. -```sh +```powershell gh api repos/OWNER/REPO/rulesets -gh api --method PUT repos/OWNER/REPO/rulesets/ \ - --input .github/rulesets/main.json +gh api --method PUT repos/OWNER/REPO/rulesets/ --input .github/rulesets/main.json gh api repos/OWNER/REPO/rulesets/ ``` To create the ruleset when none exists: -```sh -gh api --method POST repos/OWNER/REPO/rulesets \ - --input .github/rulesets/main.json +```powershell +gh api --method POST repos/OWNER/REPO/rulesets --input .github/rulesets/main.json ``` The required check context must match the workflow job name. A rename on either diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19a4abb..229230c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_dispatch: pull_request: push: branches: @@ -69,3 +70,63 @@ jobs: path: .art/shots/ if-no-files-found: warn retention-days: 7 + + verify-windows: + name: Verify (Windows) + if: github.event_name == 'workflow_dispatch' + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + + - name: Check Chrome + shell: pwsh + run: | + $chrome = (Get-Command chrome.exe -ErrorAction SilentlyContinue).Source + if (-not $chrome) { + $chrome = @( + (Join-Path $env:ProgramFiles 'Google\Chrome\Application\chrome.exe'), + (Join-Path ${env:ProgramFiles(x86)} 'Google\Chrome\Application\chrome.exe'), + (Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe') + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + } + if (-not $chrome) { + throw 'Google Chrome was not found on PATH or in its standard installation folders.' + } + & $chrome --version + + - name: Install ImageMagick + shell: pwsh + run: | + if (-not (Get-Command magick.exe -ErrorAction SilentlyContinue)) { + choco install imagemagick --yes --no-progress + $magick = Get-ChildItem $env:ProgramFiles -Directory -Filter 'ImageMagick*' | + Select-Object -First 1 + if (-not $magick) { + throw 'ImageMagick installed but magick.exe was not found.' + } + $magick.FullName | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + } + magick -version + + - name: Install dependencies + run: npm ci + + - name: Verify + run: npm run verify -- --no-cache + + - name: Upload screenshots + if: always() + uses: actions/upload-artifact@v7 + with: + name: web-screenshots-windows + path: .art/shots/ + if-no-files-found: warn + retention-days: 7 diff --git a/README.md b/README.md index 0213e04..352c4d0 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,89 @@ -# TypeScript static web template +# Your first web app -An accessible hello-world web app with TypeScript, Vite, layered documentation, -bounded verification, and CI-gated GitHub Pages deployment. +This template gives you a simple web page to explore and change. You do not +need to know Git or GitHub to get started. GitHub is where your project lives; +the Copilot App is where you can open and work on it. -[Live example](https://OWNER.github.io/REPO/) · -[Use this template](https://github.com/OWNER/REPO/generate) +[See the live example](https://OWNER.github.io/REPO/) · +[Create your own copy](https://github.com/OWNER/REPO/generate) -## Start an app +## Start in the Copilot App -Choose **Use this template** on GitHub to create a repository with its own -history. Clone your new repository, use Node.js 22.12+ (or a supported newer -version), and run: +1. On GitHub, choose **Create your own copy** (or **Use this template**) and + create a repository for your project. +2. Open that new repository in the Copilot App. +3. Install [Node.js 22.12 or newer](https://nodejs.org/en/download/) on your + Windows laptop. Node.js is the tool that runs this project. If the + Copilot App was open during installation, reopen its terminal. +4. In the Copilot App terminal, run these commands: -```sh -npm ci -npm run dev -``` - -Replace the greeting in `src/main.ts`, styles in `src/style.css`, and page -metadata in `index.html`. Normal browser zoom is enabled. There are no runtime -dependencies, binary assets, or application network services. + ```powershell + npm ci + npm run dev + ``` -## Verify and build + `npm ci` sets up the tools for this project. You only need it the first time + (or after the project's tools change). `npm run dev` starts your web page on + your laptop. Open the address printed in the terminal, usually + `http://localhost:5173/`. Leave the command running while you work; press + **Ctrl+C** in the terminal when you are done. -```sh -npm run verify -- --no-cache -npm run build -npm run preview -``` +5. Open `src/main.ts` in the editor and change the greeting. Save the file to + see the page update in your browser. You can change its colors and layout in + `src/style.css`, and its browser-tab title in `index.html`. -Full verification needs Chrome/Chromium and ImageMagick as well as Node.js. -On Debian or Ubuntu: +The page works on small screens, supports normal browser zoom, and has no +application services to set up. -```sh -sudo apt-get install chromium imagemagick -``` +## When you are ready for more -Set `CHROME_BIN` when your browser has another name. Verification includes -unit tests, TypeScript, lint, formatting, documentation links and context -budgets, bundle budgets, and real-browser root/subpath checks at narrow/wide -sizes. Screenshots and the contact sheet land in `.art/shots/`, not the site. -Bundle thresholds remain explicit in `scripts/check-bundle.mjs`. +Run `npm run verify` to check the project, or `npm run build` to make its +publishable files. Full verification works on Windows and Linux and needs +Chrome or Chromium plus ImageMagick; setup details are in the +[code guide](docs/code.md#local-and-ci-environments). GitHub also runs the +checks for you when you make a change. The normal checks run on Linux even if +you are working on Windows; maintainers can manually run the CI workflow to +check Windows too. -## Configure the generated repository +## For repository maintainers GitHub templates copy files, **not live repository settings**. -1. Rename the package metadata with `npm pkg set name=your-app` and update the - lockfile with `npm install --package-lock-only`. Replace `OWNER` and `REPO` - in this README and [ruleset instructions](.github/rulesets/README.md), the - path example in `vite.config.ts`, and the owner in `.github/CODEOWNERS`. -2. Update the working agreement and selected topic guides for your app. - Keep the original MIT copyright notice when retaining this code; add your - own attribution where appropriate. +1. Rename the package with `npm pkg set name=your-app`, then update the lockfile + with `npm install --package-lock-only`. Replace `OWNER` and `REPO` in this + README and [ruleset instructions](.github/rulesets/README.md), the path + example in `vite.config.ts`, and the owner in `.github/CODEOWNERS`. +2. Update the [working agreement](.github/copilot-instructions.md) and selected + [documentation topics](docs/) for your app. Keep the original MIT copyright + notice when retaining this code; add your own attribution where appropriate. 3. Enable GitHub Actions and select **Settings → Pages → Source → GitHub Actions**. An administrator may instead configure it with: - ```sh + ```powershell gh api --method POST repos/OWNER/REPO/pages -f build_type=workflow # If Pages already exists: gh api --method PUT repos/OWNER/REPO/pages -f build_type=workflow ``` -4. Push the initial app to `main` and wait for **CI / Verify** to succeed. - Run **Deploy** manually once if its completion trigger was not yet present - on the default branch. Manual deployment also requires successful push CI - for that exact main commit. +4. Push the initial app to `main` and wait for **CI / Verify** to succeed. Run + **Deploy** manually once if its completion trigger was not yet present on + the default branch. Manual deployment also requires successful push CI for + that exact `main` commit. 5. Apply and read back the checked-in [main ruleset](.github/rulesets/README.md) after initial publication. Its required check is `Verify`; it requires no human approval for a solo maintainer. Review CODEOWNERS and the policy before using them in a team. -Publishing is a Pages deployment of the exact successful main push SHA, -not a tagged release or a committed build branch. The workflow token does not -have administration permission and cannot configure Pages or branch rules. -The relative Vite base supports any repository name without rebuilding for -another URL prefix. +Publishing deploys the exact successful `main` push commit to GitHub Pages; it +is not a tagged release or a committed build branch. The workflow token cannot +configure Pages or branch rules. The relative Vite base supports any +repository name without rebuilding for another URL prefix. ## Working agreement Start with [AGENTS.md](AGENTS.md), which points to the -[working agreement](.github/copilot-instructions.md). It routes contributors to -focused [documentation topics](docs/) and on-demand +[working agreement](.github/copilot-instructions.md). It routes contributors +to focused [documentation topics](docs/) and on-demand [decision records](docs/decisions/README.md). ## Attribution diff --git a/docs/code.md b/docs/code.md index 4371cdb..0079fd8 100644 --- a/docs/code.md +++ b/docs/code.md @@ -19,6 +19,33 @@ The available commands and their current composition live in `package.json` and the scripts themselves. Use a command's help or source instead of maintaining a second catalogue here. +## Local and CI environments + +The project commands work from Windows PowerShell or a Linux shell. Full local +verification needs Chrome and ImageMagick on Windows, or Chromium and +ImageMagick on Linux. GitHub Actions runs the same verification on Ubuntu. + +On Debian or Ubuntu, install the screenshot tools with: + +```sh +sudo apt-get install chromium imagemagick +``` + +On Windows, install Google Chrome and ImageMagick, and make sure ImageMagick's +`magick.exe` is on `PATH`. Chrome is found in its standard installation +folders; set these PowerShell variables if either tool is installed elsewhere: +replace the example paths with the actual locations on your computer. + +```powershell +$env:CHROME_BIN = 'C:\Program Files\Google\Chrome\Application\chrome.exe' +$env:IMAGEMAGICK_BIN = 'C:\Program Files\ImageMagick\magick.exe' +npm run verify +``` + +The variables only apply to the current terminal window. +The CI workflow's Windows job is available through a manual run; pull-request +and push checks continue to use Ubuntu. + ## Change the owning abstraction - Search for the canonical type, table, helper, or check before adding another @@ -68,9 +95,8 @@ A stack is reviewed and merged from its base upward. 2. When a pull request's base is not `main`, explicitly request Copilot review; it does not start automatically: - ```sh - gh api -X POST repos///pulls//requested_reviewers \ - -f "reviewers[]=copilot-pull-request-reviewer[bot]" + ```powershell + gh api -X POST repos///pulls//requested_reviewers -f "reviewers[]=copilot-pull-request-reviewer[bot]" ``` 3. Do not push new commits to a branch another open pull request uses as its @@ -87,7 +113,7 @@ A stack is reviewed and merged from its base upward. 4. If the upload surface is unavailable, say so and link the screenshot artifact from the current CI run instead: - ```sh + ```powershell gh run list --branch --workflow CI --limit 1 gh run view ``` diff --git a/scripts/chrome.mjs b/scripts/chrome.mjs index 0a22f35..d4d4bd4 100644 --- a/scripts/chrome.mjs +++ b/scripts/chrome.mjs @@ -11,10 +11,29 @@ import { spawn } from "node:child_process"; import { existsSync, readFileSync, rmSync } from "node:fs"; import { createConnection } from "node:net"; -import { join } from "node:path"; +import { join, win32 } from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; -const CHROME = process.env.CHROME_BIN || "chromium"; +export function resolveChromeExecutable({ + platform = process.platform, + env = process.env, + exists = existsSync, +} = {}) { + if (env.CHROME_BIN) return env.CHROME_BIN; + if (platform !== "win32") return "chromium"; + + const chromePath = (base) => + base ? win32.join(base, "Google", "Chrome", "Application", "chrome.exe") : null; + const installed = [ + chromePath(env.PROGRAMFILES), + chromePath(env["PROGRAMFILES(X86)"]), + chromePath(env.LOCALAPPDATA), + ].find((candidate) => candidate && exists(candidate)); + + return installed ?? "chrome.exe"; +} + +const CHROME = resolveChromeExecutable(); /** * Is anything already listening on the debugging port? @@ -73,9 +92,12 @@ export async function openChrome({ debugPort, profileDir, windowSize = "1280,800 (browser ? `It says it is ${browser}.\n` : "") + "\nThat is nearly always a headless browser left behind by an " + "interrupted run.\nIt has to go, because attaching to it would drive " + - "the wrong browser and fail\nsomewhere unrelated. Find it and stop it:\n\n" + - ` ps -eo pid,cmd | grep '[r]emote-debugging-port=${debugPort}'\n` + - " kill \n", + "the wrong browser and fail\nsomewhere unrelated. Close the leftover " + + "headless browser, then try again.\n" + + (process.platform === "win32" + ? "On Windows, find it in Task Manager and choose End task.\n" + : `On Linux, find it with: ps -eo pid,cmd | grep '[r]emote-debugging-port=${debugPort}'\n` + + "Then stop it with: kill \n"), ); process.exit(1); } @@ -107,8 +129,10 @@ export async function openChrome({ debugPort, profileDir, windowSize = "1280,800 const why = error.code === "ENOENT" ? `no such program: ${CHROME}` : error.message; launchError = new Error( `Could not start Chrome (${why}).\n` + - "Install Chromium, or point CHROME_BIN at the browser to use:\n" + - " CHROME_BIN=google-chrome npm run shot", + "Install Chrome/Chromium, or point CHROME_BIN at the browser executable.\n" + + (process.platform === "win32" + ? "PowerShell: $env:CHROME_BIN = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; npm run shot\n" + : "Linux: CHROME_BIN=google-chrome npm run shot\n"), ); }); diff --git a/scripts/shot-sheet.mjs b/scripts/shot-sheet.mjs index a999688..ce757d4 100644 --- a/scripts/shot-sheet.mjs +++ b/scripts/shot-sheet.mjs @@ -140,7 +140,9 @@ export function buildSheet() { if (import.meta.url === `file://${process.argv[1]}`) { if (!haveImageMagick()) { console.error( - "Cannot find ImageMagick.\nInstall it first - on Debian or Ubuntu: sudo apt-get install imagemagick", + "Cannot find ImageMagick.\n" + + "On Debian or Ubuntu: sudo apt-get install imagemagick.\n" + + "On Windows, install ImageMagick and make sure magick.exe is on PATH.", ); process.exit(1); } diff --git a/scripts/shot.mjs b/scripts/shot.mjs index 87e85a4..35c8d63 100644 --- a/scripts/shot.mjs +++ b/scripts/shot.mjs @@ -14,7 +14,7 @@ import { statSync, writeFileSync, } from "node:fs"; -import { extname, join, relative, resolve } from "node:path"; +import { extname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { rm } from "node:fs/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; import { setTimeout as sleep } from "node:timers/promises"; @@ -76,8 +76,10 @@ export function serve(req, res) { } const prefix = pathname.startsWith("/template-preview/") ? "/template-preview/" : "/"; const name = pathname.slice(prefix.length) || "index.html"; - const file = resolve(dist, name); - if (!file.startsWith(`${resolve(dist)}/`)) { + const buildRoot = resolve(dist); + const file = resolve(buildRoot, name); + const relativeFile = relative(buildRoot, file); + if (relativeFile === ".." || relativeFile.startsWith(`..${sep}`) || isAbsolute(relativeFile)) { res.writeHead(403).end("outside build"); return; } diff --git a/scripts/tools.mjs b/scripts/tools.mjs index 3b23f3d..ed1a6c8 100644 --- a/scripts/tools.mjs +++ b/scripts/tools.mjs @@ -1,19 +1,33 @@ /** Resolve ImageMagick 6/7 for the screenshot contact sheet. */ import { execFileSync } from "node:child_process"; -function have(binary) { +function isImageMagick(binary) { try { - execFileSync("/bin/sh", ["-c", 'command -v -- "$1"', "sh", binary], { stdio: "ignore" }); - return true; + const version = execFileSync(binary, ["-version"], { encoding: "utf8" }); + return /ImageMagick/i.test(version); } catch { return false; } } +export function resolveImageMagick({ + platform = process.platform, + env = process.env, + supports = isImageMagick, +} = {}) { + if (env.IMAGEMAGICK_BIN) return env.IMAGEMAGICK_BIN; + + const candidates = + platform === "win32" + ? ["magick.exe", "magick", "convert.exe", "convert"] + : ["magick", "convert"]; + return candidates.find(supports) ?? null; +} + let binary; function imagemagick() { if (binary === undefined) { - binary = process.env.IMAGEMAGICK_BIN ?? ["magick", "convert"].find(have) ?? null; + binary = resolveImageMagick(); } return binary; } @@ -23,7 +37,8 @@ export const haveImageMagick = () => imagemagick() !== null; export function requireImageMagick() { if (!haveImageMagick()) { throw new Error( - "Cannot find ImageMagick. On Debian or Ubuntu: sudo apt-get install imagemagick", + "Cannot find ImageMagick. On Debian or Ubuntu: sudo apt-get install imagemagick. " + + "On Windows, install ImageMagick and make sure magick.exe is on PATH, or set IMAGEMAGICK_BIN.", ); } } diff --git a/tests/platform-tools.test.mjs b/tests/platform-tools.test.mjs new file mode 100644 index 0000000..2c83a39 --- /dev/null +++ b/tests/platform-tools.test.mjs @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; + +import { resolveChromeExecutable } from "../scripts/chrome.mjs"; +import { resolveImageMagick } from "../scripts/tools.mjs"; + +describe("browser executable discovery", () => { + it("uses the configured browser path on either platform", () => { + expect( + resolveChromeExecutable({ + platform: "win32", + env: { CHROME_BIN: "D:\\Browsers\\chrome.exe" }, + }), + ).toBe("D:\\Browsers\\chrome.exe"); + }); + + it("uses Chromium by default on Linux", () => { + expect(resolveChromeExecutable({ platform: "linux", env: {} })).toBe("chromium"); + }); + + it("finds Chrome in the standard Windows installation folders", () => { + const installedChrome = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; + + expect( + resolveChromeExecutable({ + platform: "win32", + env: { + PROGRAMFILES: "C:\\Program Files", + LOCALAPPDATA: "C:\\Users\\Student\\AppData\\Local", + }, + exists: (path) => path === installedChrome, + }), + ).toBe(installedChrome); + }); + + it("falls back to the Windows PATH command", () => { + expect(resolveChromeExecutable({ platform: "win32", env: {}, exists: () => false })).toBe( + "chrome.exe", + ); + }); +}); + +describe("ImageMagick discovery", () => { + it("checks Windows executable names and selects the first working command", () => { + const tried = []; + + const binary = resolveImageMagick({ + platform: "win32", + env: {}, + supports: (candidate) => { + tried.push(candidate); + return candidate === "magick"; + }, + }); + + expect(tried).toEqual(["magick.exe", "magick"]); + expect(binary).toBe("magick"); + }); + + it("checks Linux commands without requiring a shell", () => { + const tried = []; + + const binary = resolveImageMagick({ + platform: "linux", + env: {}, + supports: (candidate) => { + tried.push(candidate); + return candidate === "convert"; + }, + }); + + expect(tried).toEqual(["magick", "convert"]); + expect(binary).toBe("convert"); + }); + + it("honors an explicit ImageMagick path", () => { + expect( + resolveImageMagick({ + platform: "win32", + env: { IMAGEMAGICK_BIN: "C:\\ImageMagick\\magick.exe" }, + supports: () => { + throw new Error("should not probe the PATH"); + }, + }), + ).toBe("C:\\ImageMagick\\magick.exe"); + }); +}); From e16f537ad02a4ad5050078eb6e49207ffc732dda Mon Sep 17 00:00:00 2001 From: Jonas Jensen Date: Sun, 8 Nov 2026 22:04:22 +0100 Subject: [PATCH 2/3] Normalize line endings across platforms Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf From 12f9d5b43e635f101c84e161bcaa6837d0429e17 Mon Sep 17 00:00:00 2001 From: Jonas Jensen Date: Mon, 9 Nov 2026 16:45:06 +0100 Subject: [PATCH 3/3] Address Windows verification review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 61 ------------------------- .github/workflows/windows-verify.yml | 68 ++++++++++++++++++++++++++++ README.md | 2 +- docs/code.md | 4 +- scripts/shot-sheet.mjs | 4 +- tests/shot-sheet.test.mjs | 34 ++++++++++++++ 6 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/windows-verify.yml create mode 100644 tests/shot-sheet.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 229230c..19a4abb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,6 @@ name: CI on: - workflow_dispatch: pull_request: push: branches: @@ -70,63 +69,3 @@ jobs: path: .art/shots/ if-no-files-found: warn retention-days: 7 - - verify-windows: - name: Verify (Windows) - if: github.event_name == 'workflow_dispatch' - runs-on: windows-latest - - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Set up Node.js - uses: actions/setup-node@v7 - with: - node-version: 22 - cache: npm - - - name: Check Chrome - shell: pwsh - run: | - $chrome = (Get-Command chrome.exe -ErrorAction SilentlyContinue).Source - if (-not $chrome) { - $chrome = @( - (Join-Path $env:ProgramFiles 'Google\Chrome\Application\chrome.exe'), - (Join-Path ${env:ProgramFiles(x86)} 'Google\Chrome\Application\chrome.exe'), - (Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe') - ) | Where-Object { Test-Path $_ } | Select-Object -First 1 - } - if (-not $chrome) { - throw 'Google Chrome was not found on PATH or in its standard installation folders.' - } - & $chrome --version - - - name: Install ImageMagick - shell: pwsh - run: | - if (-not (Get-Command magick.exe -ErrorAction SilentlyContinue)) { - choco install imagemagick --yes --no-progress - $magick = Get-ChildItem $env:ProgramFiles -Directory -Filter 'ImageMagick*' | - Select-Object -First 1 - if (-not $magick) { - throw 'ImageMagick installed but magick.exe was not found.' - } - $magick.FullName | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 - } - magick -version - - - name: Install dependencies - run: npm ci - - - name: Verify - run: npm run verify -- --no-cache - - - name: Upload screenshots - if: always() - uses: actions/upload-artifact@v7 - with: - name: web-screenshots-windows - path: .art/shots/ - if-no-files-found: warn - retention-days: 7 diff --git a/.github/workflows/windows-verify.yml b/.github/workflows/windows-verify.yml new file mode 100644 index 0000000..9832107 --- /dev/null +++ b/.github/workflows/windows-verify.yml @@ -0,0 +1,68 @@ +name: Windows verification + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify-windows: + name: Verify (Windows) + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + + - name: Check Chrome + shell: pwsh + run: | + $chrome = (Get-Command chrome.exe -ErrorAction SilentlyContinue).Source + if (-not $chrome) { + $chrome = @( + (Join-Path $env:ProgramFiles 'Google\Chrome\Application\chrome.exe'), + (Join-Path ${env:ProgramFiles(x86)} 'Google\Chrome\Application\chrome.exe'), + (Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe') + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + } + if (-not $chrome) { + throw 'Google Chrome was not found on PATH or in its standard installation folders.' + } + & $chrome --version + + - name: Install ImageMagick + shell: pwsh + run: | + if (-not (Get-Command magick.exe -ErrorAction SilentlyContinue)) { + choco install imagemagick --yes --no-progress + $magick = Get-ChildItem $env:ProgramFiles -Directory -Filter 'ImageMagick*' | + Select-Object -First 1 + if (-not $magick) { + throw 'ImageMagick installed but magick.exe was not found.' + } + $env:PATH = "$($magick.FullName);$env:PATH" + $magick.FullName | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 + } + magick -version + + - name: Install dependencies + run: npm ci + + - name: Verify + run: npm run verify -- --no-cache + + - name: Upload screenshots + if: always() + uses: actions/upload-artifact@v7 + with: + name: web-screenshots-windows + path: .art/shots/ + if-no-files-found: warn + retention-days: 7 diff --git a/README.md b/README.md index 352c4d0..aacdbbf 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ publishable files. Full verification works on Windows and Linux and needs Chrome or Chromium plus ImageMagick; setup details are in the [code guide](docs/code.md#local-and-ci-environments). GitHub also runs the checks for you when you make a change. The normal checks run on Linux even if -you are working on Windows; maintainers can manually run the CI workflow to +you are working on Windows; maintainers can manually run Windows verification to check Windows too. ## For repository maintainers diff --git a/docs/code.md b/docs/code.md index 0079fd8..433a0f8 100644 --- a/docs/code.md +++ b/docs/code.md @@ -43,8 +43,8 @@ npm run verify ``` The variables only apply to the current terminal window. -The CI workflow's Windows job is available through a manual run; pull-request -and push checks continue to use Ubuntu. +The Windows verification workflow is available through a manual run; +pull-request and push checks continue to use Ubuntu. ## Change the owning abstraction diff --git a/scripts/shot-sheet.mjs b/scripts/shot-sheet.mjs index ce757d4..78fc5c9 100644 --- a/scripts/shot-sheet.mjs +++ b/scripts/shot-sheet.mjs @@ -14,7 +14,7 @@ */ import { existsSync, readdirSync, rmSync } from "node:fs"; import { join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { magick, haveImageMagick } from "./tools.mjs"; @@ -137,7 +137,7 @@ export function buildSheet() { } } -if (import.meta.url === `file://${process.argv[1]}`) { +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { if (!haveImageMagick()) { console.error( "Cannot find ImageMagick.\n" + diff --git a/tests/shot-sheet.test.mjs b/tests/shot-sheet.test.mjs new file mode 100644 index 0000000..33f529d --- /dev/null +++ b/tests/shot-sheet.test.mjs @@ -0,0 +1,34 @@ +import { spawnSync } from "node:child_process"; +import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +describe("screenshot sheet command", () => { + it("runs its entrypoint from a path with spaces and reports missing screenshots", () => { + const root = mkdtempSync(join(tmpdir(), "shot sheet ")); + const scripts = join(root, "scripts"); + try { + mkdirSync(scripts); + for (const name of ["shot-sheet.mjs", "tools.mjs"]) { + copyFileSync( + fileURLToPath(new URL(`../scripts/${name}`, import.meta.url)), + join(scripts, name), + ); + } + + const result = spawnSync(process.execPath, [join(scripts, "shot-sheet.mjs")], { + env: { ...process.env, IMAGEMAGICK_BIN: process.execPath }, + encoding: "utf8", + }); + + expect(result.error).toBeUndefined(); + expect(result.status).toBe(1); + expect(result.stderr).toContain("No screenshots"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +});