Skip to content

Fix SVG trophy generation in the GitHub Action: empty GraphQL body, null fields, token leak in logs, output path crash; add layout inputs - #458

Open
satishjhanwer wants to merge 5 commits into
ryo-ma:masterfrom
satishjhanwer:fix/svg-generation-bugs
Open

satishjhanwer wants to merge 5 commits into
ryo-ma:masterfrom
satishjhanwer:fix/svg-generation-bugs

Conversation

@satishjhanwer

@satishjhanwer satishjhanwer commented Aug 4, 2026 •

Copy link
Copy Markdown

Running the render_svg.ts / composite action on master currently fails for every user with a generic, unhelpful error:

Error fetching user info for username: <username>
Failed to fetch user info. Check token, username and rate limits.

The real errors were being swallowed by an empty catch {} block. After fixing the logging, two separate root causes surfaced, plus a token-leak risk uncovered while fixing the logging. Getting the action to run end to end then exposed an output-path crash, and the layout options couldn't be set from the action at all. This PR fixes all of it.

1. GraphQL request body was always empty

src/Services/request.ts calls:

const response = await soxa.post("", {}, {
  data: { query, variables },
  headers: { Authorization: `bearer ${token}` },
});

soxa mirrors axios's post(url, data, config) signature, where the explicit data argument (2nd param) always wins over config.data during merge. So the actual POST body sent to api.github.com/graphql was {} - the query/variables in config.data were silently discarded. GitHub's API rejects that, causing every request to fail regardless of token or username.

Fix: pass { query, variables } as the actual data argument.

2. Unguarded property access on optional/missing GraphQL fields

Once (1) was fixed, UserInfo.fromCombined() in src/user_info.ts crashed with TypeError: Cannot read properties of null. GitHub's GraphQL API can return null for fields like organizations when the token lacks the relevant scope (a known API behavior), and null entries inside repositories.nodes for repos it can't fully resolve for the given token. The constructor did unguarded access like userActivity.organizations.totalCount and node.stargazerCount with no null checks anywhere.

Fix: default missing sub-fields to 0/[], and filter null entries out of repositories.nodes before use.

3. Errors were logged with no detail, which hid both of the above

GithubApiService.requestUserInfo had a bare catch { Logger.error("generic message") } that discarded the actual thrown error, making this impossible to diagnose from CI logs alone.

Fix: log the actual error too.

4. Fixing (3) risked leaking the auth token into logs

While fixing (3), found that soxa attaches the full request config - including the Authorization: bearer <token> header - onto every thrown error, and even defines toJSON() to re-serialize it. Several catch blocks (GithubApiService.ts, Retry.ts) logged the raw error object or JSON.stringify(error.cause), which would print that header straight into the Actions log.

Fix: added src/Helpers/safeErrorMessage.ts, which extracts only .message from any thrown value. Routed all error logging through it instead of logging raw error/cause objects.

5. Action crashes when output_path is a bare filename

With the fixes above, the action fetched data and rendered the SVG, but then failed when writing it out with the default output_path: trophy.svg:

error: Uncaught (in promise) Error: Is a directory (os error 21): writefile 'trophy.svg'

render_svg.ts found the parent directory with outputPath.replace(/\/[^/]+$/, ""). When the path has no /, that returns the whole filename, so the script ran Deno.mkdir("trophy.svg") and then tried to write a file onto that directory.

Fix: use posix.dirname() from std, and skip mkdir when the result is . (e.g. trophy.svg, ./trophy.svg).

6. Layout options were hard-coded and couldn't be set from the action

action.yml only exposed username, output_path, token and theme. Columns, rows, margins and the other options were constants in render_svg.ts, so a workflow passing max-cols or margin-width only got an "Unexpected input(s)" warning and the setting was ignored.

Fix: added the inputs max-cols, max-rows, margin-width, margin-height, panel-size, no-bg, no-frame, title and rank.

  • They reach the script as TROPHY_* environment variables, so the positional CLI arguments don't change and no input is put into the shell command.
  • title and rank take comma-separated values and support the - exclusion prefix, the same as the web API.
  • Each input defaults to the value that was previously hard-coded, so existing workflows produce the same SVG. Empty or invalid values fall back to those defaults.
  • Option parsing and directory handling moved into src/Helpers/renderOptions.ts so they can be unit-tested. The README action example lists the new inputs.

Testing

  • Unit tests: src/Helpers/__tests__/renderOptions.test.ts adds 6 tests: a regression test for the bare-filename crash, nested output paths, defaults, empty action inputs, value parsing (numbers, booleans, comma-separated lists) and fallback for invalid values. deno test: 21 passed, 0 failed. deno check and deno fmt --check are clean.
  • End to end: verified in a real GitHub Actions workflow against a live account (logs here). I reproduced the original failure, then confirmed trophy.svg was generated after each fix, and that the sanitized logging still shows useful error messages without exposing the token.

soxa.post(url, data, config) uses the explicit data argument over
config.data when merging, so passing {} as data and {query, variables}
inside config.data silently sent an empty body to the GitHub GraphQL
API on every request.
GitHub's GraphQL API can return null for fields like organizations
when the token lacks the relevant scope, and null entries inside
repositories.nodes for repos it can't fully resolve for the given
token. UserInfo's constructor did unguarded property access on these,
throwing TypeErrors that were silently swallowed by an empty catch
block elsewhere. Default missing sub-fields to 0/[] and filter null
nodes out of repositories.nodes before use.
requestUserInfo had a bare catch {} that discarded the actual thrown
error, making failures impossible to diagnose from CI logs. Fixing
that surfaced a second issue: soxa attaches the full request config
(including the Authorization bearer token header) onto thrown errors
and defines toJSON() to re-serialize it, and executeQuery/Retry logged
raw error objects or JSON.stringify(error.cause), which would print
that header into the Actions log. Added safeErrorMessage(), which only
ever extracts .message from a thrown value, and routed all error
logging in these files through it.
- request.ts: assert the POST body actually sent to soxa.post is
  {query, variables}, not {} - fails on the pre-fix code with the
  exact empty-body bug.
- user_info.ts: assert UserInfo defaults null/missing GraphQL
  sub-fields instead of throwing - fails on the pre-fix code with the
  exact 'Cannot read properties of null' crash seen in production.
- safeErrorMessage.ts: unit tests for the new helper, including
  asserting it never surfaces extra properties (e.g. a token in
  error.config) attached to an Error.

Verified both regression tests fail against the original buggy
src/Services/request.ts and src/user_info.ts before being fixed, and
that the full suite (15 tests) passes with the fixes applied.
@Illyriat

Illyriat commented Aug 5, 2026

Copy link
Copy Markdown

awesome hopefully this is picked up soon

@satishjhanwer

Copy link
Copy Markdown
Author

Please do let me know, if anything else is required from my side.

@satishjhanwer satishjhanwer changed the title SVG trophy generation fails silently - broken GraphQL request body, unguarded null fields, token leak in logs Fix SVG trophy generation in the GitHub Action: empty GraphQL body, null fields, token leak in logs, output path crash; add layout inputs Oct 24, 2026
@satishjhanwer

Copy link
Copy Markdown
Author

@ryo-ma Hey if you get a chance, could you please take a look at this PR and merge it if its all good.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants