create-daloy 1.0.0-beta.1 → 1.0.0-beta.3
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/README.md +11 -7
- package/bin/create-daloy.mjs +23 -7
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/_ci/deno/SECURITY.md +4 -1
- package/templates/_ci/deno/_github/workflows/eol-scan.yml +57 -0
- package/templates/_ci/deno/scripts/verify-runtime-eol.mjs +211 -0
- package/templates/_ci/node/SECURITY.md +2 -1
- package/templates/_ci/node/_github/workflows/eol-scan.yml +67 -0
- package/templates/_ci/node/scripts/verify-runtime-eol.mjs +211 -0
- package/templates/bun-basic/_githooks/pre-push +4 -4
- package/templates/bun-basic/package.json +1 -1
- package/templates/cloudflare-worker/_githooks/pre-push +5 -5
- package/templates/cloudflare-worker/package.json +2 -2
- package/templates/deno-basic/_githooks/pre-push +4 -4
- package/templates/deno-basic/deno.json +5 -5
- package/templates/node-basic/_githooks/pre-push +5 -5
- package/templates/node-basic/package.json +3 -3
- package/templates/vercel/_githooks/pre-push +5 -5
- package/templates/vercel/package.json +3 -3
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ pnpm create daloy@latest my-api \
|
|
|
50
50
|
|
|
51
51
|
| Flag | Description |
|
|
52
52
|
| --- | --- |
|
|
53
|
-
| `--template <name>` | `node-basic` (default), `vercel`, `cloudflare-worker`, `bun-basic`, or `deno-basic
|
|
53
|
+
| `--template <name>` | `node-basic` (default), `vercel`, `cloudflare-worker`, `bun-basic`, or `deno-basic`, `vercel`. |
|
|
54
54
|
| `--package-manager <pm>` | `pnpm` (default), `npm`, `yarn`, or `bun`. Ignored for `deno-basic`. |
|
|
55
55
|
| `--list-templates` | Print available templates with descriptions. |
|
|
56
56
|
| `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to **Y** for npm/yarn/bun and **N** for pnpm so you can review the hardened `.npmrc` / `pnpm-workspace.yaml` and aren't blocked by the 24h `minimumReleaseAge` embargo on the first run. |
|
|
@@ -100,8 +100,6 @@ for standalone functions, on Fluid Compute) using `@daloyjs/core/vercel` with:
|
|
|
100
100
|
- `secureHeaders` and `requestId` enabled by default, with smaller serverless-friendly body and timeout limits.
|
|
101
101
|
- A health route and bookstore route mirroring the Node starter.
|
|
102
102
|
|
|
103
|
-
> The previous template name `vercel-edge` still works as a deprecated alias for `vercel`.
|
|
104
|
-
|
|
105
103
|
### `bun-basic`
|
|
106
104
|
|
|
107
105
|
A [Bun](https://bun.sh) runtime starter using `@daloyjs/core/bun` with:
|
|
@@ -198,6 +196,11 @@ For Node-style templates, the bundle adds the following.
|
|
|
198
196
|
[npm-audit-guide](https://www.aikido.dev/blog/npm-audit-guide) write-ups warn
|
|
199
197
|
about, and the Deno scaffold gets it too (Deno has no `audit` built in, so
|
|
200
198
|
without OSV-Scanner a Deno scaffold would have no scheduled SCA at all).
|
|
199
|
+
- `.github/workflows/eol-scan.yml` — daily runtime end-of-life detection for
|
|
200
|
+
pinned Node, Bun, and Deno versions in package metadata and GitHub Actions.
|
|
201
|
+
A runtime can keep running after it stops receiving security patches; this
|
|
202
|
+
job fails once a pinned runtime cycle is already EOL and warns within 90 days
|
|
203
|
+
of EOL.
|
|
201
204
|
|
|
202
205
|
**Secret and static analysis**
|
|
203
206
|
|
|
@@ -237,8 +240,9 @@ out a reusable package, opt into npm trusted publishing yourself.
|
|
|
237
240
|
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow, a manual-only
|
|
238
241
|
container publish starter for GHCR that is guarded to `main` or a tag by
|
|
239
242
|
default, plus CodeQL, Opengrep, **OSV-Scanner** (the only scheduled SCA layer
|
|
240
|
-
a Deno scaffold has, since Deno ships no `audit`),
|
|
241
|
-
Dependabot for GitHub Actions, CODEOWNERS, and
|
|
243
|
+
a Deno scaffold has, since Deno ships no `audit`), runtime EOL scanning,
|
|
244
|
+
Scorecard, zizmor, Dependabot for GitHub Actions, CODEOWNERS, and
|
|
245
|
+
`SECURITY.md`.
|
|
242
246
|
|
|
243
247
|
If you want the governance bundle but not the deployment starter, pass
|
|
244
248
|
`--with-ci --no-deploy`. If you only want a deployment starter, pass
|
|
@@ -251,7 +255,7 @@ required status checks in the repository settings.
|
|
|
251
255
|
|
|
252
256
|
## Container-first scaffolds
|
|
253
257
|
|
|
254
|
-
Every template (Node, Bun, Vercel
|
|
258
|
+
Every template (Node, Bun, Vercel, Cloudflare Worker, and Deno) ships a
|
|
255
259
|
production-oriented `Dockerfile` and `.dockerignore` with secure-by-default
|
|
256
260
|
posture: a non-root user, `STOPSIGNAL SIGTERM`, `tini` as PID 1, and a
|
|
257
261
|
`HEALTHCHECK` pointed at `/readyz`. Node-style templates also ship an
|
|
@@ -276,6 +280,6 @@ Every scaffolded project ships with two files that help AI coding agents (Copilo
|
|
|
276
280
|
- `AGENTS.md` (repo root) — a small, top-of-context file (per the open [AGENTS.md](https://agents.md) convention): one-line project description, package manager / runtime, project shape, core rules, and the few commands an agent needs. It links to the full skill below.
|
|
277
281
|
- `.agents/skills/daloyjs-best-practices/SKILL.md` — comprehensive operational guidance following the open `agents/skills/<skill-name>/SKILL.md` convention: when to use the skill, project structure, core workflows (adding routes, regenerating the OpenAPI spec and client), schema and validation conventions, error-handling patterns, middleware order, testing best practices (happy and unhappy paths), security best practices, logging and observability notes, configuration and secrets handling, deployment notes, pitfalls and guardrails, and process expectations.
|
|
278
282
|
|
|
279
|
-
Both files are tailored to the chosen template (Node, Bun, Deno, Vercel
|
|
283
|
+
Both files are tailored to the chosen template (Node, Bun, Deno, Vercel, or Cloudflare Workers), and Node-style templates rewrite their commands to match your selected package manager. They follow the "instruction budget" advice — small root file, progressive disclosure for the rest — so they don't waste agent tokens. Edit or delete them freely; the framework does not depend on them at runtime.
|
|
280
284
|
|
|
281
285
|
Every scaffold also ships a `.vscode/mcp.json` (authored as `_vscode/mcp.json` in the template) that wires VS Code and other MCP-aware editors to the DaloyJS documentation MCP server (`https://daloyjs.dev/mcp`) over HTTP. AI assistants in your editor can then pull current DaloyJS docs with no manual setup. Delete the file or remove the server entry to opt out; it is editor configuration only and the framework does not depend on it at runtime.
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -58,9 +58,8 @@ const PACKAGE_MANAGERS = PACKAGE_MANAGER_OPTIONS.map((option) => option.value);
|
|
|
58
58
|
// `--template <old>` commands (and published docs/blog posts) keep working.
|
|
59
59
|
// Each maps to its current canonical template value.
|
|
60
60
|
const TEMPLATE_ALIASES = new Map([
|
|
61
|
-
// `vercel
|
|
61
|
+
// `vercel` shipped before Vercel deprecated standalone Edge Functions;
|
|
62
62
|
// the template now targets the recommended Node.js runtime as `vercel`.
|
|
63
|
-
["vercel-edge", "vercel"],
|
|
64
63
|
]);
|
|
65
64
|
|
|
66
65
|
const RENAME_ON_COPY = new Map([
|
|
@@ -1138,14 +1137,26 @@ async function writePackageJson(dir, packageJson) {
|
|
|
1138
1137
|
await writeFile(path.join(dir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n", "utf8");
|
|
1139
1138
|
}
|
|
1140
1139
|
|
|
1141
|
-
async function
|
|
1140
|
+
async function addSecurityScripts(dir, packageManager) {
|
|
1142
1141
|
const packageJson = await readPackageJsonIfPresent(dir);
|
|
1143
1142
|
if (!packageJson) return;
|
|
1144
1143
|
packageJson.scripts ??= {};
|
|
1145
1144
|
packageJson.scripts["verify:lockfile"] = "node scripts/verify-lockfile-sources.mjs";
|
|
1145
|
+
packageJson.scripts["verify:runtime-eol"] =
|
|
1146
|
+
packageManager === "bun" ? "bun scripts/verify-runtime-eol.mjs" : "node scripts/verify-runtime-eol.mjs";
|
|
1146
1147
|
await writePackageJson(dir, packageJson);
|
|
1147
1148
|
}
|
|
1148
1149
|
|
|
1150
|
+
async function addDenoSecurityTasks(dir) {
|
|
1151
|
+
const file = path.join(dir, "deno.json");
|
|
1152
|
+
if (!existsSync(file)) return;
|
|
1153
|
+
const denoJson = JSON.parse(await readFile(file, "utf8"));
|
|
1154
|
+
denoJson.tasks ??= {};
|
|
1155
|
+
denoJson.tasks["verify:runtime-eol"] =
|
|
1156
|
+
"deno run --allow-read --allow-net=endoflife.date scripts/verify-runtime-eol.mjs";
|
|
1157
|
+
await writeFile(file, JSON.stringify(denoJson, null, 2) + "\n", "utf8");
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1149
1160
|
function renderCiReplacements({ packageManager, template, packageJson, codeOwner, includeSecurityBundle = true }) {
|
|
1150
1161
|
const setupPm = setupPackageManagerStep(packageManager);
|
|
1151
1162
|
const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
|
|
@@ -1184,6 +1195,7 @@ function renderCiReplacements({ packageManager, template, packageJson, codeOwner
|
|
|
1184
1195
|
["__SETUP_BUN_RUNTIME_STEP__", needsBunRuntime ? setupBunStep() : ""],
|
|
1185
1196
|
["__INSTALL_COMMAND__", installCommand(packageManager)],
|
|
1186
1197
|
["__VERIFY_LOCKFILE_COMMAND__", runScriptCommand(packageManager, "verify:lockfile")],
|
|
1198
|
+
["__RUNTIME_EOL_COMMAND__", runScriptCommand(packageManager, "verify:runtime-eol")],
|
|
1187
1199
|
["__TYPECHECK_COMMAND__", runScriptCommand(packageManager, "typecheck")],
|
|
1188
1200
|
["__TEST_COMMAND__", runScriptCommand(packageManager, "test")],
|
|
1189
1201
|
["__BUILD_STEP__", buildStep],
|
|
@@ -1218,14 +1230,15 @@ async function replacePlaceholdersInTree(dir, replacements) {
|
|
|
1218
1230
|
async function pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow }) {
|
|
1219
1231
|
if (!includeSecurityBundle) {
|
|
1220
1232
|
const workflowFiles = flavor === "deno"
|
|
1221
|
-
? ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "zizmor.yml"]
|
|
1222
|
-
: ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "vuln-scan.yml", "zizmor.yml"];
|
|
1233
|
+
? ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "eol-scan.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "zizmor.yml"]
|
|
1234
|
+
: ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "eol-scan.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "vuln-scan.yml", "zizmor.yml"];
|
|
1223
1235
|
for (const file of workflowFiles) {
|
|
1224
1236
|
await rm(path.join(targetDir, ".github", "workflows", file), { force: true });
|
|
1225
1237
|
}
|
|
1226
1238
|
await rm(path.join(targetDir, ".github", "dependabot.yml"), { force: true });
|
|
1227
1239
|
await rm(path.join(targetDir, ".github", "CODEOWNERS"), { force: true });
|
|
1228
1240
|
await rm(path.join(targetDir, "SECURITY.md"), { force: true });
|
|
1241
|
+
await rm(path.join(targetDir, "scripts", "verify-runtime-eol.mjs"), { force: true });
|
|
1229
1242
|
if (flavor === "node") {
|
|
1230
1243
|
await rm(path.join(targetDir, "scripts", "verify-lockfile-sources.mjs"), { force: true });
|
|
1231
1244
|
}
|
|
@@ -1265,6 +1278,9 @@ async function copyCiBundle(
|
|
|
1265
1278
|
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1266
1279
|
);
|
|
1267
1280
|
}
|
|
1281
|
+
if (includeSecurityBundle) {
|
|
1282
|
+
await addDenoSecurityTasks(targetDir);
|
|
1283
|
+
}
|
|
1268
1284
|
await replacePlaceholdersInTree(targetDir, new Map([["__CODE_OWNER__", owner]]));
|
|
1269
1285
|
return;
|
|
1270
1286
|
}
|
|
@@ -1275,7 +1291,7 @@ async function copyCiBundle(
|
|
|
1275
1291
|
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1276
1292
|
);
|
|
1277
1293
|
}
|
|
1278
|
-
await
|
|
1294
|
+
await addSecurityScripts(targetDir, packageManager);
|
|
1279
1295
|
}
|
|
1280
1296
|
const packageJson = await readPackageJsonIfPresent(targetDir);
|
|
1281
1297
|
await replacePlaceholdersInTree(
|
|
@@ -1802,7 +1818,7 @@ async function main() {
|
|
|
1802
1818
|
if (!template) {
|
|
1803
1819
|
template = rl ? await askChoice(rl, "Choose a starter template:", TEMPLATE_OPTIONS, "node-basic") : "node-basic";
|
|
1804
1820
|
}
|
|
1805
|
-
// Resolve deprecated template aliases
|
|
1821
|
+
// Resolve deprecated template aliases.
|
|
1806
1822
|
if (TEMPLATE_ALIASES.has(template)) {
|
|
1807
1823
|
const canonical = TEMPLATE_ALIASES.get(template);
|
|
1808
1824
|
logWarn(`Template "${template}" is deprecated; using "${canonical}" instead.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-daloy",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/sbom.cdx.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:8f52f21d-0a1b-58e6-bf02-59600dc6cbef",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-06-
|
|
7
|
+
"timestamp": "2026-06-24T05:34:06.627Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.0.0-beta.
|
|
12
|
+
"version": "1.0.0-beta.3"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "library",
|
|
18
|
-
"bom-ref": "pkg:npm/create-daloy@1.0.0-beta.
|
|
18
|
+
"bom-ref": "pkg:npm/create-daloy@1.0.0-beta.3",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "1.0.0-beta.
|
|
20
|
+
"version": "1.0.0-beta.3",
|
|
21
21
|
"description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
|
|
22
|
-
"purl": "pkg:npm/create-daloy@1.0.0-beta.
|
|
22
|
+
"purl": "pkg:npm/create-daloy@1.0.0-beta.3",
|
|
23
23
|
"licenses": [
|
|
24
24
|
{
|
|
25
25
|
"license": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
}
|
|
43
43
|
],
|
|
44
44
|
"swid": {
|
|
45
|
-
"tagId": "swidtag-create-daloy-1.0.0-beta.
|
|
45
|
+
"tagId": "swidtag-create-daloy-1.0.0-beta.3",
|
|
46
46
|
"name": "create-daloy",
|
|
47
|
-
"version": "1.0.0-beta.
|
|
47
|
+
"version": "1.0.0-beta.3",
|
|
48
48
|
"tagVersion": 0,
|
|
49
49
|
"patch": false
|
|
50
50
|
}
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"components": [],
|
|
54
54
|
"dependencies": [
|
|
55
55
|
{
|
|
56
|
-
"ref": "pkg:npm/create-daloy@1.0.0-beta.
|
|
56
|
+
"ref": "pkg:npm/create-daloy@1.0.0-beta.3",
|
|
57
57
|
"dependsOn": []
|
|
58
58
|
}
|
|
59
59
|
]
|
package/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "create-daloy-1.0.0-beta.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-beta.
|
|
5
|
+
"name": "create-daloy-1.0.0-beta.3",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-beta.3-8f52f21d-0a1b-58e6-bf02-59600dc6cbef",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-06-
|
|
8
|
+
"created": "2026-06-24T05:34:06.627Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package-create-daloy",
|
|
18
18
|
"name": "create-daloy",
|
|
19
|
-
"versionInfo": "1.0.0-beta.
|
|
19
|
+
"versionInfo": "1.0.0-beta.3",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/create-daloy@1.0.0-beta.
|
|
30
|
+
"referenceLocator": "pkg:npm/create-daloy@1.0.0-beta.3"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
|
@@ -27,6 +27,9 @@ The `--with-ci` bundle adds these defaults:
|
|
|
27
27
|
Semgrep, verified via sigstore cosign before execution), OpenSSF
|
|
28
28
|
Scorecard, zizmor, Dependabot for GitHub Actions and Docker
|
|
29
29
|
base images, and CODEOWNERS are generated.
|
|
30
|
+
- A daily scheduled `eol-scan.yml` checks pinned Deno versions in `deno.json`
|
|
31
|
+
and GitHub Actions against endoflife.date so a runtime that has stopped
|
|
32
|
+
receiving security patches does not stay unnoticed on `main`.
|
|
30
33
|
- A `secret-scan.yml` workflow runs [gitleaks](https://github.com/gitleaks/gitleaks) against the working tree on every PR / push and against the **full git history** on a daily schedule. The gitleaks binary is downloaded from a pinned official release and verified by SHA-256 before execution so the scan does not introduce a new third-party action into the supply chain. Findings block the merge; matched values are redacted from the public log. The rationale (and the remediation playbook for a confirmed leak) follows Aikido's [Secrets Detection guide](https://www.aikido.dev/blog/secret-detection-application-security): a secret in any commit, branch, or tag should be treated as compromised, and detection must consider the entire history — not just the latest snapshot — alongside GitHub-native push protection.
|
|
31
34
|
|
|
32
35
|
## Cloud posture (operator checklist)
|
|
@@ -62,4 +65,4 @@ Before relying on these files for a company project:
|
|
|
62
65
|
1. Replace `@your-org/security-team` in `.github/CODEOWNERS` or pass `--code-owner` when scaffolding.
|
|
63
66
|
2. Protect the `main` branch and require the CI, CodeQL, Opengrep, Scorecard, and zizmor checks.
|
|
64
67
|
3. Enable GitHub secret scanning and push protection.
|
|
65
|
-
4. Keep Deno permissions narrow; do not switch tasks to `--allow-all`.
|
|
68
|
+
4. Keep Deno permissions narrow; do not switch tasks to `--allow-all`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# End-of-life runtime scan generated by create-daloy --with-ci.
|
|
2
|
+
#
|
|
3
|
+
# Why this exists:
|
|
4
|
+
# A Deno version can keep running after it stops receiving security patches.
|
|
5
|
+
# This scheduled check looks at pinned runtime versions in deno.json and
|
|
6
|
+
# GitHub Actions, then compares them with endoflife.date so old runtime pins
|
|
7
|
+
# do not sit unnoticed on main.
|
|
8
|
+
#
|
|
9
|
+
# Hardening:
|
|
10
|
+
# * `permissions: {}` at the top level; the job opts in to contents: read.
|
|
11
|
+
# * Third-party actions are SHA-pinned and managed by Dependabot.
|
|
12
|
+
# * `step-security/harden-runner` runs in audit mode.
|
|
13
|
+
# * `actions/checkout` runs with `persist-credentials: false`.
|
|
14
|
+
|
|
15
|
+
name: EOL runtime scan
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
schedule:
|
|
19
|
+
# 08:21 UTC daily, offset from the other scheduled security scans.
|
|
20
|
+
- cron: "21 8 * * *"
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
|
|
23
|
+
permissions: {}
|
|
24
|
+
|
|
25
|
+
concurrency:
|
|
26
|
+
group: eol-scan-${{ github.workflow }}-${{ github.ref }}
|
|
27
|
+
cancel-in-progress: true
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
eol:
|
|
31
|
+
name: Daily EOL runtime scan
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 10
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- name: Harden runner
|
|
39
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
40
|
+
with:
|
|
41
|
+
egress-policy: audit
|
|
42
|
+
disable-sudo: true
|
|
43
|
+
disable-file-monitoring: false
|
|
44
|
+
|
|
45
|
+
- name: Checkout
|
|
46
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
47
|
+
with:
|
|
48
|
+
persist-credentials: false
|
|
49
|
+
show-progress: false
|
|
50
|
+
|
|
51
|
+
- name: Set up Deno
|
|
52
|
+
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
|
53
|
+
with:
|
|
54
|
+
deno-version: v2.x
|
|
55
|
+
|
|
56
|
+
- name: Verify pinned runtimes are not EOL
|
|
57
|
+
run: deno task verify:runtime-eol
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const EOL_WARN_DAYS = 90;
|
|
2
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
3
|
+
const RUNTIME_FEEDS = {
|
|
4
|
+
node: "nodejs",
|
|
5
|
+
bun: "bun",
|
|
6
|
+
deno: "deno",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isDeno = typeof globalThis.Deno !== "undefined";
|
|
10
|
+
const args = isDeno ? globalThis.Deno.args : globalThis.process.argv.slice(2);
|
|
11
|
+
const cwd = isDeno ? globalThis.Deno.cwd() : globalThis.process.cwd();
|
|
12
|
+
|
|
13
|
+
function joinPath(...parts) {
|
|
14
|
+
return parts.filter(Boolean).join("/").replace(/\/+/g, "/");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readText(file) {
|
|
18
|
+
if (isDeno) return await globalThis.Deno.readTextFile(file);
|
|
19
|
+
const { readFile } = await import("node:fs/promises");
|
|
20
|
+
return await readFile(file, "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readDirNames(dir) {
|
|
24
|
+
if (isDeno) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for await (const entry of globalThis.Deno.readDir(dir)) out.push(entry.name);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
const { readdir } = await import("node:fs/promises");
|
|
30
|
+
return await readdir(dir);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseVersionKey(value) {
|
|
34
|
+
if (typeof value !== "string") return null;
|
|
35
|
+
if (/latest/i.test(value)) return null;
|
|
36
|
+
const match = value.match(/v?\D*(\d+)(?:\.(\d+))?/);
|
|
37
|
+
if (!match) return null;
|
|
38
|
+
return match[2] ? `${Number(match[1])}.${Number(match[2])}` : String(Number(match[1]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectFromPackageJson(json, source) {
|
|
42
|
+
const out = [];
|
|
43
|
+
const engines = json && typeof json === "object" ? json.engines : null;
|
|
44
|
+
if (!engines || typeof engines !== "object") return out;
|
|
45
|
+
for (const runtime of Object.keys(RUNTIME_FEEDS)) {
|
|
46
|
+
const version = parseVersionKey(engines[runtime]);
|
|
47
|
+
if (version) out.push({ runtime, version, source: `${source}: engines.${runtime}` });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectFromDenoJson(json, source) {
|
|
53
|
+
const out = [];
|
|
54
|
+
const version = parseVersionKey(json?.runtime?.deno ?? json?.engines?.deno);
|
|
55
|
+
if (version) out.push({ runtime: "deno", version, source: `${source}: deno runtime` });
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectFromWorkflow(yaml, source) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const patterns = [
|
|
62
|
+
["node", /node-version:\s*['"]?([^'"\n#]+)/g],
|
|
63
|
+
["bun", /bun-version:\s*['"]?([^'"\n#]+)/g],
|
|
64
|
+
["deno", /deno-version:\s*['"]?([^'"\n#]+)/g],
|
|
65
|
+
];
|
|
66
|
+
for (const [runtime, pattern] of patterns) {
|
|
67
|
+
let match = null;
|
|
68
|
+
while ((match = pattern.exec(yaml)) !== null) {
|
|
69
|
+
const version = parseVersionKey(match[1]);
|
|
70
|
+
if (version) out.push({ runtime, version, source });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function collectPinnedVersions() {
|
|
77
|
+
const found = [];
|
|
78
|
+
try {
|
|
79
|
+
found.push(...collectFromPackageJson(JSON.parse(await readText(joinPath(cwd, "package.json"))), "package.json"));
|
|
80
|
+
} catch {
|
|
81
|
+
// No package.json is fine for Deno-native scaffolds.
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
found.push(...collectFromDenoJson(JSON.parse(await readText(joinPath(cwd, "deno.json"))), "deno.json"));
|
|
85
|
+
} catch {
|
|
86
|
+
// Node/Bun scaffolds do not ship deno.json.
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const workflowsDir = joinPath(cwd, ".github", "workflows");
|
|
90
|
+
for (const name of await readDirNames(workflowsDir)) {
|
|
91
|
+
if (!/\.(ya?ml)$/.test(name)) continue;
|
|
92
|
+
found.push(...collectFromWorkflow(await readText(joinPath(workflowsDir, name)), `.github/workflows/${name}`));
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// A local run without GitHub workflows should still scan package metadata.
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchFeed(runtime) {
|
|
101
|
+
const feed = RUNTIME_FEEDS[runtime];
|
|
102
|
+
const ac = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(`https://endoflife.date/api/${feed}.json`, {
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": "daloy-runtime-eol-scan/1",
|
|
108
|
+
Accept: "application/json",
|
|
109
|
+
},
|
|
110
|
+
signal: ac.signal,
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) throw new Error(`${feed} feed returned HTTP ${res.status}`);
|
|
113
|
+
const json = await res.json();
|
|
114
|
+
if (!Array.isArray(json)) throw new Error(`${feed} feed returned a non-array payload`);
|
|
115
|
+
return json;
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cycleNumber(cycle) {
|
|
122
|
+
const n = Number.parseFloat(String(cycle));
|
|
123
|
+
return Number.isFinite(n) ? n : -1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findCycle(feed, version) {
|
|
127
|
+
const exact = feed.find((item) => String(item.cycle) === version);
|
|
128
|
+
if (exact) return exact;
|
|
129
|
+
if (!/^\d+$/.test(version)) return null;
|
|
130
|
+
const major = Number(version);
|
|
131
|
+
const candidates = feed
|
|
132
|
+
.filter((item) => Math.floor(cycleNumber(item.cycle)) === major)
|
|
133
|
+
.sort((a, b) => cycleNumber(b.cycle) - cycleNumber(a.cycle));
|
|
134
|
+
return candidates[0] ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function daysUntil(date, now) {
|
|
138
|
+
return Math.floor((date.getTime() - now.getTime()) / 86_400_000);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function groupPinned(pinned) {
|
|
142
|
+
const grouped = new Map();
|
|
143
|
+
for (const pin of pinned) {
|
|
144
|
+
const key = `${pin.runtime}@${pin.version}`;
|
|
145
|
+
const current = grouped.get(key);
|
|
146
|
+
if (current) current.sources.add(pin.source);
|
|
147
|
+
else grouped.set(key, { ...pin, sources: new Set([pin.source]) });
|
|
148
|
+
}
|
|
149
|
+
return [...grouped.values()];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function failProcess() {
|
|
153
|
+
if (globalThis.process) {
|
|
154
|
+
globalThis.process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (isDeno) globalThis.Deno.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function main() {
|
|
161
|
+
const strict = args.includes("--strict");
|
|
162
|
+
const now = new Date();
|
|
163
|
+
const findings = [];
|
|
164
|
+
const pinned = groupPinned(await collectPinnedVersions());
|
|
165
|
+
|
|
166
|
+
if (pinned.length === 0) {
|
|
167
|
+
console.log("verify-runtime-eol: no pinned runtime versions found.");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const feeds = new Map();
|
|
172
|
+
for (const pin of pinned) {
|
|
173
|
+
if (!feeds.has(pin.runtime)) feeds.set(pin.runtime, await fetchFeed(pin.runtime));
|
|
174
|
+
const cycle = findCycle(feeds.get(pin.runtime), pin.version);
|
|
175
|
+
if (!cycle || cycle.eol === false) continue;
|
|
176
|
+
const eol = new Date(cycle.eol);
|
|
177
|
+
if (Number.isNaN(eol.getTime())) continue;
|
|
178
|
+
const remaining = daysUntil(eol, now);
|
|
179
|
+
if (remaining < 0 || remaining <= EOL_WARN_DAYS) {
|
|
180
|
+
findings.push({
|
|
181
|
+
runtime: pin.runtime,
|
|
182
|
+
version: pin.version,
|
|
183
|
+
cycle: cycle.cycle,
|
|
184
|
+
eol: cycle.eol,
|
|
185
|
+
days: remaining,
|
|
186
|
+
severity: remaining < 0 ? "eol" : "warn",
|
|
187
|
+
sources: [...pin.sources].sort(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const finding of findings) {
|
|
193
|
+
const label = finding.severity === "eol" ? "EOL" : "WARNING";
|
|
194
|
+
const timing =
|
|
195
|
+
finding.severity === "eol"
|
|
196
|
+
? `${Math.abs(finding.days)} day(s) ago`
|
|
197
|
+
: `in ${finding.days} day(s)`;
|
|
198
|
+
console.error(
|
|
199
|
+
`${label}: ${finding.runtime} ${finding.version} maps to cycle ${finding.cycle}, EOL ${timing} (${finding.eol}). Sources: ${finding.sources.join(", ")}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (findings.some((f) => f.severity === "eol") || (strict && findings.length > 0)) {
|
|
204
|
+
failProcess();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log("verify-runtime-eol: all pinned runtimes are inside supported windows.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await main();
|
|
@@ -25,6 +25,7 @@ The `--with-ci` bundle adds these defaults:
|
|
|
25
25
|
- CodeQL, OpenSSF Scorecard, zizmor, Dependabot, and CODEOWNERS are generated.
|
|
26
26
|
- A weekly scheduled `opengrep.yml` runs a second SAST engine (Aikido's LGPL-2.1 fork of Semgrep) against the repository using the curated `p/security-audit`, `p/owasp-top-ten`, `p/cwe-top-25`, `p/javascript`, `p/typescript`, `p/nodejs`, and `p/secrets` rule packs. The Opengrep binary is downloaded from a pinned GitHub release and its sigstore cosign signature is verified against the official `opengrep/opengrep` release identity before it runs — no third-party action sits in the supply chain just for this scan. Running two SAST engines (CodeQL + Opengrep) catches different bug classes than either alone; see Aikido's [Ultimate SAST Guide](https://www.aikido.dev/blog/ultimate-sast-guide-static-application-security-testing) for why a layered SAST posture is the recommended default.
|
|
27
27
|
- A daily scheduled `vuln-scan.yml` runs the package manager's audit against the committed lockfile so newly-disclosed CVEs are surfaced even when no PR or push has run CI (SOC 2 CC7.1 continuous-vulnerability-management evidence).
|
|
28
|
+
- A daily scheduled `eol-scan.yml` checks pinned Node, Bun, and Deno versions in package metadata and GitHub Actions against endoflife.date so a runtime that has stopped receiving security patches does not stay unnoticed on `main`.
|
|
28
29
|
- A weekly scheduled `dast.yml` boots the built application and runs an OWASP ZAP baseline scan against it. CodeQL + Opengrep + the `verify:*` family are the SAST half of the posture; this is the DAST half — see Aikido's [SAST vs DAST](https://www.aikido.dev/blog/sast-vs-dast-what-you-need-to-now) write-up for why both matter. Findings are summarized in the workflow log and the job fails on HIGH-risk alerts; MEDIUM / LOW / INFO are surfaced for triage but non-blocking.
|
|
29
30
|
- A `secret-scan.yml` workflow runs [gitleaks](https://github.com/gitleaks/gitleaks) against the working tree on every PR / push and against the **full git history** on a daily schedule. The gitleaks binary is downloaded from a pinned official release and verified by SHA-256 before execution so the scan does not introduce a new third-party action into the supply chain. Findings block the merge; matched values are redacted from the public log. The rationale (and the remediation playbook for a confirmed leak) follows Aikido's [Secrets Detection guide](https://www.aikido.dev/blog/secret-detection-application-security): a secret in any commit, branch, or tag should be treated as compromised, and detection must consider the entire history — not just the latest snapshot — alongside GitHub-native push protection.
|
|
30
31
|
- No npm publish workflow is generated: this scaffold is a REST API service, not a published package. If you later carve out a reusable library you can opt into npm trusted publishing yourself.
|
|
@@ -162,4 +163,4 @@ Before relying on these files for a company project:
|
|
|
162
163
|
1. Replace `@your-org/security-team` in `.github/CODEOWNERS` or pass `--code-owner` when scaffolding.
|
|
163
164
|
2. Protect the `main` branch and require the CI, CodeQL, Opengrep, Scorecard, and zizmor checks.
|
|
164
165
|
3. Enable GitHub secret scanning and push protection.
|
|
165
|
-
4. Keep `ignore-scripts=true` and the `pnpm-workspace.yaml` supply-chain settings on when using pnpm.
|
|
166
|
+
4. Keep `ignore-scripts=true` and the `pnpm-workspace.yaml` supply-chain settings on when using pnpm.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# End-of-life runtime scan generated by create-daloy --with-ci.
|
|
2
|
+
#
|
|
3
|
+
# Why this exists:
|
|
4
|
+
# A runtime version can keep running after it stops receiving security
|
|
5
|
+
# patches. This scheduled check looks at pinned Node/Bun/Deno versions in
|
|
6
|
+
# package metadata and GitHub Actions, then compares them with
|
|
7
|
+
# endoflife.date so old runtime pins do not sit unnoticed on main.
|
|
8
|
+
#
|
|
9
|
+
# Hardening:
|
|
10
|
+
# * `permissions: {}` at the top level; the job opts in to contents: read.
|
|
11
|
+
# * Third-party actions are SHA-pinned and managed by Dependabot.
|
|
12
|
+
# * `step-security/harden-runner` runs in audit mode.
|
|
13
|
+
# * `actions/checkout` runs with `persist-credentials: false`.
|
|
14
|
+
# * Install steps run with lifecycle scripts disabled.
|
|
15
|
+
|
|
16
|
+
name: EOL runtime scan
|
|
17
|
+
|
|
18
|
+
on:
|
|
19
|
+
schedule:
|
|
20
|
+
# 08:21 UTC daily, offset from the other scheduled security scans.
|
|
21
|
+
- cron: "21 8 * * *"
|
|
22
|
+
workflow_dispatch:
|
|
23
|
+
|
|
24
|
+
permissions: {}
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
group: eol-scan-${{ github.workflow }}-${{ github.ref }}
|
|
28
|
+
cancel-in-progress: true
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
eol:
|
|
32
|
+
name: Daily EOL runtime scan
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
timeout-minutes: 10
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- name: Harden runner
|
|
40
|
+
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
|
|
41
|
+
with:
|
|
42
|
+
egress-policy: audit
|
|
43
|
+
disable-sudo: true
|
|
44
|
+
disable-file-monitoring: false
|
|
45
|
+
|
|
46
|
+
- name: Checkout
|
|
47
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
48
|
+
with:
|
|
49
|
+
persist-credentials: false
|
|
50
|
+
show-progress: false
|
|
51
|
+
|
|
52
|
+
__SETUP_PACKAGE_MANAGER_STEP__
|
|
53
|
+
|
|
54
|
+
- name: Set up Node.js
|
|
55
|
+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
56
|
+
with:
|
|
57
|
+
node-version: 24
|
|
58
|
+
|
|
59
|
+
__SETUP_BUN_RUNTIME_STEP__
|
|
60
|
+
|
|
61
|
+
- name: Install dependencies (no scripts)
|
|
62
|
+
run: __INSTALL_COMMAND__
|
|
63
|
+
env:
|
|
64
|
+
npm_config_ignore_scripts: "true"
|
|
65
|
+
|
|
66
|
+
- name: Verify pinned runtimes are not EOL
|
|
67
|
+
run: __RUNTIME_EOL_COMMAND__
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const EOL_WARN_DAYS = 90;
|
|
2
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
3
|
+
const RUNTIME_FEEDS = {
|
|
4
|
+
node: "nodejs",
|
|
5
|
+
bun: "bun",
|
|
6
|
+
deno: "deno",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isDeno = typeof globalThis.Deno !== "undefined";
|
|
10
|
+
const args = isDeno ? globalThis.Deno.args : globalThis.process.argv.slice(2);
|
|
11
|
+
const cwd = isDeno ? globalThis.Deno.cwd() : globalThis.process.cwd();
|
|
12
|
+
|
|
13
|
+
function joinPath(...parts) {
|
|
14
|
+
return parts.filter(Boolean).join("/").replace(/\/+/g, "/");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readText(file) {
|
|
18
|
+
if (isDeno) return await globalThis.Deno.readTextFile(file);
|
|
19
|
+
const { readFile } = await import("node:fs/promises");
|
|
20
|
+
return await readFile(file, "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readDirNames(dir) {
|
|
24
|
+
if (isDeno) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for await (const entry of globalThis.Deno.readDir(dir)) out.push(entry.name);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
const { readdir } = await import("node:fs/promises");
|
|
30
|
+
return await readdir(dir);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseVersionKey(value) {
|
|
34
|
+
if (typeof value !== "string") return null;
|
|
35
|
+
if (/latest/i.test(value)) return null;
|
|
36
|
+
const match = value.match(/v?\D*(\d+)(?:\.(\d+))?/);
|
|
37
|
+
if (!match) return null;
|
|
38
|
+
return match[2] ? `${Number(match[1])}.${Number(match[2])}` : String(Number(match[1]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectFromPackageJson(json, source) {
|
|
42
|
+
const out = [];
|
|
43
|
+
const engines = json && typeof json === "object" ? json.engines : null;
|
|
44
|
+
if (!engines || typeof engines !== "object") return out;
|
|
45
|
+
for (const runtime of Object.keys(RUNTIME_FEEDS)) {
|
|
46
|
+
const version = parseVersionKey(engines[runtime]);
|
|
47
|
+
if (version) out.push({ runtime, version, source: `${source}: engines.${runtime}` });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectFromDenoJson(json, source) {
|
|
53
|
+
const out = [];
|
|
54
|
+
const version = parseVersionKey(json?.runtime?.deno ?? json?.engines?.deno);
|
|
55
|
+
if (version) out.push({ runtime: "deno", version, source: `${source}: deno runtime` });
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectFromWorkflow(yaml, source) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const patterns = [
|
|
62
|
+
["node", /node-version:\s*['"]?([^'"\n#]+)/g],
|
|
63
|
+
["bun", /bun-version:\s*['"]?([^'"\n#]+)/g],
|
|
64
|
+
["deno", /deno-version:\s*['"]?([^'"\n#]+)/g],
|
|
65
|
+
];
|
|
66
|
+
for (const [runtime, pattern] of patterns) {
|
|
67
|
+
let match = null;
|
|
68
|
+
while ((match = pattern.exec(yaml)) !== null) {
|
|
69
|
+
const version = parseVersionKey(match[1]);
|
|
70
|
+
if (version) out.push({ runtime, version, source });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function collectPinnedVersions() {
|
|
77
|
+
const found = [];
|
|
78
|
+
try {
|
|
79
|
+
found.push(...collectFromPackageJson(JSON.parse(await readText(joinPath(cwd, "package.json"))), "package.json"));
|
|
80
|
+
} catch {
|
|
81
|
+
// No package.json is fine for Deno-native scaffolds.
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
found.push(...collectFromDenoJson(JSON.parse(await readText(joinPath(cwd, "deno.json"))), "deno.json"));
|
|
85
|
+
} catch {
|
|
86
|
+
// Node/Bun scaffolds do not ship deno.json.
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const workflowsDir = joinPath(cwd, ".github", "workflows");
|
|
90
|
+
for (const name of await readDirNames(workflowsDir)) {
|
|
91
|
+
if (!/\.(ya?ml)$/.test(name)) continue;
|
|
92
|
+
found.push(...collectFromWorkflow(await readText(joinPath(workflowsDir, name)), `.github/workflows/${name}`));
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// A local run without GitHub workflows should still scan package metadata.
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchFeed(runtime) {
|
|
101
|
+
const feed = RUNTIME_FEEDS[runtime];
|
|
102
|
+
const ac = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(`https://endoflife.date/api/${feed}.json`, {
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": "daloy-runtime-eol-scan/1",
|
|
108
|
+
Accept: "application/json",
|
|
109
|
+
},
|
|
110
|
+
signal: ac.signal,
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) throw new Error(`${feed} feed returned HTTP ${res.status}`);
|
|
113
|
+
const json = await res.json();
|
|
114
|
+
if (!Array.isArray(json)) throw new Error(`${feed} feed returned a non-array payload`);
|
|
115
|
+
return json;
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function cycleNumber(cycle) {
|
|
122
|
+
const n = Number.parseFloat(String(cycle));
|
|
123
|
+
return Number.isFinite(n) ? n : -1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findCycle(feed, version) {
|
|
127
|
+
const exact = feed.find((item) => String(item.cycle) === version);
|
|
128
|
+
if (exact) return exact;
|
|
129
|
+
if (!/^\d+$/.test(version)) return null;
|
|
130
|
+
const major = Number(version);
|
|
131
|
+
const candidates = feed
|
|
132
|
+
.filter((item) => Math.floor(cycleNumber(item.cycle)) === major)
|
|
133
|
+
.sort((a, b) => cycleNumber(b.cycle) - cycleNumber(a.cycle));
|
|
134
|
+
return candidates[0] ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function daysUntil(date, now) {
|
|
138
|
+
return Math.floor((date.getTime() - now.getTime()) / 86_400_000);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function groupPinned(pinned) {
|
|
142
|
+
const grouped = new Map();
|
|
143
|
+
for (const pin of pinned) {
|
|
144
|
+
const key = `${pin.runtime}@${pin.version}`;
|
|
145
|
+
const current = grouped.get(key);
|
|
146
|
+
if (current) current.sources.add(pin.source);
|
|
147
|
+
else grouped.set(key, { ...pin, sources: new Set([pin.source]) });
|
|
148
|
+
}
|
|
149
|
+
return [...grouped.values()];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function failProcess() {
|
|
153
|
+
if (globalThis.process) {
|
|
154
|
+
globalThis.process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (isDeno) globalThis.Deno.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function main() {
|
|
161
|
+
const strict = args.includes("--strict");
|
|
162
|
+
const now = new Date();
|
|
163
|
+
const findings = [];
|
|
164
|
+
const pinned = groupPinned(await collectPinnedVersions());
|
|
165
|
+
|
|
166
|
+
if (pinned.length === 0) {
|
|
167
|
+
console.log("verify-runtime-eol: no pinned runtime versions found.");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const feeds = new Map();
|
|
172
|
+
for (const pin of pinned) {
|
|
173
|
+
if (!feeds.has(pin.runtime)) feeds.set(pin.runtime, await fetchFeed(pin.runtime));
|
|
174
|
+
const cycle = findCycle(feeds.get(pin.runtime), pin.version);
|
|
175
|
+
if (!cycle || cycle.eol === false) continue;
|
|
176
|
+
const eol = new Date(cycle.eol);
|
|
177
|
+
if (Number.isNaN(eol.getTime())) continue;
|
|
178
|
+
const remaining = daysUntil(eol, now);
|
|
179
|
+
if (remaining < 0 || remaining <= EOL_WARN_DAYS) {
|
|
180
|
+
findings.push({
|
|
181
|
+
runtime: pin.runtime,
|
|
182
|
+
version: pin.version,
|
|
183
|
+
cycle: cycle.cycle,
|
|
184
|
+
eol: cycle.eol,
|
|
185
|
+
days: remaining,
|
|
186
|
+
severity: remaining < 0 ? "eol" : "warn",
|
|
187
|
+
sources: [...pin.sources].sort(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const finding of findings) {
|
|
193
|
+
const label = finding.severity === "eol" ? "EOL" : "WARNING";
|
|
194
|
+
const timing =
|
|
195
|
+
finding.severity === "eol"
|
|
196
|
+
? `${Math.abs(finding.days)} day(s) ago`
|
|
197
|
+
: `in ${finding.days} day(s)`;
|
|
198
|
+
console.error(
|
|
199
|
+
`${label}: ${finding.runtime} ${finding.version} maps to cycle ${finding.cycle}, EOL ${timing} (${finding.eol}). Sources: ${finding.sources.join(", ")}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (findings.some((f) => f.severity === "eol") || (strict && findings.length > 0)) {
|
|
204
|
+
failProcess();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log("verify-runtime-eol: all pinned runtimes are inside supported windows.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await main();
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
# daloyjs-pre-push-contract-hook v1
|
|
3
3
|
#
|
|
4
4
|
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
-
# error-level issues: missing or duplicate operationIds,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# it leaves your machine, with CI as the backstop.
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, examples that don't
|
|
6
|
+
# match their schema, or routes with no declared responses. Safe-method body
|
|
7
|
+
# schemas are reported as warnings. Catches the class of drift `tsc` can't see
|
|
8
|
+
# before it leaves your machine, with CI as the backstop.
|
|
9
9
|
#
|
|
10
10
|
# Enable once per clone: bun run hooks:install (sets core.hooksPath here)
|
|
11
11
|
# Bypass once: git push --no-verify
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# daloyjs-pre-push-contract-hook v1
|
|
3
3
|
#
|
|
4
4
|
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
-
# error-level issues: missing or duplicate operationIds,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# it leaves your machine, with CI as the backstop.
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, examples that don't
|
|
6
|
+
# match their schema, or routes with no declared responses. Safe-method body
|
|
7
|
+
# schemas are reported as warnings. Catches the class of drift `tsc` can't see
|
|
8
|
+
# before it leaves your machine, with CI as the backstop.
|
|
9
9
|
#
|
|
10
|
-
# Enable once per clone:
|
|
10
|
+
# Enable once per clone: pnpm hooks:install (sets core.hooksPath here)
|
|
11
11
|
# Bypass once: git push --no-verify
|
|
12
12
|
DALOY="./node_modules/.bin/daloy"
|
|
13
13
|
if [ ! -x "$DALOY" ]; then
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"dev": "wrangler dev",
|
|
8
8
|
"deploy": "wrangler deploy",
|
|
9
9
|
"typecheck": "tsc --noEmit",
|
|
10
|
-
"test": "node --import tsx
|
|
10
|
+
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
11
11
|
"contract": "daloy inspect --check src/index.ts",
|
|
12
12
|
"audit": "pnpm audit --prod",
|
|
13
13
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@daloyjs/core": "^1.0.0-beta.
|
|
16
|
+
"@daloyjs/core": "^1.0.0-beta.3",
|
|
17
17
|
"zod": "^4.4.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
# daloyjs-pre-push-contract-hook v1
|
|
3
3
|
#
|
|
4
4
|
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
-
# error-level issues: missing or duplicate operationIds,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# see before it leaves your machine, with CI as the backstop.
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, examples that don't
|
|
6
|
+
# match their schema, or routes with no declared responses. Safe-method body
|
|
7
|
+
# schemas are reported as warnings. Catches the class of drift the type-checker
|
|
8
|
+
# can't see before it leaves your machine, with CI as the backstop.
|
|
9
9
|
#
|
|
10
10
|
# Enable once per clone: deno task hooks:install (sets core.hooksPath here)
|
|
11
11
|
# Bypass once: git push --no-verify
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
11
11
|
},
|
|
12
12
|
"imports": {
|
|
13
|
-
"@daloyjs/core": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
14
|
-
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
15
|
-
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
16
|
-
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
17
|
-
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
13
|
+
"@daloyjs/core": "jsr:@daloyjs/daloy@^1.0.0-beta.3",
|
|
14
|
+
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.3/banner",
|
|
15
|
+
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.3/contract",
|
|
16
|
+
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.3/deno",
|
|
17
|
+
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.3/openapi",
|
|
18
18
|
"zod": "npm:zod@^4.4.3"
|
|
19
19
|
},
|
|
20
20
|
"compilerOptions": {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# daloyjs-pre-push-contract-hook v1
|
|
3
3
|
#
|
|
4
4
|
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
-
# error-level issues: missing or duplicate operationIds,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# it leaves your machine, with CI as the backstop.
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, examples that don't
|
|
6
|
+
# match their schema, or routes with no declared responses. Safe-method body
|
|
7
|
+
# schemas are reported as warnings. Catches the class of drift `tsc` can't see
|
|
8
|
+
# before it leaves your machine, with CI as the backstop.
|
|
9
9
|
#
|
|
10
|
-
# Enable once per clone:
|
|
10
|
+
# Enable once per clone: pnpm hooks:install (sets core.hooksPath here)
|
|
11
11
|
# Bypass once: git push --no-verify
|
|
12
12
|
DALOY="./node_modules/.bin/daloy"
|
|
13
13
|
if [ ! -x "$DALOY" ]; then
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"start": "node dist/index.js",
|
|
12
12
|
"build": "tsc -p tsconfig.build.json",
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
|
-
"test": "node --import tsx
|
|
14
|
+
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
15
15
|
"contract": "daloy inspect --check src/build-app.ts",
|
|
16
|
-
"gen:openapi": "node --import tsx
|
|
16
|
+
"gen:openapi": "node --import tsx scripts/dump-openapi.ts",
|
|
17
17
|
"gen:client": "openapi-ts",
|
|
18
18
|
"gen": "pnpm gen:openapi && pnpm gen:client",
|
|
19
19
|
"audit": "pnpm audit --prod",
|
|
20
20
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@daloyjs/core": "^1.0.0-beta.
|
|
23
|
+
"@daloyjs/core": "^1.0.0-beta.3",
|
|
24
24
|
"zod": "^4.4.3"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# daloyjs-pre-push-contract-hook v1
|
|
3
3
|
#
|
|
4
4
|
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
-
# error-level issues: missing or duplicate operationIds,
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# it leaves your machine, with CI as the backstop.
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, examples that don't
|
|
6
|
+
# match their schema, or routes with no declared responses. Safe-method body
|
|
7
|
+
# schemas are reported as warnings. Catches the class of drift `tsc` can't see
|
|
8
|
+
# before it leaves your machine, with CI as the backstop.
|
|
9
9
|
#
|
|
10
|
-
# Enable once per clone:
|
|
10
|
+
# Enable once per clone: pnpm hooks:install (sets core.hooksPath here)
|
|
11
11
|
# Bypass once: git push --no-verify
|
|
12
12
|
DALOY="./node_modules/.bin/daloy"
|
|
13
13
|
if [ ! -x "$DALOY" ]; then
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "node --import tsx
|
|
7
|
+
"dev": "node --import tsx src/dev.ts",
|
|
8
8
|
"deploy": "vercel deploy",
|
|
9
9
|
"typecheck": "tsc --noEmit",
|
|
10
|
-
"test": "node --import tsx
|
|
10
|
+
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
11
11
|
"contract": "daloy inspect --check api/index.ts",
|
|
12
12
|
"audit": "pnpm audit --prod",
|
|
13
13
|
"hooks:install": "git config core.hooksPath .githooks"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@daloyjs/core": "^1.0.0-beta.
|
|
16
|
+
"@daloyjs/core": "^1.0.0-beta.3",
|
|
17
17
|
"zod": "^4.4.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|