From c2cbe61835e9ca392b07a99c2fd4f503136a5f7c Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 8 Jun 2026 01:21:58 -0700 Subject: [PATCH] ci(actions): add actionlint download fallback --- package-lock.json | 32 ++++++++++++++++++++++ package.json | 1 + scripts/actionlint.mjs | 62 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cacf8cfe7..59d27d7fab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.10", + "@tktco/node-actionlint": "^1.6.0", "@types/node": "^24.10.1", "@vitest/coverage-v8": "^4.1.7", "drizzle-kit": "^0.31.7", @@ -6331,6 +6332,37 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tktco/node-actionlint": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@tktco/node-actionlint/-/node-actionlint-1.6.0.tgz", + "integrity": "sha512-fk0UKkws3QOnnpwbVtYLW5eZUozktdassXGQYeWvX5sLlPZIby4XRXDnHxaqnfNxH3b3DZJ/xNA23wf2L2Y1JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "chalk": "^5.4.1", + "fast-glob": "^3.3.3" + }, + "bin": { + "node-actionlint": "bin/node-actionlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@tktco/node-actionlint/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", diff --git a/package.json b/package.json index 8df87416d3..cf93dd542e 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.10", + "@tktco/node-actionlint": "^1.6.0", "@types/node": "^24.10.1", "@vitest/coverage-v8": "^4.1.7", "drizzle-kit": "^0.31.7", diff --git a/scripts/actionlint.mjs b/scripts/actionlint.mjs index 17952e8bc6..0501bd611d 100644 --- a/scripts/actionlint.mjs +++ b/scripts/actionlint.mjs @@ -1,6 +1,10 @@ -import { execFileSync } from "node:child_process"; -import { readdirSync } from "node:fs"; +import { readFileSync, readdirSync } from "node:fs"; +import { createRequire } from "node:module"; import { join } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; + +const require = createRequire(import.meta.url); +const { actionlint } = require("github-actionlint"); const workflowDir = ".github/workflows"; const files = readdirSync(workflowDir) @@ -12,5 +16,55 @@ if (files.length === 0) { process.exit(1); } -const bin = process.platform === "win32" ? "github-actionlint.cmd" : "github-actionlint"; -execFileSync(bin, files, { stdio: "inherit", shell: process.platform === "win32" }); +const maxAttempts = Math.max(1, Number.parseInt(process.env.ACTIONLINT_DOWNLOAD_ATTEMPTS ?? "4", 10) || 4); +const retryDelaysMs = [1000, 3000, 7000]; +const retryableSetupError = /Download failed: (?:408|425|429|5\d\d)\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up/i; + +function errorMessage(error) { + return error instanceof Error ? error.message : String(error); +} + +async function runWasmActionlint(reason) { + console.warn(`official actionlint setup unavailable, using WASM fallback: ${reason}`); + const { getLintLog, runLint } = require("@tktco/node-actionlint"); + const fileData = files.map((file) => ({ path: file, data: readFileSync(file, "utf8") })); + const results = ( + await Promise.all( + fileData.map(async (file) => { + const lintResults = await runLint(file.data, file.path); + return lintResults.map((result) => ({ ...result, ...file })); + }), + ) + ) + .flat() + .filter((result) => result.message); + const log = getLintLog(results); + if (log) { + console.error(log); + return { code: 1 }; + } + return { code: 0 }; +} + +async function runActionlint() { + if (process.env.ACTIONLINT_FORCE_WASM_FALLBACK === "1") { + return runWasmActionlint("forced by ACTIONLINT_FORCE_WASM_FALLBACK"); + } + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + return await actionlint({ args: files, spawnOptions: { stdio: "inherit" } }); + } catch (error) { + const message = errorMessage(error); + if (!retryableSetupError.test(message)) throw error; + if (attempt >= maxAttempts) return runWasmActionlint(message); + const delayMs = retryDelaysMs[Math.min(attempt - 1, retryDelaysMs.length - 1)]; + console.warn(`actionlint setup failed, retrying (${attempt}/${maxAttempts}): ${message}`); + await delay(delayMs); + } + } + throw new Error("actionlint setup failed without returning a result"); +} + +const result = await runActionlint(); +process.exit(result.code);