paperlint 11.1.0 → 11.1.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/.github/workflows/dependabot-automerge.yml +20 -6
- package/.github/workflows/release.yml +10 -0
- package/package.json +1 -1
- package/scripts/refresh-deadlines/main.test.ts +5 -2
- package/scripts/refresh-deadlines/refresh.test.ts +15 -14
- package/src/cycle-rules.test.ts +14 -6
- package/src/presets.test.ts +34 -24
- package/src/submission-show.test.ts +9 -0
|
@@ -31,19 +31,29 @@ on:
|
|
|
31
31
|
workflows: ["ci"]
|
|
32
32
|
types: [completed]
|
|
33
33
|
|
|
34
|
-
# Merge and delete branch — nothing else this workflow needs.
|
|
34
|
+
# Merge and delete branch, then start release.yml — nothing else this workflow needs.
|
|
35
35
|
permissions:
|
|
36
36
|
contents: write
|
|
37
37
|
pull-requests: write
|
|
38
|
+
actions: write
|
|
38
39
|
|
|
39
40
|
jobs:
|
|
40
41
|
merge:
|
|
41
|
-
# Three conditions, each blocking a case: the run
|
|
42
|
-
#
|
|
42
|
+
# Three conditions, each blocking a case: the run is GREEN · it was for one of two bots' pull
|
|
43
|
+
# requests, never a person's · and that pull request is the bot's own. Remove any one and
|
|
44
|
+
# meaning changes. The two bots:
|
|
45
|
+
# - Dependabot: its PR's `ci` is an ordinary `pull_request` run.
|
|
46
|
+
# - the deadline refresh (deadlines.yml): a PR opened with GITHUB_TOKEN starts no workflow, so
|
|
47
|
+
# deadlines.yml dispatches `ci` on its branch; that run is `workflow_dispatch` by
|
|
48
|
+
# github-actions[bot] on `bot/preset-deadlines`, and GitHub still lists the PR in
|
|
49
|
+
# `pull_requests` (measured on run 39489222900, PR #248).
|
|
43
50
|
if: >
|
|
44
|
-
github.event.workflow_run.
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
github.event.workflow_run.conclusion == 'success' && (
|
|
52
|
+
(github.event.workflow_run.event == 'pull_request' &&
|
|
53
|
+
github.event.workflow_run.actor.login == 'dependabot[bot]') ||
|
|
54
|
+
(github.event.workflow_run.event == 'workflow_dispatch' &&
|
|
55
|
+
github.event.workflow_run.actor.login == 'github-actions[bot]' &&
|
|
56
|
+
github.event.workflow_run.head_branch == 'bot/preset-deadlines'))
|
|
47
57
|
runs-on: ubuntu-latest
|
|
48
58
|
timeout-minutes: 5
|
|
49
59
|
steps:
|
|
@@ -68,3 +78,7 @@ jobs:
|
|
|
68
78
|
fi
|
|
69
79
|
gh pr merge "${PR}" --repo "${REPO}" --squash --delete-branch --match-head-commit "${HEAD_SHA}"
|
|
70
80
|
echo "merged PR #${PR} on green ci run ${RUN_ID}"
|
|
81
|
+
# A merge made with GITHUB_TOKEN pushes to main without starting release.yml (GitHub's rule
|
|
82
|
+
# against loops), so a `fix(presets):` would never be released. Start it by hand; a
|
|
83
|
+
# `deps:` merge releases nothing either way, so the dispatch is harmless there.
|
|
84
|
+
gh workflow run release.yml --repo "${REPO}" --ref main
|
|
@@ -10,6 +10,16 @@ name: Release
|
|
|
10
10
|
on:
|
|
11
11
|
push:
|
|
12
12
|
branches: [main]
|
|
13
|
+
# dependabot-automerge.yml merges with GITHUB_TOKEN, and such a push starts no workflow: it
|
|
14
|
+
# dispatches this one instead.
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
# One release at a time: two runs started close together (a push and a dispatch, or two bot merges)
|
|
18
|
+
# would both read the same last tag, and the one that loses the tag race leaves its merge
|
|
19
|
+
# unreleased. A queued run reads every commit since the tag, so the last one in still releases all.
|
|
20
|
+
concurrency:
|
|
21
|
+
group: release
|
|
22
|
+
cancel-in-progress: false
|
|
13
23
|
|
|
14
24
|
permissions:
|
|
15
25
|
contents: write
|
package/package.json
CHANGED
|
@@ -21,12 +21,15 @@ import { promisify } from "node:util";
|
|
|
21
21
|
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
22
|
import { parsePreset } from "#src/tex-requirements";
|
|
23
23
|
import { argsOf, presetsIn, summaryTo } from "./main.ts";
|
|
24
|
+
import { FROZEN_DIR, frozenText } from "../../test/frozen-presets.ts";
|
|
24
25
|
|
|
25
26
|
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
26
27
|
const PRESETS = join(ROOT, "presets");
|
|
27
28
|
const CASSETTE = join(ROOT, "src", "adapters", "hotcrp", "cassette");
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Frozen at the portal's reading of 2026-10-19 (test/frozen-presets.ts): the shipped file's dates
|
|
30
|
+
// are the refresh's own output.
|
|
31
|
+
const MSR = join(FROZEN_DIR, "msr.jsonc");
|
|
32
|
+
const msrText = frozenText("msr.jsonc");
|
|
30
33
|
const readings = (text: string) => parsePreset(text, MSR).deadlines ?? [];
|
|
31
34
|
/** MSR with its submission deadline two days before the portal's. */
|
|
32
35
|
const earlier = msrText.replace(
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* fake VenueDeadlines port that answers plain domain values (how HotCRP's page is parsed is
|
|
4
4
|
* src/adapters/hotcrp/deadlines.test.ts's) and presets held in memory.
|
|
5
5
|
*/
|
|
6
|
-
import { readdirSync
|
|
7
|
-
import {
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readdirSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
9
8
|
import { describe, expect, test } from "vitest";
|
|
10
9
|
import type { PortalDeadlines } from "#src/domain/deadline";
|
|
11
10
|
import { err, ok, type Result } from "#src/domain/result";
|
|
12
11
|
import type { PortalFailure } from "#src/domain/submission";
|
|
13
12
|
import type { VenueDeadlines } from "#src/ports/venue-deadlines";
|
|
14
13
|
import { parsePreset } from "#src/tex-requirements";
|
|
14
|
+
import { FROZEN_DIR, frozenText } from "../../test/frozen-presets.ts";
|
|
15
15
|
import {
|
|
16
16
|
refreshPreset,
|
|
17
17
|
runRefresh,
|
|
@@ -20,10 +20,11 @@ import {
|
|
|
20
20
|
type PresetFiles,
|
|
21
21
|
} from "./refresh.ts";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
23
|
+
// The logic is tested over presets FROZEN at the portal's reading of 2026-10-19 (test/frozen-presets.ts):
|
|
24
|
+
// the shipped files' dates are the refresh's own output, and a test of the refresh that read them
|
|
25
|
+
// would turn red on the refresh's own pull request.
|
|
26
|
+
const MSR = join(FROZEN_DIR, "msr.jsonc");
|
|
27
|
+
const msrText = frozenText("msr.jsonc");
|
|
27
28
|
const readings = (text: string) => parsePreset(text, MSR).deadlines ?? [];
|
|
28
29
|
/** MSR with its submission deadline two days before the portal's. */
|
|
29
30
|
const earlier = msrText.replace(
|
|
@@ -39,12 +40,12 @@ function inMemory(files: Readonly<Record<string, string>>) {
|
|
|
39
40
|
write: (name, text) => {
|
|
40
41
|
written[name] = text;
|
|
41
42
|
},
|
|
42
|
-
path: (name) => join(
|
|
43
|
+
path: (name) => join(FROZEN_DIR, name),
|
|
43
44
|
};
|
|
44
45
|
return { presets, written };
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/** What MSR 2027's portal lists — the readings
|
|
48
|
+
/** What MSR 2027's portal lists — the readings the frozen msr.jsonc holds. */
|
|
48
49
|
const MSR_PAGE: PortalDeadlines = {
|
|
49
50
|
now: 1792434476,
|
|
50
51
|
open: true,
|
|
@@ -164,12 +165,12 @@ describe("refreshPreset on MSR 2027's page", () => {
|
|
|
164
165
|
|
|
165
166
|
describe("refreshPreset — what it skips, and a portal it cannot read", () => {
|
|
166
167
|
test("a preset without a portal, or with one paperlint has no adapter for, is skipped", async () => {
|
|
167
|
-
const aisec = join(
|
|
168
|
+
const aisec = join(FROZEN_DIR, "aisec.jsonc");
|
|
168
169
|
const skipped = { kind: "skipped" };
|
|
169
170
|
expect(
|
|
170
171
|
await refreshPreset({
|
|
171
172
|
file: aisec,
|
|
172
|
-
text:
|
|
173
|
+
text: frozenText("aisec.jsonc"),
|
|
173
174
|
portal: answering(ok(MSR_PAGE)),
|
|
174
175
|
today: "2026-10-05",
|
|
175
176
|
}),
|
|
@@ -226,9 +227,9 @@ describe("runRefresh — what it prints, writes and returns", () => {
|
|
|
226
227
|
test("nothing changed: one line, no preset written, exit 0", async () => {
|
|
227
228
|
const { presets, written } = inMemory(
|
|
228
229
|
Object.fromEntries(
|
|
229
|
-
readdirSync(
|
|
230
|
+
readdirSync(FROZEN_DIR)
|
|
230
231
|
.filter((f) => f.endsWith(".jsonc"))
|
|
231
|
-
.map((f) => [f,
|
|
232
|
+
.map((f) => [f, frozenText(f)]),
|
|
232
233
|
),
|
|
233
234
|
);
|
|
234
235
|
const lines: string[] = [];
|
|
@@ -241,7 +242,7 @@ describe("runRefresh — what it prints, writes and returns", () => {
|
|
|
241
242
|
});
|
|
242
243
|
expect([code, written]).toStrictEqual([0, {}]);
|
|
243
244
|
expect(lines.at(-1)).toMatch(
|
|
244
|
-
/^✓ no deadline changed: 2 preset\(s\) with a portal read,
|
|
245
|
+
/^✓ no deadline changed: 2 preset\(s\) with a portal read, 1 skipped$/,
|
|
245
246
|
);
|
|
246
247
|
});
|
|
247
248
|
|
package/src/cycle-rules.test.ts
CHANGED
|
@@ -8,15 +8,23 @@ import { describe, expect, it } from "vitest";
|
|
|
8
8
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
9
|
import { presetsDir } from "./package-dirs.ts";
|
|
10
10
|
import { cycleRules } from "./cycle-rules.ts";
|
|
11
|
+
import { frozenText } from "../test/frozen-presets.ts";
|
|
11
12
|
|
|
12
13
|
const VENUES = presetsDir();
|
|
13
14
|
const DIR = "/work/papers/p";
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The shipped presets, so a cycle naming `paperlint:msr` resolves through the memory files — msr
|
|
17
|
+
* FROZEN (test/frozen-presets.ts): its deadlines are rewritten by the scheduled refresh, and the
|
|
18
|
+
* tests below assert the readings they sit beside.
|
|
19
|
+
*/
|
|
20
|
+
const SHIPPED = {
|
|
21
|
+
...Object.fromEntries(
|
|
22
|
+
readdirSync(VENUES)
|
|
23
|
+
.filter((f) => f.endsWith(".jsonc"))
|
|
24
|
+
.map((f) => [join(VENUES, f), readFileSync(join(VENUES, f), "utf8")]),
|
|
25
|
+
),
|
|
26
|
+
[join(VENUES, "msr.jsonc")]: frozenText("msr.jsonc"),
|
|
27
|
+
};
|
|
20
28
|
|
|
21
29
|
interface Reported {
|
|
22
30
|
readonly rule: string;
|
package/src/presets.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
10
10
|
import { join, relative } from "node:path";
|
|
11
11
|
import { describe, expect, it } from "vitest";
|
|
12
12
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
13
|
+
import { frozenText } from "../test/frozen-presets.ts";
|
|
13
14
|
import { presetsDir } from "./package-dirs.ts";
|
|
14
15
|
import { parsePreset } from "./tex-requirements.ts";
|
|
15
16
|
import {
|
|
@@ -23,18 +24,27 @@ import {
|
|
|
23
24
|
} from "./presets.ts";
|
|
24
25
|
|
|
25
26
|
const VENUES = presetsDir();
|
|
26
|
-
const
|
|
27
|
+
const live = Object.fromEntries(
|
|
27
28
|
readdirSync(VENUES)
|
|
28
29
|
.filter((f) => f.endsWith(".jsonc") || f.endsWith(".json"))
|
|
29
30
|
.map((f) => [join(VENUES, f), readFileSync(join(VENUES, f))]),
|
|
30
31
|
);
|
|
32
|
+
// The shipped msr.jsonc's deadlines are rewritten by the scheduled refresh, so the tests of LOGIC
|
|
33
|
+
// read it FROZEN (test/frozen-presets.ts); only the shape test below reads the shipped file.
|
|
34
|
+
const shipped = {
|
|
35
|
+
...live,
|
|
36
|
+
[join(VENUES, "msr.jsonc")]: frozenText("msr.jsonc"),
|
|
37
|
+
};
|
|
31
38
|
const PAPER_FILE = "/work/papers/p/paperlint.json";
|
|
32
|
-
const deps = (extra: Record<string, string> = {}) => ({
|
|
33
|
-
files: memoryFiles({ ...
|
|
39
|
+
const deps = (extra: Record<string, string> = {}, base = shipped) => ({
|
|
40
|
+
files: memoryFiles({ ...base, ...extra }),
|
|
34
41
|
venuesDir: VENUES,
|
|
35
42
|
});
|
|
36
|
-
const resolve = (
|
|
37
|
-
|
|
43
|
+
const resolve = (
|
|
44
|
+
spec: string,
|
|
45
|
+
extra: Record<string, string> = {},
|
|
46
|
+
base = shipped,
|
|
47
|
+
) => resolvePreset(spec, PAPER_FILE, deps(extra, base));
|
|
38
48
|
/** A venue preset of a project's own: the identity every venue declares, then `body`. */
|
|
39
49
|
const venue = (body: Record<string, unknown>, name = "Our Workshop"): string =>
|
|
40
50
|
JSON.stringify({
|
|
@@ -80,7 +90,7 @@ describe("shipped presets", () => {
|
|
|
80
90
|
|
|
81
91
|
describe("shipped presets: paperlint:msr", () => {
|
|
82
92
|
it("paperlint:msr → msr over ieee-conference: one kind, appendices counted as body, compsoc forbidden", () => {
|
|
83
|
-
const r = resolve("paperlint:msr");
|
|
93
|
+
const r = resolve("paperlint:msr", {}, live);
|
|
84
94
|
expect(r.ok).toBe(true);
|
|
85
95
|
if (!r.ok) return;
|
|
86
96
|
expect(r.value.chain.map((f) => f.split("/").pop())).toEqual([
|
|
@@ -103,26 +113,26 @@ describe("shipped presets: paperlint:msr", () => {
|
|
|
103
113
|
kind: "hotcrp",
|
|
104
114
|
url: "https://msr2027.hotcrp.com",
|
|
105
115
|
});
|
|
106
|
-
// The
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
source:
|
|
114
|
-
url:
|
|
115
|
-
read:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
// The deadlines' SHAPE, never their dates: the scheduled refresh rewrites those whenever the
|
|
117
|
+
// portal moves them (deadlines.yml), and a date asserted here turns its pull request red.
|
|
118
|
+
// Guards: the shipped preset keeps a submission deadline, every reading is the portal's own
|
|
119
|
+
// from the venue's deadlines page, with a read day and an instant that carries its zone.
|
|
120
|
+
expect(r.value.deadlines.map((d) => d.what)).toContain("submission");
|
|
121
|
+
expect(
|
|
122
|
+
r.value.deadlines.map((d) => ({
|
|
123
|
+
source: d.source,
|
|
124
|
+
url: d.url,
|
|
125
|
+
read: /^\d{4}-\d{2}-\d{2}$/.test(d.read),
|
|
126
|
+
at: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(d.at),
|
|
127
|
+
})),
|
|
128
|
+
).toEqual(
|
|
129
|
+
r.value.deadlines.map(() => ({
|
|
120
130
|
source: "portal",
|
|
121
131
|
url: "https://msr2027.hotcrp.com/deadlines",
|
|
122
|
-
read:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
read: true,
|
|
133
|
+
at: true,
|
|
134
|
+
})),
|
|
135
|
+
);
|
|
126
136
|
// No rule turned on: the IEEE capitalization rule has not been read and quoted.
|
|
127
137
|
expect(r.value.rules).toEqual({});
|
|
128
138
|
expect(r.value.registerAnchors).toEqual([]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** `paperlint submission show`: what the portal holds, whether it is the local build, the deadlines in force. */
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { expect, test } from "vitest";
|
|
3
4
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
4
5
|
import { err, ok } from "./domain/result.ts";
|
|
@@ -20,6 +21,12 @@ import {
|
|
|
20
21
|
run,
|
|
21
22
|
STEPS,
|
|
22
23
|
} from "../test/submission-fixtures.ts";
|
|
24
|
+
import { frozenText } from "../test/frozen-presets.ts";
|
|
25
|
+
|
|
26
|
+
/** msr FROZEN (test/frozen-presets.ts): the shipped file's deadlines are the scheduled refresh's output. */
|
|
27
|
+
const frozenMsr = {
|
|
28
|
+
[join(presetsDir(), "msr.jsonc")]: frozenText("msr.jsonc"),
|
|
29
|
+
};
|
|
23
30
|
|
|
24
31
|
test("show: the portal from the preset, the id from the paper, the token from the site's variable", async () => {
|
|
25
32
|
const r = await run({});
|
|
@@ -130,6 +137,7 @@ test("show sends nothing, so it asks for no check", async () => {
|
|
|
130
137
|
|
|
131
138
|
test("show prints the current attempt's deadlines in force: the preset's portal readings, the paper's call readings and override over them", () => {
|
|
132
139
|
const fs = memoryFiles({
|
|
140
|
+
...frozenMsr,
|
|
133
141
|
"/w/package.json": "{}",
|
|
134
142
|
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
135
143
|
cycles: [
|
|
@@ -188,6 +196,7 @@ test("show prints the current attempt's deadlines in force: the preset's portal
|
|
|
188
196
|
|
|
189
197
|
test("show, in the flat form: the deadlines in force are the preset's readings alone", () => {
|
|
190
198
|
const flat = memoryFiles({
|
|
199
|
+
...frozenMsr,
|
|
191
200
|
"/w/package.json": "{}",
|
|
192
201
|
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
193
202
|
extends: "paperlint:msr",
|