Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<card>.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

Expand Down
9 changes: 9 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") || "");
Comment on lines -184 to -186

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the || "" since the getInput JSDoc states:

Returns an empty string if the value is not defined.

Reference: https://github.com/actions/toolkit/blob/e7728b1bcda3082eca6b716f0c6e20c743b7972d/packages/core/src/core.ts#L142-L146

const coreVersion = validateCoreVersion(getInput("core_version"));
const failOnError = /^(true|1|yes)$/i.test(getInput("fail_on_error"));

const coreModule = await loadCoreModule(coreVersion);

Expand All @@ -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);
Expand Down
36 changes: 35 additions & 1 deletion tests/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -23,6 +23,7 @@ const runCard = (card, options, output, coreVersion) =>
INPUT_OPTIONS: options,
INPUT_PATH: output,
INPUT_CORE_VERSION: coreVersion,
...extraEnv,
},
});

Expand Down Expand Up @@ -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("<svg");
expect(data).toContain("Something went wrong");
});

test("rejects invalid core_version input before install", async () => {
const invalidVersionPath = path.join(buildDir, "invalid-version.svg");

Expand Down
Loading