diff --git a/README.md b/README.md index 49b47ad..67800b8 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ This action is a recommended deployment option. You can also use [our public Git - `path`: Output path for the SVG file. Defaults to `profile/.svg`. - `token`: GitHub token (PAT or `GITHUB_TOKEN`). For private repo stats, use a [PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with `repo` and `read:user` scopes. For any gist, use a PAT with `gist` scope. - `core_version`: Version of [GitHub Stats Extended](https://github.com/stats-organization/github-stats-extended) to use internally. When omitted, the action uses the latest 2.x.x version. +- `fail_on_error`: Fail the action when data fetching fails (e.g. a GitHub API rate limit) instead of writing the "Something went wrong" error card.\ + Defaults to `false` for backwards compatibility. ## Outputs diff --git a/action.yml b/action.yml index ad7ef5c..0c1db12 100644 --- a/action.yml +++ b/action.yml @@ -21,6 +21,14 @@ inputs: description: Version of @stats-organization/github-readme-stats-core to use internally. required: false default: "v2" + fail_on_error: + description: >- + Fail the action when the card renderer reports a data-fetch error (e.g. a + GitHub API rate limit) instead of writing the "Something went wrong" error + card. Defaults to "false" to preserve backwards-compatible behaviour; set + to "true" to fail the action on a broken card. + required: false + default: "false" outputs: path: description: Path where the SVG file was written. @@ -59,6 +67,7 @@ runs: INPUT_OPTIONS: ${{ inputs.options }} INPUT_PATH: ${{ inputs.path }} INPUT_CORE_VERSION: ${{ inputs.core_version }} + INPUT_FAIL_ON_ERROR: ${{ inputs.fail_on_error }} PAT_1: ${{ inputs.token || github.token }} branding: icon: bar-chart-2 diff --git a/index.js b/index.js index 50468a1..0f62e23 100644 --- a/index.js +++ b/index.js @@ -181,9 +181,10 @@ const validateCardOptions = (card, query, repoOwner) => { const run = async () => { const card = getInput("card", { required: true }).toLowerCase(); - const optionsInput = getInput("options") || ""; + const optionsInput = getInput("options"); const outputPathInput = getInput("path"); - const coreVersion = validateCoreVersion(getInput("core_version") || ""); + const coreVersion = validateCoreVersion(getInput("core_version")); + const failOnError = /^(true|1|yes)$/i.test(getInput("fail_on_error")); const coreModule = await loadCoreModule(coreVersion); @@ -201,13 +202,25 @@ const run = async () => { const outputPathValue = outputPathInput || path.join("profile", `${card}.svg`); const outputPath = path.resolve(process.cwd(), outputPathValue); - await mkdir(path.dirname(outputPath), { recursive: true }); - const svg = (await handler(query))?.content; + const result = await handler(query); + const svg = result?.content; + + // The core renderer never throws on a data-fetch error; it returns a `status` + // starting with "error" and a "Something went wrong" SVG. When fail_on_error + // is enabled, fail the action so the broken card is never written or committed. + // Older core versions may not return a `status`, so this is a no-op for them. + if (failOnError && String(result?.status).startsWith("error")) { + throw new Error( + `Card generation failed while fetching data (${result.status}).`, + ); + } + if (!svg) { throw new Error("Card renderer returned empty output."); } + await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, svg, "utf8"); info(`Wrote ${outputPath}`); setOutput("path", outputPathValue); diff --git a/tests/e2e.test.js b/tests/e2e.test.js index 126f4c6..731edff 100644 --- a/tests/e2e.test.js +++ b/tests/e2e.test.js @@ -13,7 +13,7 @@ const rootDir = path.resolve( const repoOwner = process.env.GITHUB_REPOSITORY_OWNER ?? "rickstaa"; let buildDir; -const runCard = (card, options, output, coreVersion) => +const runCard = (card, options, output, coreVersion, extraEnv = {}) => new Promise((resolve, reject) => { const child = spawn(process.execPath, [path.join(rootDir, "index.js")], { stdio: "inherit", @@ -23,6 +23,7 @@ const runCard = (card, options, output, coreVersion) => INPUT_OPTIONS: options, INPUT_PATH: output, INPUT_CORE_VERSION: coreVersion, + ...extraEnv, }, }); @@ -87,6 +88,39 @@ describe.concurrent("generate cards locally", () => { await assertSvg(wakatimePath); }); + test("fails when fail_on_error is enabled and the renderer reports an error", async () => { + // An invalid locale makes the core renderer return an "error - permanent" + // result offline (no network), which the action must surface as a failure + // when fail_on_error is opted into. + const errorPath = path.join(buildDir, "error-fails.svg"); + + await expect( + runCard( + "stats", + `username=${repoOwner}&locale=zzinvalid`, + errorPath, + "", + { + INPUT_FAIL_ON_ERROR: "true", + }, + ), + ).rejects.toThrow(); + + // The error card must not be written when the action fails. + await expect(readFile(errorPath, "utf8")).rejects.toThrow(); + }); + + test("writes the error card by default (fail_on_error off)", async () => { + // Default behaviour: the action writes the error card and succeeds. + const errorPath = path.join(buildDir, "error-allowed.svg"); + + await runCard("stats", `username=${repoOwner}&locale=zzinvalid`, errorPath); + + const data = await readFile(errorPath, "utf8"); + expect(data).toContain(" { const invalidVersionPath = path.join(buildDir, "invalid-version.svg");