@heroiclands/package-build 23.5.0 → 23.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ `cdn publish` skips SVGs the CDN already holds, so a re-run uploads only what changed.
8
+
9
+ `package-build labels check` is gone, because issue labels are defined once for the whole organisation; remove any `lint:labels` script and `.github/labels.yml`. A project created by `package-build init` no longer gets a label registry.
10
+
3
11
  ## 23.5.0
4
12
 
5
13
  ### Minor Changes
@@ -114,7 +114,6 @@ import { walkMarkdownTree } from "../engine/helpers.mjs";
114
114
  import { reckoningContext } from "../engine/reckoning-markers.mjs";
115
115
  import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
116
116
  import { validateLangSource } from "../lang.mjs";
117
- import { checkLabelRegistry } from "../labels.mjs";
118
117
  import { lintYaml } from "../engine/yaml-lint.mjs";
119
118
  import { lintChangesetText, lintReleaseText } from "../engine/changelog-lint.mjs";
120
119
  import { groupChangelogText } from "../engine/changelog-group.mjs";
@@ -1328,77 +1327,6 @@ function changelogGroup(args) {
1328
1327
  console.log(`package-build: ${files.length} file(s) grouped · ${warnings} warning(s)`);
1329
1328
  }
1330
1329
 
1331
- /**
1332
- * `labels check` — do the machine registry and the documented table agree?
1333
- *
1334
- * `.github/labels.yml` is synced to GitHub and the §3 table in
1335
- * `.github/ISSUE_REPORTING.md` is what a person reads. Neither derives from the
1336
- * other, so either can drift, and nothing notices until an issue is filed
1337
- * against a label that does not exist.
1338
- *
1339
- * Every repository wants this and only the paths ever differed, which is why it
1340
- * is here rather than copied into each as a `utils/` script.
1341
- *
1342
- * @returns {object} The yargs command module.
1343
- */
1344
- function labelsCommand() {
1345
- return {
1346
- command: "labels <action>",
1347
- describe: "Issue-label registry checks",
1348
- builder: (y) =>
1349
- y
1350
- .positional("action", {
1351
- choices: ["check"],
1352
- describe: "check: the registry and the documented table name the same labels",
1353
- })
1354
- .option("registry", {
1355
- type: "string",
1356
- default: ".github/labels.yml",
1357
- describe: "The machine registry synced to GitHub",
1358
- })
1359
- .option("doc", {
1360
- type: "string",
1361
- default: ".github/ISSUE_REPORTING.md",
1362
- describe: "The documented reference whose §3 table lists the same labels",
1363
- }),
1364
- handler: handler(async (args) => labelsCheck(args)),
1365
- };
1366
- }
1367
-
1368
- /**
1369
- * Read both faces of the registry and report where they disagree.
1370
- *
1371
- * Findings are reported against the file each belongs to, so a missing row is
1372
- * located in the documentation and a missing entry in the registry, rather than
1373
- * both being attributed to whichever file was read first.
1374
- *
1375
- * @param {object} args - Parsed CLI arguments.
1376
- */
1377
- function labelsCheck(args) {
1378
- for (const target of [args.registry, args.doc]) {
1379
- if (!fs.existsSync(target)) die(`labels check: ${target} does not exist.`);
1380
- }
1381
- const registryText = fs.readFileSync(args.registry, "utf8");
1382
- const docText = fs.readFileSync(args.doc, "utf8");
1383
-
1384
- const { registry, doc, count } = checkLabelRegistry({
1385
- registryText,
1386
- docText,
1387
- docPath: args.doc,
1388
- });
1389
-
1390
- const errors =
1391
- reportFindings(registry, { file: args.registry }) + reportFindings(doc, { file: args.doc });
1392
- if (errors) {
1393
- console.error(
1394
- `\nThe registry and ${args.doc} §3 are edited together — ` +
1395
- `change one and change the other.`,
1396
- );
1397
- process.exit(1);
1398
- }
1399
- console.log(`package-build: registry and §3 agree (${count} labels).`);
1400
- }
1401
-
1402
1330
  /**
1403
1331
  * `lang <action>` — the three localization guards.
1404
1332
  *
@@ -1641,7 +1569,7 @@ function cdnPublishCommand() {
1641
1569
  .option("set", {
1642
1570
  type: "string",
1643
1571
  demandOption: true,
1644
- describe: "The set file's name under the package's CDN root, e.g. ai-art.json",
1572
+ describe: "The set file's name under the package's CDN root, e.g. alt-art.json",
1645
1573
  })
1646
1574
  .option("name", {
1647
1575
  type: "string",
@@ -1972,7 +1900,6 @@ registerContentCommands(
1972
1900
  .command(typesCommand())
1973
1901
  .command(schemaCommand())
1974
1902
  .command(langCommand())
1975
- .command(labelsCommand())
1976
1903
  .command(bumpCommand())
1977
1904
  .command(yamlCommand())
1978
1905
  .command(changelogCommand())
package/docs/commands.md CHANGED
@@ -789,50 +789,6 @@ package-build: 1 template(s) fully localized and compiling.
789
789
  `package-build lang check`, `package-build lang coverage`,
790
790
  [Diagnostics](diagnostics.md), [Configuration](configuration.md).
791
791
 
792
- ### `package-build labels check`
793
-
794
- **NAME**
795
-
796
- Compare the machine label registry against the documented reference table
797
- (`check` is the only action).
798
-
799
- **SYNOPSIS**
800
-
801
- ```
802
- package-build labels check [--registry <path>] [--doc <path>]
803
- ```
804
-
805
- **DESCRIPTION**
806
-
807
- Compares the machine label registry synced to GitHub against the documented
808
- reference table, and reports where they disagree — a label the registry has
809
- that the table does not, or the reverse. Neither derives from the other, so
810
- nothing else notices when they drift. Reads both files named below; writes
811
- nothing.
812
-
813
- **OPTIONS**
814
-
815
- | Option | Type | Default | Description |
816
- | ------------ | ------ | ---------------------------- | -------------------------------------------------------------- |
817
- | `--registry` | string | `.github/labels.yml` | The machine registry synced to GitHub. |
818
- | `--doc` | string | `.github/ISSUE_REPORTING.md` | The documented reference whose §3 table lists the same labels. |
819
-
820
- **EXIT STATUS**
821
-
822
- 1 if either file does not exist. 1 if the registry and the table disagree on
823
- any label. Otherwise 0.
824
-
825
- **EXAMPLES**
826
-
827
- ```
828
- $ package-build labels check --registry .github/labels.yml --doc .github/ISSUE_REPORTING.md
829
- package-build: registry and §3 agree (11 labels).
830
- ```
831
-
832
- **SEE ALSO**
833
-
834
- None.
835
-
836
792
  ### `package-build bump [packages..]`
837
793
 
838
794
  **NAME**
@@ -1348,7 +1304,7 @@ A workflow calls the command the same way, passing the four values as named
1348
1304
  secrets in the step's environment:
1349
1305
 
1350
1306
  ```yaml
1351
- - run: npx package-build cdn publish --set ai-art.json --name "Thalorna — illustrated"
1307
+ - run: npx package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
1352
1308
  env:
1353
1309
  CDN_ENDPOINT: ${{ secrets.CDN_ENDPOINT }}
1354
1310
  CDN_BUCKET: ${{ secrets.CDN_BUCKET }}
@@ -1374,12 +1330,12 @@ it. Otherwise 0.
1374
1330
  **EXAMPLES**
1375
1331
 
1376
1332
  ```
1377
- $ package-build cdn publish --set ai-art.json --name "Thalorna — illustrated" --dry-run
1333
+ $ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
1378
1334
  Dry run: comparing 582 pictures against https://cdn.heroiclands.org, read-only.
1379
1335
  would upload thalorna/icons/anvilbdg.svg (969 bytes)
1380
1336
  …
1381
- would upload thalorna/ai-art.json (582 assets)
1382
- Would upload 582, skipped 0, failed 0; https://cdn.heroiclands.org/thalorna/ai-art.json lists 582 left-out pictures (would change).
1337
+ would upload thalorna/alt-art.json (582 assets)
1338
+ Would upload 582, skipped 0, failed 0; https://cdn.heroiclands.org/thalorna/alt-art.json lists 582 left-out pictures (would change).
1383
1339
 
1384
1340
  $ package-build cdn publish --set default.json --name Shipped
1385
1341
  package-build: `--set default.json` is refused: `default.json` is the shipped set's name, the fallback every other set is read against. Name the set for its art
@@ -284,7 +284,7 @@ when it reports at least one `error`-severity diagnostic. A `warning` alone
284
284
  never fails a run.** `bin/report.mjs`'s `reportFindings` draws this line once
285
285
  for the commands built from the package's pure rule functions (`package-build
286
286
  lint`, `package-build lang check`, `package-build lang coverage`,
287
- `package-build yaml`, `package-build labels check`): it emits every finding
287
+ `package-build yaml`): it emits every finding
288
288
  and returns only the count whose `severity` is `error`, and the command sets
289
289
  `process.exitCode = 1` exactly when that count is nonzero. Commands whose
290
290
  findings are hand-assembled — `package-build links`, `package-build
@@ -335,7 +335,6 @@ over one would teach people to stop reading the report.
335
335
  | `package-build lang check` | any localization finding is `error`-severity |
336
336
  | `package-build lang coverage` | a referenced key is missing (`error`-severity); an unreferenced key is always advisory and never fails the run |
337
337
  | `package-build yaml [paths..]` | any YAML finding is `error`-severity |
338
- | `package-build labels check` | the label registry and the documented table disagree on any label |
339
338
 
340
339
  Every command also fails on a thrown error unrelated to a
341
340
  diagnostic — a missing configuration, an unreadable file, a network failure —
@@ -861,7 +861,7 @@ everything that reads the tree runs before everything that writes the stage.
861
861
 
862
862
  [`project-setup.md`](../project-setup.md) turns this into npm scripts, and covers
863
863
  everything a repository carries beyond the build itself: the git hooks, the
864
- changeset directory, the label registry, and what each script in the chain is
864
+ changeset directory, and what each script in the chain is
865
865
  for.
866
866
 
867
867
  ## What comes next
@@ -139,13 +139,13 @@ reaches a world from there as a named asset set. `package-build cdn publish`
139
139
  is the one command that writes there:
140
140
 
141
141
  ```bash
142
- package-build cdn publish --set ai-art.json --name "Thalorna — illustrated" --dry-run
143
- package-build cdn publish --set ai-art.json --name "Thalorna — illustrated"
142
+ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated" --dry-run
143
+ package-build cdn publish --set alt-art.json --name "Thalorna — illustrated"
144
144
  ```
145
145
 
146
146
  It uploads every icon and image flat, transformed as the stage transforms
147
147
  them, skips what the CDN already holds at the same size, and writes
148
- `<site.assets>/<contentPackage>/ai-art.json` listing the left-out pictures.
148
+ `<site.assets>/<contentPackage>/alt-art.json` listing the left-out pictures.
149
149
  Run it after the pictures or their provenance change; it is not part of a
150
150
  build or a release.
151
151
 
@@ -30,8 +30,7 @@ acme-bestiary/
30
30
  │ ├── config.json
31
31
  │ └── *.md # one per unreleased change
32
32
  ├── .github/
33
- │ ├── labels.yml # the closed label registry
34
- │ ├── ISSUE_REPORTING.md # §3 lists the same labels
33
+ │ │ ├── ISSUE_REPORTING.md # how issues are filed
35
34
  │ └── workflows/
36
35
  ├── assets/
37
36
  │ └── content/ # authored Markdown notes
@@ -161,19 +160,18 @@ anything to remove.
161
160
 
162
161
  ### `lint:*` — the checks, one per question
163
162
 
164
- Seven checks, each answering one question and blind to what the others see.
163
+ Six checks, each answering one question and blind to what the others see.
165
164
  They are separate scripts rather than one because a failing chain stops at its
166
165
  first failure, and knowing _which_ question failed is most of the diagnosis.
167
166
 
168
167
  ```json
169
- "lint": "run-s lint:format lint:markdown lint:addresses lint:content-links lint:lang lint:labels lint:changelog",
168
+ "lint": "run-s lint:format lint:markdown lint:addresses lint:content-links lint:lang lint:changelog",
170
169
  "lint:format": "package-build format",
171
170
  "lint:markdown": "package-build markdown",
172
171
  "lint:markdown:fix": "package-build markdown --fix",
173
172
  "lint:addresses": "package-build lint",
174
173
  "lint:content-links": "package-build links",
175
174
  "lint:lang": "package-build lang check",
176
- "lint:labels": "package-build labels check",
177
175
  "lint:changelog": "package-build changelog check"
178
176
  ```
179
177
 
@@ -194,9 +192,6 @@ first failure, and knowing _which_ question failed is most of the diagnosis.
194
192
  `package-build lang coverage` and `package-build lang hardcoded`; a
195
193
  content-only package has nothing for those two to read, and each dies naming
196
194
  the glob that matched nothing rather than passing vacuously.
197
- - **`lint:labels`** — do `.github/labels.yml` and §3 of
198
- `.github/ISSUE_REPORTING.md` still list the same labels? Neither derives from
199
- the other, so nothing else notices when they drift.
200
195
  - **`lint:changelog`** — does every pending `.changeset/*.md` read like a
201
196
  release note rather than a pasted pull-request description? Passes
202
197
  trivially when nothing is pending: no pull request is required to carry a
@@ -499,46 +494,9 @@ tool:
499
494
 
500
495
  ## `.github/`
501
496
 
502
- Two files the toolchain reads, and they must agree.
503
-
504
- **`.github/labels.yml`** is the closed label registry — the machine-readable
505
- source of truth, synced to GitHub by the org-wide `labels` action. A label on
506
- GitHub that is not listed there is deleted on the next sync, which is what makes
507
- the set closed and why nothing invents one.
508
-
509
- ```yaml
510
- - name: documentation
511
- color: "0075ca"
512
- description: Documentation about this repository — README, process, authoring guides.
513
-
514
- - name: devops
515
- color: "e07b31"
516
- description: Build, tooling, CI, release, repo config.
517
- ```
518
-
519
- **`.github/ISSUE_REPORTING.md`** §3 is the same list in prose, for a human
520
- filing an issue. `package-build labels check` compares the two and fails when
521
- they disagree:
522
-
523
- ```console
524
- $ npx package-build labels check
525
- package-build: registry and §3 agree (2 labels).
526
- ```
527
-
528
- Neither file derives from the other, so this check is the only thing standing
529
- between them and a silent divergence. Editing the registry means editing both.
530
-
531
- Labels are **subject matter only**. Work shape — bug, feature, epic, task, spike
532
- — is an issue _type_, shared across the organization, so there is no `bug` label
533
- and filtering on one returns nothing.
534
-
535
- Neither file exists in a fresh repository, and `labels check` says so rather
536
- than passing vacuously:
537
-
538
- ```console
539
- $ npx package-build labels check
540
- package-build: labels check: .github/labels.yml does not exist.
541
- ```
497
+ **`.github/ISSUE_REPORTING.md`** says how issues are filed. Labels are
498
+ organisation-level and defined in `HeroicLands/.github`, so the repository
499
+ carries no label registry of its own.
542
500
 
543
501
  `.github/workflows/` then carries the repository's own CI. The org-wide
544
502
  `HeroicLands/.github` repository supplies the shared composite actions those
@@ -549,17 +507,16 @@ site-deploy workflow, so a fix to any of them reaches every repository at once.
549
507
 
550
508
  A summary, because "where does this come from" is the question that recurs.
551
509
 
552
- | Read | By |
553
- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
554
- | `package.json` | The Foundry package id, the version, the release addresses; the site's `baseURL`, description and author. |
555
- | `package-build.config.yaml` | Everything else about the build. |
556
- | `assets/content/**/*.md` | Every content command. |
557
- | `.gitignore` | `package-build format`, `package-build markdown`. |
558
- | `.prettierignore` | `package-build format`. |
559
- | `.github/labels.yml`, `.github/ISSUE_REPORTING.md` | `package-build labels check`. |
560
- | `lang/*.json` | `package-build lang`. |
561
- | `.env.local` | `package-build deploy`. |
562
- | `.github/workflows/build.yml` | The `pre-push` hook, for its step list. |
510
+ | Read | By |
511
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------- |
512
+ | `package.json` | The Foundry package id, the version, the release addresses; the site's `baseURL`, description and author. |
513
+ | `package-build.config.yaml` | Everything else about the build. |
514
+ | `assets/content/**/*.md` | Every content command. |
515
+ | `.gitignore` | `package-build format`, `package-build markdown`. |
516
+ | `.prettierignore` | `package-build format`. |
517
+ | `lang/*.json` | `package-build lang`. |
518
+ | `.env.local` | `package-build deploy`. |
519
+ | `.github/workflows/build.yml` | The `pre-push` hook, for its step list. |
563
520
 
564
521
  Everything written goes under `build/`. Nothing the toolchain generates is
565
522
  committed.
@@ -179,7 +179,8 @@ export function cdnCredentials(env) {
179
179
  *
180
180
  * @typedef {object} CdnStore
181
181
  * @property {(key: string) => Promise<number|undefined>} size - An object's
182
- * size in bytes, `undefined` when there is none.
182
+ * size in bytes, `undefined` when there is none or the store does not state
183
+ * it.
183
184
  * @property {(key: string) => Promise<Buffer|undefined>} read - An object's
184
185
  * bytes, `undefined` when there is none.
185
186
  * @property {((key: string, body: Buffer, contentType: string) => Promise<void>)|undefined} write -
@@ -188,6 +189,26 @@ export function cdnCredentials(env) {
188
189
  * secret.
189
190
  */
190
191
 
192
+ /**
193
+ * Request headers that keep the CDN from compressing a HEAD answer: a
194
+ * gzip-encoded answer carries no content-length.
195
+ */
196
+ const IDENTITY = { "accept-encoding": "identity" };
197
+
198
+ /**
199
+ * The object size a HEAD answer states.
200
+ *
201
+ * @param {Response} response - A successful HEAD response.
202
+ * @returns {number|undefined} The content-length, `undefined` when the answer
203
+ * states none.
204
+ */
205
+ function contentLength(response) {
206
+ const header = response.headers.get("content-length");
207
+ if (header === null || header.trim() === "") return undefined;
208
+ const length = Number(header);
209
+ return Number.isFinite(length) && length >= 0 ? length : undefined;
210
+ }
211
+
191
212
  /**
192
213
  * Throw for a response the store refused.
193
214
  *
@@ -224,10 +245,10 @@ export function s3Store({ endpoint, bucket, accessKeyId, secretAccessKey, fetch:
224
245
  return {
225
246
  describe: `bucket ${bucket} at ${new URL(endpoint).host}`,
226
247
  async size(key) {
227
- const response = await send(key, { method: "HEAD" });
248
+ const response = await send(key, { method: "HEAD", headers: IDENTITY });
228
249
  if (response.status === 404) return undefined;
229
250
  if (!response.ok) refused(`HEAD ${key}`, response);
230
- return Number(response.headers.get("content-length"));
251
+ return contentLength(response);
231
252
  },
232
253
  async read(key) {
233
254
  const response = await send(key, { method: "GET" });
@@ -256,14 +277,18 @@ export function s3Store({ endpoint, bucket, accessKeyId, secretAccessKey, fetch:
256
277
  */
257
278
  export function publicStore(host, fetchImpl) {
258
279
  const base = host.replace(/\/+$/, "");
259
- const get = (key, method) => (fetchImpl ?? fetch)(`${base}/${key}`, { method });
280
+ const get = (key, method) =>
281
+ (fetchImpl ?? fetch)(`${base}/${key}`, {
282
+ method,
283
+ headers: method === "HEAD" ? IDENTITY : undefined,
284
+ });
260
285
  return {
261
286
  describe: `${base}, read-only`,
262
287
  async size(key) {
263
288
  const response = await get(key, "HEAD");
264
289
  if (response.status === 404 || response.status === 403) return undefined;
265
290
  if (!response.ok) refused(`HEAD ${key}`, response);
266
- return Number(response.headers.get("content-length"));
291
+ return contentLength(response);
267
292
  },
268
293
  async read(key) {
269
294
  const response = await get(key, "GET");
@@ -20,7 +20,6 @@ import prettier from "prettier";
20
20
  import prettierConfig from "../prettier-config.mjs";
21
21
  import { CONFIG_FILENAMES, configFromData } from "./pack-config.mjs";
22
22
  import { positionOfYamlPath } from "./diagnostics.mjs";
23
- import { checkLabelRegistry } from "../labels.mjs";
24
23
  import { formatNoteFrontmatter } from "./note-format.mjs";
25
24
 
26
25
  /** Package kinds the initializer can create. */
@@ -94,13 +93,12 @@ function projectScripts(answers) {
94
93
  "serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
95
94
  "serve:site-html": "hugo server --source build/hugo",
96
95
  "build:book": "package-build pdf",
97
- lint: "run-s lint:format lint:markdown lint:addresses lint:content-links lint:labels",
96
+ lint: "run-s lint:format lint:markdown lint:addresses lint:content-links",
98
97
  "lint:format": "package-build format",
99
98
  "lint:markdown": "package-build markdown",
100
99
  "lint:markdown:fix": "package-build markdown --fix",
101
100
  "lint:addresses": "package-build lint",
102
101
  "lint:content-links": "package-build links",
103
- "lint:labels": "package-build labels check",
104
102
  format: "package-build format --write",
105
103
  "format:check": "package-build format",
106
104
  changeset: "changeset",
@@ -125,8 +123,7 @@ function projectScripts(answers) {
125
123
  "push:qa": "package-build deploy qa",
126
124
  "push:prod": "package-build deploy prod",
127
125
  });
128
- scripts.lint =
129
- "run-s lint:format lint:markdown lint:lang lint:addresses lint:content-links lint:labels";
126
+ scripts.lint = "run-s lint:format lint:markdown lint:lang lint:addresses lint:content-links";
130
127
  return scripts;
131
128
  }
132
129
 
@@ -295,14 +292,6 @@ function projectText(answers) {
295
292
  updateInternalDependencies: "patch",
296
293
  ignore: [],
297
294
  }),
298
- ".github/labels.yml": YAML.stringify([
299
- {
300
- name: "documentation",
301
- color: "0075ca",
302
- description: "Documentation and author guides.",
303
- },
304
- { name: "devops", color: "e07b31", description: "Build, CI, and release work." },
305
- ]),
306
295
  ".github/ISSUE_REPORTING.md": [
307
296
  "# Issue reporting",
308
297
  "",
@@ -314,12 +303,9 @@ function projectText(answers) {
314
303
  "",
315
304
  "Use the organization issue type for a bug, feature, task, spike, or epic.",
316
305
  "",
317
- "## 3. Labels — the closed registry",
306
+ "## 3. Labels",
318
307
  "",
319
- "| Label | Use it for |",
320
- "| ----- | ---------- |",
321
- "| `documentation` | Documentation and author guides. |",
322
- "| `devops` | Build, CI, and release work. |",
308
+ "Labels are organization-level, defined in `HeroicLands/.github`.",
323
309
  "",
324
310
  ].join("\n"),
325
311
  ".github/workflows/build.yml": [
@@ -437,7 +423,6 @@ export function checkProject(rootDir) {
437
423
  "README.md",
438
424
  "LICENSE.md",
439
425
  ".changeset/config.json",
440
- ".github/labels.yml",
441
426
  ".github/ISSUE_REPORTING.md",
442
427
  ".github/workflows/build.yml",
443
428
  ];
@@ -525,15 +510,5 @@ export function checkProject(rootDir) {
525
510
  } else if (!fs.readdirSync(contentRoot).some((file) => file.endsWith(".md"))) {
526
511
  add("assets/content", "the content tree has no notes");
527
512
  }
528
- const labelsPath = path.join(root, ".github/labels.yml");
529
- const reportingPath = path.join(root, ".github/ISSUE_REPORTING.md");
530
- if (fs.existsSync(labelsPath) && fs.existsSync(reportingPath)) {
531
- const result = checkLabelRegistry({
532
- registryText: fs.readFileSync(labelsPath, "utf8"),
533
- docText: fs.readFileSync(reportingPath, "utf8"),
534
- });
535
- for (const finding of result.registry) add(".github/labels.yml", finding.message);
536
- for (const finding of result.doc) add(".github/ISSUE_REPORTING.md", finding.message);
537
- }
538
513
  return findings;
539
514
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.5.0",
3
+ "version": "23.5.1",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -120,7 +120,6 @@
120
120
  "hugo-theme",
121
121
  "index.mjs",
122
122
  "lang.mjs",
123
- "labels.mjs",
124
123
  "manifest.mjs",
125
124
  "markdownlint-config.mjs",
126
125
  "prettier-config.mjs",
@@ -192,11 +191,10 @@
192
191
  "build:asset-index": "node ci/emit-asset-index.mjs",
193
192
  "format": "node ci/prettier.mjs --write",
194
193
  "format:check": "node ci/prettier.mjs --check",
195
- "lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:image-roles lint:banners lint:content-format lint:docs-links",
194
+ "lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:image-roles lint:banners lint:content-format lint:docs-links",
196
195
  "lint:markdown": "node bin/package-build.mjs markdown",
197
196
  "lint:docs-links": "node bin/package-build.mjs docs links",
198
197
  "lint:yaml": "node bin/package-build.mjs yaml",
199
- "lint:labels": "node bin/package-build.mjs labels check",
200
198
  "lint:image-roles": "vitest run tests/theme-image-roles.test.ts",
201
199
  "lint:banners": "node ci/check-banners.mjs",
202
200
  "lint:markdown:fix": "node bin/package-build.mjs markdown --fix",
@@ -220,31 +220,6 @@ export const CDN_ENV: Readonly<{
220
220
  accessKeyId: string;
221
221
  secretAccessKey: string;
222
222
  }>;
223
- /**
224
- * A store holding the CDN's objects.
225
- */
226
- export type CdnStore = {
227
- /**
228
- * - An object's
229
- * size in bytes, `undefined` when there is none.
230
- */
231
- size: (key: string) => Promise<number | undefined>;
232
- /**
233
- * - An object's
234
- * bytes, `undefined` when there is none.
235
- */
236
- read: (key: string) => Promise<Buffer | undefined>;
237
- /**
238
- * -
239
- * Store an object; absent on a read-only store.
240
- */
241
- write: ((key: string, body: Buffer, contentType: string) => Promise<void>) | undefined;
242
- /**
243
- * - What the store is, for the log. Never a
244
- * secret.
245
- */
246
- describe: string;
247
- };
248
223
  /**
249
224
  * One picture to publish.
250
225
  */
@@ -309,3 +284,29 @@ export type CdnPlan = {
309
284
  */
310
285
  cdnRoot: string;
311
286
  };
287
+ /**
288
+ * A store holding the CDN's objects.
289
+ */
290
+ export type CdnStore = {
291
+ /**
292
+ * - An object's
293
+ * size in bytes, `undefined` when there is none or the store does not state
294
+ * it.
295
+ */
296
+ size: (key: string) => Promise<number | undefined>;
297
+ /**
298
+ * - An object's
299
+ * bytes, `undefined` when there is none.
300
+ */
301
+ read: (key: string) => Promise<Buffer | undefined>;
302
+ /**
303
+ * -
304
+ * Store an object; absent on a read-only store.
305
+ */
306
+ write: ((key: string, body: Buffer, contentType: string) => Promise<void>) | undefined;
307
+ /**
308
+ * - What the store is, for the log. Never a
309
+ * secret.
310
+ */
311
+ describe: string;
312
+ };
package/labels.mjs DELETED
@@ -1,191 +0,0 @@
1
- /*
2
- * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
- * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
- *
5
- * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
- * You may copy, modify, and distribute it under the terms of that license.
7
- *
8
- * For full terms, see the LICENSE.md file in the project root or visit:
9
- * https://www.gnu.org/licenses/gpl-3.0.html
10
- *
11
- * SPDX-License-Identifier: GPL-3.0-or-later
12
- */
13
-
14
- /**
15
- * The label registry has two faces that must agree.
16
- *
17
- * `.github/labels.yml` is the machine source synced to GitHub, and the §3 table
18
- * in `.github/ISSUE_REPORTING.md` is the documented reference a person reads.
19
- * Neither is derived from the other, so either can drift — or invent a label
20
- * the other has never heard of — and nothing notices until someone files an
21
- * issue against a label that does not exist, or the sync pushes a label the
22
- * documentation never mentions.
23
- *
24
- * **This lives here because every repository wants it and only the paths ever
25
- * differed.** It was a `utils/check-labels.mjs` copied per repository, which is
26
- * the shape a shared check takes just before it starts drifting between copies
27
- * — the same argument that moved the no-attribution check to a shared action.
28
- *
29
- * The checks are pure and text-in: the caller reads the two files and decides
30
- * what to do with the findings, so this module needs no filesystem and is
31
- * testable without one.
32
- *
33
- * @module
34
- */
35
-
36
- import { parse } from "yaml";
37
-
38
- /** GitHub rejects a label description beyond this with a 422. */
39
- export const MAX_DESCRIPTION = 100;
40
-
41
- /**
42
- * The line a label's `name:` entry sits on, 1-based.
43
- *
44
- * Located rather than tracked, because `yaml`'s document API would have to be
45
- * threaded through the parse for a value this simple to recover: a registry is
46
- * a flat list of `- name: <label>`, so the first line declaring that name is
47
- * the entry. A name that cannot be found yields no position, which
48
- * `reportFindings` drops rather than guessing.
49
- *
50
- * @param {string} text - The registry file's contents.
51
- * @param {string} name - The label name.
52
- * @returns {{line?: number, column?: number}} Spreadable position fields.
53
- */
54
- function positionOfLabel(text, name) {
55
- const lines = text.split("\n");
56
- for (let i = 0; i < lines.length; i += 1) {
57
- const m = lines[i].match(/^\s*-?\s*name:\s*["']?(.+?)["']?\s*$/);
58
- if (m && m[1] === name) return { line: i + 1, column: lines[i].indexOf(name) + 1 };
59
- }
60
- return {};
61
- }
62
-
63
- /**
64
- * The line a label's row sits on in the documented table, 1-based.
65
- *
66
- * @param {string} text - The documentation file's contents.
67
- * @param {string} name - The label name.
68
- * @returns {{line?: number, column?: number}} Spreadable position fields.
69
- */
70
- function positionOfDocRow(text, name) {
71
- const lines = text.split("\n");
72
- for (let i = 0; i < lines.length; i += 1) {
73
- const m = lines[i].match(/^\|\s*`([a-z][a-z0-9-]*)`\s*\|/);
74
- if (m && m[1] === name) return { line: i + 1, column: lines[i].indexOf(name) + 1 };
75
- }
76
- return {};
77
- }
78
-
79
- /**
80
- * The label names the documented §3 table lists.
81
- *
82
- * A registry row is a table row whose first cell is a backticked label name,
83
- * which is narrow on purpose: §3 carries prose and other tables, and a looser
84
- * match would read a heading or an example as a label.
85
- *
86
- * @param {string} text - The documentation file's contents.
87
- * @returns {{names: Set<string>, found: boolean}} The names, and whether §3 was
88
- * located at all — an absent section is a different failure from an empty one.
89
- */
90
- export function documentedLabels(text) {
91
- const lines = text.split("\n");
92
- const start = lines.findIndex((l) => /^##\s+3\./.test(l));
93
- if (start < 0) return { names: new Set(), found: false };
94
- const after = lines.findIndex((l, i) => i > start && /^##\s+\d/.test(l));
95
- const section = lines.slice(start, after < 0 ? lines.length : after);
96
- const names = new Set();
97
- for (const line of section) {
98
- const m = line.match(/^\|\s*`([a-z][a-z0-9-]*)`\s*\|/);
99
- if (m) names.add(m[1]);
100
- }
101
- return { names, found: true };
102
- }
103
-
104
- /**
105
- * Check the machine registry against the documented table.
106
- *
107
- * Findings are returned per file rather than merged, because each names a
108
- * position in a different document and the caller reports them against the
109
- * file they belong to.
110
- *
111
- * @param {object} sources
112
- * @param {string} sources.registryText - `.github/labels.yml`.
113
- * @param {string} sources.docText - `.github/ISSUE_REPORTING.md`.
114
- * @param {string} [sources.docPath] - The doc's path, for the message naming it.
115
- * @returns {{registry: object[], doc: object[], count: number}} Findings for
116
- * each file, and how many labels the registry declares.
117
- */
118
- export function checkLabelRegistry({
119
- registryText,
120
- docText,
121
- docPath = ".github/ISSUE_REPORTING.md",
122
- }) {
123
- const registryFindings = [];
124
- const docFindings = [];
125
-
126
- let entries;
127
- try {
128
- entries = parse(registryText);
129
- } catch (error) {
130
- registryFindings.push({
131
- severity: "error",
132
- message: `label registry is not valid YAML — ${error.message}`,
133
- });
134
- return { registry: registryFindings, doc: docFindings, count: 0 };
135
- }
136
- if (!Array.isArray(entries)) {
137
- registryFindings.push({
138
- severity: "error",
139
- message: "label registry must be a list of `{ name, description }` entries",
140
- });
141
- return { registry: registryFindings, doc: docFindings, count: 0 };
142
- }
143
-
144
- // Caught here rather than mid-sync, where it surfaces as a bare 422 from
145
- // the GitHub API naming neither the label nor the limit.
146
- for (const entry of entries) {
147
- const length = (entry?.description ?? "").length;
148
- if (length > MAX_DESCRIPTION) {
149
- registryFindings.push({
150
- ...positionOfLabel(registryText, entry.name),
151
- severity: "error",
152
- message:
153
- `"${entry.name}" has a ${length}-character description; ` +
154
- `GitHub rejects anything over ${MAX_DESCRIPTION}`,
155
- });
156
- }
157
- }
158
-
159
- const registryNames = new Set(entries.map((e) => e?.name).filter(Boolean));
160
- const { names: docNames, found } = documentedLabels(docText);
161
- if (!found) {
162
- docFindings.push({
163
- severity: "error",
164
- message: `no §3 section found in ${docPath}, so the registry has nothing to agree with`,
165
- });
166
- return { registry: registryFindings, doc: docFindings, count: registryNames.size };
167
- }
168
-
169
- for (const name of registryNames) {
170
- if (docNames.has(name)) continue;
171
- registryFindings.push({
172
- ...positionOfLabel(registryText, name),
173
- severity: "error",
174
- message:
175
- `"${name}" is in the registry but not in ${docPath} §3; ` +
176
- `the two are edited together, so add the row`,
177
- });
178
- }
179
- for (const name of docNames) {
180
- if (registryNames.has(name)) continue;
181
- docFindings.push({
182
- ...positionOfDocRow(docText, name),
183
- severity: "error",
184
- message:
185
- `"${name}" is documented in §3 but absent from the registry, ` +
186
- `so it is never synced and an issue cannot carry it`,
187
- });
188
- }
189
-
190
- return { registry: registryFindings, doc: docFindings, count: registryNames.size };
191
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * The label names the documented §3 table lists.
3
- *
4
- * A registry row is a table row whose first cell is a backticked label name,
5
- * which is narrow on purpose: §3 carries prose and other tables, and a looser
6
- * match would read a heading or an example as a label.
7
- *
8
- * @param {string} text - The documentation file's contents.
9
- * @returns {{names: Set<string>, found: boolean}} The names, and whether §3 was
10
- * located at all — an absent section is a different failure from an empty one.
11
- */
12
- export function documentedLabels(text: string): {
13
- names: Set<string>;
14
- found: boolean;
15
- };
16
- /**
17
- * Check the machine registry against the documented table.
18
- *
19
- * Findings are returned per file rather than merged, because each names a
20
- * position in a different document and the caller reports them against the
21
- * file they belong to.
22
- *
23
- * @param {object} sources
24
- * @param {string} sources.registryText - `.github/labels.yml`.
25
- * @param {string} sources.docText - `.github/ISSUE_REPORTING.md`.
26
- * @param {string} [sources.docPath] - The doc's path, for the message naming it.
27
- * @returns {{registry: object[], doc: object[], count: number}} Findings for
28
- * each file, and how many labels the registry declares.
29
- */
30
- export function checkLabelRegistry({ registryText, docText, docPath, }: {
31
- registryText: string;
32
- docText: string;
33
- docPath?: string | undefined;
34
- }): {
35
- registry: object[];
36
- doc: object[];
37
- count: number;
38
- };
39
- /** GitHub rejects a label description beyond this with a 422. */
40
- export const MAX_DESCRIPTION: 100;