create-daloy 1.0.0-beta.2 → 1.0.0-beta.4
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 +8 -2
- package/bin/create-daloy.mjs +21 -4
- 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
|
@@ -196,6 +196,11 @@ For Node-style templates, the bundle adds the following.
|
|
|
196
196
|
[npm-audit-guide](https://www.aikido.dev/blog/npm-audit-guide) write-ups warn
|
|
197
197
|
about, and the Deno scaffold gets it too (Deno has no `audit` built in, so
|
|
198
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.
|
|
199
204
|
|
|
200
205
|
**Secret and static analysis**
|
|
201
206
|
|
|
@@ -235,8 +240,9 @@ out a reusable package, opt into npm trusted publishing yourself.
|
|
|
235
240
|
For `deno-basic`, `--with-ci` generates a Deno-native CI workflow, a manual-only
|
|
236
241
|
container publish starter for GHCR that is guarded to `main` or a tag by
|
|
237
242
|
default, plus CodeQL, Opengrep, **OSV-Scanner** (the only scheduled SCA layer
|
|
238
|
-
a Deno scaffold has, since Deno ships no `audit`),
|
|
239
|
-
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`.
|
|
240
246
|
|
|
241
247
|
If you want the governance bundle but not the deployment starter, pass
|
|
242
248
|
`--with-ci --no-deploy`. If you only want a deployment starter, pass
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -1137,14 +1137,26 @@ async function writePackageJson(dir, packageJson) {
|
|
|
1137
1137
|
await writeFile(path.join(dir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n", "utf8");
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
-
async function
|
|
1140
|
+
async function addSecurityScripts(dir, packageManager) {
|
|
1141
1141
|
const packageJson = await readPackageJsonIfPresent(dir);
|
|
1142
1142
|
if (!packageJson) return;
|
|
1143
1143
|
packageJson.scripts ??= {};
|
|
1144
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";
|
|
1145
1147
|
await writePackageJson(dir, packageJson);
|
|
1146
1148
|
}
|
|
1147
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
|
+
|
|
1148
1160
|
function renderCiReplacements({ packageManager, template, packageJson, codeOwner, includeSecurityBundle = true }) {
|
|
1149
1161
|
const setupPm = setupPackageManagerStep(packageManager);
|
|
1150
1162
|
const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
|
|
@@ -1183,6 +1195,7 @@ function renderCiReplacements({ packageManager, template, packageJson, codeOwner
|
|
|
1183
1195
|
["__SETUP_BUN_RUNTIME_STEP__", needsBunRuntime ? setupBunStep() : ""],
|
|
1184
1196
|
["__INSTALL_COMMAND__", installCommand(packageManager)],
|
|
1185
1197
|
["__VERIFY_LOCKFILE_COMMAND__", runScriptCommand(packageManager, "verify:lockfile")],
|
|
1198
|
+
["__RUNTIME_EOL_COMMAND__", runScriptCommand(packageManager, "verify:runtime-eol")],
|
|
1186
1199
|
["__TYPECHECK_COMMAND__", runScriptCommand(packageManager, "typecheck")],
|
|
1187
1200
|
["__TEST_COMMAND__", runScriptCommand(packageManager, "test")],
|
|
1188
1201
|
["__BUILD_STEP__", buildStep],
|
|
@@ -1217,14 +1230,15 @@ async function replacePlaceholdersInTree(dir, replacements) {
|
|
|
1217
1230
|
async function pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow }) {
|
|
1218
1231
|
if (!includeSecurityBundle) {
|
|
1219
1232
|
const workflowFiles = flavor === "deno"
|
|
1220
|
-
? ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "opengrep.yml", "osv-scan.yml", "scorecard.yml", "secret-scan.yml", "zizmor.yml"]
|
|
1221
|
-
: ["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"];
|
|
1222
1235
|
for (const file of workflowFiles) {
|
|
1223
1236
|
await rm(path.join(targetDir, ".github", "workflows", file), { force: true });
|
|
1224
1237
|
}
|
|
1225
1238
|
await rm(path.join(targetDir, ".github", "dependabot.yml"), { force: true });
|
|
1226
1239
|
await rm(path.join(targetDir, ".github", "CODEOWNERS"), { force: true });
|
|
1227
1240
|
await rm(path.join(targetDir, "SECURITY.md"), { force: true });
|
|
1241
|
+
await rm(path.join(targetDir, "scripts", "verify-runtime-eol.mjs"), { force: true });
|
|
1228
1242
|
if (flavor === "node") {
|
|
1229
1243
|
await rm(path.join(targetDir, "scripts", "verify-lockfile-sources.mjs"), { force: true });
|
|
1230
1244
|
}
|
|
@@ -1264,6 +1278,9 @@ async function copyCiBundle(
|
|
|
1264
1278
|
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1265
1279
|
);
|
|
1266
1280
|
}
|
|
1281
|
+
if (includeSecurityBundle) {
|
|
1282
|
+
await addDenoSecurityTasks(targetDir);
|
|
1283
|
+
}
|
|
1267
1284
|
await replacePlaceholdersInTree(targetDir, new Map([["__CODE_OWNER__", owner]]));
|
|
1268
1285
|
return;
|
|
1269
1286
|
}
|
|
@@ -1274,7 +1291,7 @@ async function copyCiBundle(
|
|
|
1274
1291
|
`Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
|
|
1275
1292
|
);
|
|
1276
1293
|
}
|
|
1277
|
-
await
|
|
1294
|
+
await addSecurityScripts(targetDir, packageManager);
|
|
1278
1295
|
}
|
|
1279
1296
|
const packageJson = await readPackageJsonIfPresent(targetDir);
|
|
1280
1297
|
await replacePlaceholdersInTree(
|
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.4",
|
|
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:1f97437c-9665-5c96-94a2-d8d548acda4b",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-06-
|
|
7
|
+
"timestamp": "2026-06-26T13:41:50.237Z",
|
|
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.4"
|
|
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.4",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "1.0.0-beta.
|
|
20
|
+
"version": "1.0.0-beta.4",
|
|
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.4",
|
|
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.4",
|
|
46
46
|
"name": "create-daloy",
|
|
47
|
-
"version": "1.0.0-beta.
|
|
47
|
+
"version": "1.0.0-beta.4",
|
|
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.4",
|
|
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.4",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-beta.4-1f97437c-9665-5c96-94a2-d8d548acda4b",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-06-
|
|
8
|
+
"created": "2026-06-26T13:41:50.237Z",
|
|
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.4",
|
|
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.4"
|
|
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.4",
|
|
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.4",
|
|
14
|
+
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.4/banner",
|
|
15
|
+
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.4/contract",
|
|
16
|
+
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.4/deno",
|
|
17
|
+
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.4/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.4",
|
|
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.4",
|
|
17
17
|
"zod": "^4.4.3"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|