Skip to content

Commit 4db785a

Browse files
committed
ci: test the Cloudflare worker; normalize action pins to latest
Adds worker/index.test.js (22 cases driving the fetch handler end-to-end with the upstream TSV fetches stubbed) and worker/package.json marking the worker as an ESM module. A new worker.yml workflow runs the tests via the Node built-in runner (no install) on worker/** changes; build.yml ignores worker/** so a worker-only change no longer triggers the Java build. Bumps every GitHub Actions pin to its latest release SHA: checkout v6.0.3, setup-node v6.4.0, upload-artifact v7.0.1 (aligned across all workflows).
1 parent 5880f84 commit 4db785a

9 files changed

Lines changed: 301 additions & 7 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ on:
44
push:
55
paths-ignore:
66
- 'data/**'
7+
- 'worker/**'
78
- '**/*.md'
89
pull_request:
910
paths-ignore:
1011
- 'data/**'
12+
- 'worker/**'
1113
- '**/*.md'
1214
workflow_dispatch:
1315

@@ -27,7 +29,7 @@ jobs:
2729

2830
steps:
2931
- name: Check out repository (with submodules)
30-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
32+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
3133
with:
3234
submodules: recursive
3335

.github/workflows/crawl.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575

7676
steps:
7777
- name: Check out repository
78-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
78+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
7979
with:
8080
fetch-depth: 50
8181
persist-credentials: true
@@ -140,7 +140,7 @@ jobs:
140140
141141
- name: Upload heap dump on failure
142142
if: ${{ failure() }}
143-
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
143+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
144144
with:
145145
name: crawl-heap-dump-${{ github.run_id }}
146146
path: crawl.hprof

.github/workflows/reconcile-metadata.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343

4444
steps:
4545
- name: Check out repository
46-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
46+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
4747
with:
4848
fetch-depth: 50
4949
persist-credentials: true
@@ -85,7 +85,7 @@ jobs:
8585
8686
- name: Upload heap dump on failure
8787
if: ${{ failure() }}
88-
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
88+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
8989
with:
9090
name: reconcile-heap-dump-${{ github.run_id }}
9191
path: reconcile.hprof

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
permissions:
1414
contents: write
1515
steps:
16-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
16+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
1717
with:
1818
fetch-depth: 0
1919
fetch-tags: true

.github/workflows/summary.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
steps:
1818
- name: Check out repository
19-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
2020
with:
2121
fetch-depth: 5
2222
persist-credentials: true

.github/workflows/worker.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Test Cloudflare Worker
2+
3+
# The worker (worker/index.js) is independent of the Java crawler, so it has its own
4+
# workflow gated on worker/** changes. Tests use the Node built-in test runner (node --test)
5+
# and have no dependencies, so there is no install step.
6+
on:
7+
push:
8+
paths:
9+
- 'worker/**'
10+
- '.github/workflows/worker.yml'
11+
pull_request:
12+
paths:
13+
- 'worker/**'
14+
- '.github/workflows/worker.yml'
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
test:
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 10
24+
25+
steps:
26+
- name: Check out repository
27+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
28+
29+
- name: Set up Node.js
30+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
31+
with:
32+
node-version: '22'
33+
34+
- name: Run worker tests
35+
working-directory: worker
36+
run: node --test

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ The contents of `data/modules/` are published as the **Jenesis Module Repository
263263

264264
The repository is intentionally a thin wrapper over the resolved views: every redirect target is derivable from a single row of `artifacts.tsv` or `modules.tsv`. Consumers that prefer to do their own lookup can read the TSVs directly via `raw.githubusercontent.com` (or any mirror); the worker exists so a Maven-style `<repository>` URL can be plugged in without writing a resolver.
265265

266+
The worker is covered by `worker/index.test.js`, which drives the `fetch` handler end-to-end (routing, TSV parsing, row selection, redirect-URL construction, error mapping) with the upstream TSV fetches stubbed. The tests use the Node built-in test runner and have no dependencies - run them with `node --test` from the `worker/` directory (Node 20+). The `.github/workflows/worker.yml` workflow runs them on every push or pull request that touches `worker/**`.
267+
266268
### URL shapes
267269

268270
Four modes, distinguished by the path segment immediately before the module name:

worker/index.test.js

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/**
2+
* Tests for the Cloudflare Worker in index.js.
3+
*
4+
* The worker is exercised end-to-end through its `fetch(request, env)` handler - the same
5+
* entry point Cloudflare invokes - so routing, TSV parsing, row selection, redirect-URL
6+
* construction and error mapping are all covered by real requests rather than by reaching
7+
* into private helpers. The only thing stubbed is the global `fetch` the worker uses to pull
8+
* `*.tsv` files from `DATA_BASE`: each test supplies the TSV bodies it needs keyed by the
9+
* path suffix the worker requests.
10+
*
11+
* Runs on the Node built-in test runner with no dependencies: `node --test` (Node 20+).
12+
* `Request`, `Response`, `URL` and `fetch` are Web-standard globals in Node 18+, matching
13+
* the workerd runtime closely enough for these resolution tests.
14+
*/
15+
import { test } from "node:test";
16+
import assert from "node:assert/strict";
17+
18+
import worker from "./index.js";
19+
20+
const DATA_BASE = "https://data.test/";
21+
const ARTIFACT_BASE = "https://maven.test/";
22+
const ENV = { DATA_BASE, ARTIFACT_BASE };
23+
24+
const tsv = (rows) => rows.map((cols) => cols.join("\t")).join("\n") + "\n";
25+
26+
// org.slf4j: a plain, named module with both an artifacts and a modules view.
27+
const SLF4J_ARTIFACTS = tsv([
28+
["2.0.10", "named", "org.slf4j", "slf4j-api"],
29+
["2.0.9", "named", "org.slf4j", "slf4j-api"],
30+
["1.7.36", "named", "org.slf4j", "slf4j-api"],
31+
]);
32+
const SLF4J_MODULES = tsv([
33+
["2.0.10", "org.slf4j", "slf4j-api", "2.0.10"],
34+
["2.0.9", "org.slf4j", "slf4j-api", "2.0.9"],
35+
]);
36+
// A classifier-scoped artifacts view (jackson's no_aopalliance variant).
37+
const JACKSON_NOAOP = tsv([
38+
["2.17.0", "named", "com.fasterxml.jackson.core", "jackson-core"],
39+
]);
40+
41+
const FIXTURES = {
42+
"org/slf4j/artifacts.tsv": SLF4J_ARTIFACTS,
43+
"org/slf4j/modules.tsv": SLF4J_MODULES,
44+
"com/fasterxml/jackson/core/artifacts-no_aopalliance.tsv": JACKSON_NOAOP,
45+
};
46+
47+
/**
48+
* Drive the worker for one request. `files` maps a TSV path suffix to either a string body
49+
* (served as 200) or `{ status, body }` to simulate upstream errors / 404s. Falls back to
50+
* FIXTURES, then to a 404, so most tests only declare what differs.
51+
*/
52+
async function call(path, { files = {}, env = ENV, method = "GET" } = {}) {
53+
const saved = globalThis.fetch;
54+
globalThis.fetch = async (url) => {
55+
const u = String(url);
56+
const table = { ...FIXTURES, ...files };
57+
for (const [suffix, value] of Object.entries(table)) {
58+
if (u.endsWith(suffix)) {
59+
if (typeof value === "string") {
60+
return new Response(value, { status: 200 });
61+
}
62+
return new Response(value.body ?? "", { status: value.status });
63+
}
64+
}
65+
return new Response("not found", { status: 404 });
66+
};
67+
try {
68+
const request = new Request("https://worker.test" + path, { method });
69+
return await worker.fetch(request, env);
70+
} finally {
71+
globalThis.fetch = saved;
72+
}
73+
}
74+
75+
test("root path redirects to the default home", async () => {
76+
const response = await call("/", { env: {} });
77+
assert.equal(response.status, 302);
78+
assert.equal(response.headers.get("Location"), "https://github.com/raphw/jenesis-modules");
79+
});
80+
81+
test("root path honours HOME_REDIRECT override", async () => {
82+
const response = await call("/", { env: { HOME_REDIRECT: "https://example.org/home" } });
83+
assert.equal(response.status, 302);
84+
assert.equal(response.headers.get("Location"), "https://example.org/home");
85+
});
86+
87+
test("non-GET/HEAD methods are rejected with 405 and an Allow header", async () => {
88+
const response = await call("/artifact/org.slf4j/org.slf4j.jar", { method: "POST" });
89+
assert.equal(response.status, 405);
90+
assert.equal(response.headers.get("Allow"), "GET, HEAD");
91+
});
92+
93+
test("HEAD is allowed and resolves like GET", async () => {
94+
const response = await call("/artifact/org.slf4j/org.slf4j.jar", { method: "HEAD" });
95+
assert.equal(response.status, 302);
96+
assert.equal(
97+
response.headers.get("Location"),
98+
"https://maven.test/org/slf4j/slf4j-api/2.0.10/slf4j-api-2.0.10.jar",
99+
);
100+
});
101+
102+
test("a path with too few segments is a 404", async () => {
103+
const response = await call("/artifact/org.slf4j");
104+
assert.equal(response.status, 404);
105+
});
106+
107+
test("artifact mode without a version redirects to the newest Maven coordinate", async () => {
108+
const response = await call("/artifact/org.slf4j/org.slf4j.jar");
109+
assert.equal(response.status, 302);
110+
assert.equal(
111+
response.headers.get("Location"),
112+
"https://maven.test/org/slf4j/slf4j-api/2.0.10/slf4j-api-2.0.10.jar",
113+
);
114+
assert.equal(response.headers.get("X-Jenesis-GroupId"), "org.slf4j");
115+
assert.equal(response.headers.get("X-Jenesis-ArtifactId"), "slf4j-api");
116+
assert.equal(response.headers.get("X-Jenesis-MavenVersion"), "2.0.10");
117+
});
118+
119+
test("artifact mode resolves an explicit Maven version present in the TSV", async () => {
120+
const response = await call("/artifact/org.slf4j/2.0.9/org.slf4j.jar");
121+
assert.equal(response.status, 302);
122+
assert.equal(
123+
response.headers.get("Location"),
124+
"https://maven.test/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar",
125+
);
126+
});
127+
128+
test("artifact mode passes the request extension through verbatim (.pom)", async () => {
129+
const response = await call("/artifact/org.slf4j/2.0.9/org.slf4j.pom");
130+
assert.equal(response.status, 302);
131+
assert.equal(
132+
response.headers.get("Location"),
133+
"https://maven.test/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.pom",
134+
);
135+
});
136+
137+
test("artifact mode passes a multi-dot extension through (.pom.sha256)", async () => {
138+
const response = await call("/artifact/org.slf4j/2.0.9/org.slf4j.pom.sha256");
139+
assert.equal(response.status, 302);
140+
assert.equal(
141+
response.headers.get("Location"),
142+
"https://maven.test/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.pom.sha256",
143+
);
144+
});
145+
146+
test("a classifier flips the lookup to the classifier-scoped TSV and decorates the filename", async () => {
147+
const response = await call(
148+
"/artifact/com.fasterxml.jackson.core/com.fasterxml.jackson.core-no_aopalliance.pom",
149+
);
150+
assert.equal(response.status, 302);
151+
assert.equal(
152+
response.headers.get("Location"),
153+
"https://maven.test/com/fasterxml/jackson/core/jackson-core/2.17.0/jackson-core-2.17.0-no_aopalliance.pom",
154+
);
155+
});
156+
157+
test("module mode resolves via modules.tsv (module-info version keys the row)", async () => {
158+
const response = await call("/module/org.slf4j/2.0.9/org.slf4j.jar");
159+
assert.equal(response.status, 302);
160+
assert.equal(
161+
response.headers.get("Location"),
162+
"https://maven.test/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar",
163+
);
164+
assert.equal(response.headers.get("X-Jenesis-ModuleVersion"), "2.0.9");
165+
assert.equal(response.headers.get("X-Jenesis-MavenVersion"), "2.0.9");
166+
});
167+
168+
test("module mode only accepts a .jar filename", async () => {
169+
const response = await call("/module/org.slf4j/org.slf4j.pom");
170+
assert.equal(response.status, 404);
171+
});
172+
173+
test("sources mode synthesises the -sources.jar URL", async () => {
174+
const response = await call("/sources/org.slf4j/2.0.9/org.slf4j.jar");
175+
assert.equal(response.status, 302);
176+
assert.equal(
177+
response.headers.get("Location"),
178+
"https://maven.test/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9-sources.jar",
179+
);
180+
});
181+
182+
test("documentation mode synthesises the -javadoc.jar URL", async () => {
183+
const response = await call("/documentation/org.slf4j/2.0.9/org.slf4j.jar");
184+
assert.equal(response.status, 302);
185+
assert.equal(
186+
response.headers.get("Location"),
187+
"https://maven.test/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9-javadoc.jar",
188+
);
189+
});
190+
191+
test("an unknown explicit version resolves best-effort against the newest coordinate", async () => {
192+
const response = await call("/artifact/org.slf4j/9.9.9/org.slf4j.jar");
193+
assert.equal(response.status, 302);
194+
assert.equal(
195+
response.headers.get("Location"),
196+
"https://maven.test/org/slf4j/slf4j-api/9.9.9/slf4j-api-9.9.9.jar",
197+
);
198+
assert.equal(response.headers.get("X-Jenesis-BestEffort"), "true");
199+
});
200+
201+
test("an unknown module (TSV 404 upstream) returns 404", async () => {
202+
const response = await call("/artifact/org.unknown/org.unknown.jar", {
203+
files: { "org/unknown/artifacts.tsv": { status: 404 } },
204+
});
205+
assert.equal(response.status, 404);
206+
});
207+
208+
test("an empty TSV with no rows returns 404", async () => {
209+
const response = await call("/artifact/org.empty/org.empty.jar", {
210+
files: { "org/empty/artifacts.tsv": "" },
211+
});
212+
assert.equal(response.status, 404);
213+
});
214+
215+
test("a non-404 upstream error maps to 502", async () => {
216+
const response = await call("/artifact/org.slf4j/org.slf4j.jar", {
217+
files: { "org/slf4j/artifacts.tsv": { status: 500 } },
218+
});
219+
assert.equal(response.status, 502);
220+
});
221+
222+
test("path segments before the mode marker are ignored", async () => {
223+
const response = await call("/jenesis/v1/artifact/org.slf4j/org.slf4j.jar");
224+
assert.equal(response.status, 302);
225+
assert.equal(
226+
response.headers.get("Location"),
227+
"https://maven.test/org/slf4j/slf4j-api/2.0.10/slf4j-api-2.0.10.jar",
228+
);
229+
});
230+
231+
test("the module-name segment must prefix the filename", async () => {
232+
const response = await call("/artifact/org.slf4j/com.other.jar");
233+
assert.equal(response.status, 404);
234+
});
235+
236+
test("an invalid module name (hyphen in a segment) is a 404", async () => {
237+
const response = await call("/artifact/foo-bar/foo-bar.jar");
238+
assert.equal(response.status, 404);
239+
});
240+
241+
test("an unknown mode marker is a 404", async () => {
242+
const response = await call("/bogus/org.slf4j/org.slf4j.jar");
243+
assert.equal(response.status, 404);
244+
});

worker/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "jenesis-modules-worker",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "Cloudflare Worker resolving Java module names to Maven Central artifacts",
6+
"type": "module",
7+
"scripts": {
8+
"test": "node --test"
9+
}
10+
}

0 commit comments

Comments
 (0)