Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/refresh-community-data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Fetch and update discussion posts
run: node scripts/refresh-discussions.mjs
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/refresh-community-sitemap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Generate community sitemap
run: node scripts/generate-community-sitemap.mjs
Expand Down
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,22 @@ See [`ADVENTURES.md`](ADVENTURES.md). Add/extend the YAML, add per-level `*-post

The `off-on-dev` org restricts third-party actions. Permitted: `actions/checkout`, `actions/cache`, `actions/setup-node`, `actions/create-github-app-token`, `JamesIves/github-pages-deploy-action`, `marocchino/sticky-pull-request-comment`, `rossjrw/pr-preview-action`, `fsfe/reuse-action`, actions owned by `off-on-dev`, actions created by GitHub, and Marketplace-verified actions. `withastro/action` and `actions/deploy-pages` are **NOT allowlisted**. Before adding a `uses:`, verify it is permitted.

### Workflow convention: scripts/ require npm ci

Any workflow job that invokes a script under `scripts/` via `node scripts/*.mjs` must run `npm ci` immediately after `setup-node`, before the first script step:

```yaml
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "npm"

- name: Install dependencies
run: npm ci
```

This is unconditional, even when the script currently uses only Node built-ins. Scripts acquire package imports over time; a workflow that skips `npm ci` breaks silently on the next scheduled run. With `cache: "npm"` a clean install takes a few seconds.

---

## Before Submitting Code
Expand Down
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,22 @@ See [`ADVENTURES.md`](ADVENTURES.md). In brief: add/extend the YAML at `src/data

The `off-on-dev` org restricts third-party actions. Permitted: `actions/checkout`, `actions/cache`, `actions/setup-node`, `actions/create-github-app-token`, `JamesIves/github-pages-deploy-action`, `marocchino/sticky-pull-request-comment`, `rossjrw/pr-preview-action`, `fsfe/reuse-action`, actions owned by `off-on-dev`, actions created by GitHub, and Marketplace-verified actions. **The official `withastro/action` and `actions/deploy-pages` are NOT allowlisted** — keep the JamesIves deploy flow. Before adding a `uses:`, verify it is permitted or use `gh`/shell.

### Workflow convention: scripts/ require npm ci

Any workflow job that invokes a script under `scripts/` via `node scripts/*.mjs` must run `npm ci` immediately after `setup-node`, before the first script step:

```yaml
- uses: actions/setup-node@v7
with:
node-version-file: ".nvmrc"
cache: "npm"

- name: Install dependencies
run: npm ci
```

This is unconditional, even when the script currently uses only Node built-ins. Scripts acquire package imports over time; a workflow that skips `npm ci` breaks silently on the next scheduled run. With `cache: "npm"` a clean install takes a few seconds.

---

## Before Submitting Code
Expand Down
15 changes: 9 additions & 6 deletions scripts/sync-adventure.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ function fail(msg) {
process.exit(1);
}

function currentMonth() {
export function currentMonth() {
const ABBR = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"];
const d = new Date();
return d.toLocaleString("en-GB", { month: "short" }).toUpperCase() + " " + d.getFullYear();
return ABBR[d.getUTCMonth()] + " " + d.getUTCFullYear();
}

function parseAdventureUrl(url) {
Expand Down Expand Up @@ -594,7 +595,9 @@ async function main() {
console.log(`\nDone: ${adventureName} (live: ${activeLevels.map((l) => l.level).join(", ")}${upcomingLevels.length > 0 ? ` | upcoming: ${upcomingLevels.map((u) => u.difficulty).join(", ")}` : ""})`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}
56 changes: 56 additions & 0 deletions src/test/scripts/sync-adventure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 OffOn contributors
// SPDX-License-Identifier: MIT

/**
* Unit tests for currentMonth() in scripts/sync-adventure.mjs.
*
* Non-vacuous check: the old implementation used
* new Date().toLocaleString("en-GB", { month: "short" }).toUpperCase()
* Node 26 CLDR returns "Sept" (4 letters) for September, so "SEPT 2026"
* fails the adventure schema regex /^[A-Z]{3} \d{4}$/.
*
* The it.each test pins the system clock to every month of the year so
* the suite catches a revert to toLocaleString regardless of when it runs.
* The old-implementation test always asserts "SEPT 2026" against the
* schema, providing a second revert-catch that does not depend on the clock.
*/

import { describe, it, expect, vi, afterEach } from "vitest";
import { currentMonth } from "../../../scripts/sync-adventure.mjs";

const MONTH_SCHEMA = /^[A-Z]{3} \d{4}$/;

const ALL_MONTHS: [number, string][] = [
[0, "JAN"], [1, "FEB"], [2, "MAR"], [3, "APR"],
[4, "MAY"], [5, "JUN"], [6, "JUL"], [7, "AUG"],
[8, "SEP"], [9, "OCT"], [10, "NOV"], [11, "DEC"],
];

describe("currentMonth", () => {
afterEach(() => {
vi.useRealTimers();
});

it.each(ALL_MONTHS)(
"month %i (%s): produces correct abbreviation and passes schema regex",
(monthIndex, abbr) => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, monthIndex, 15));
expect(currentMonth()).toBe(`${abbr} 2026`);
expect(currentMonth()).toMatch(MONTH_SCHEMA);
},
);

it("old toLocaleString implementation produces 'SEPT 2026' for September, failing the schema", () => {
// Inline the old implementation so this assertion is always true regardless
// of the current month. A revert of the fix causes the it.each test above
// to fail for September; this test makes the cause immediately legible.
const oldImpl = (d: Date) =>
d.toLocaleString("en-GB", { month: "short" }).toUpperCase() +
" " +
d.getFullYear();
const result = oldImpl(new Date(2026, 8, 15));
expect(result).toBe("SEPT 2026");
expect(result).not.toMatch(MONTH_SCHEMA);
});
});
Loading