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
4 changes: 2 additions & 2 deletions actions/setup/js/log_parser_bootstrap.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ async function runLogParser(options) {
}
}

core.summary.addRaw(fullMarkdown).write();
await core.summary.addRaw(fullMarkdown).write();
} else {
// Fallback path: markdown exists but no structured log entries were parsed.
// Suppress the "parsed successfully" message for Claude since it always produces
Expand Down Expand Up @@ -327,7 +327,7 @@ async function runLogParser(options) {
fullMarkdown += "\n" + safeOutputsMarkdown;
}
}
core.summary.addRaw(fullMarkdown).write();
await core.summary.addRaw(fullMarkdown).write();
}
} else {
core.error(`Failed to parse ${parserName} log`);
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/parse_firewall_logs.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ async function main() {
requestsByDomain,
});

core.summary.addRaw(summary).write();
await core.summary.addRaw(summary).write();
core.info("Firewall log summary generated successfully");
} catch (error) {
core.setFailed(`${ERR_PARSE}: ${error instanceof Error ? error.message : String(error)}`);
Expand Down
10 changes: 5 additions & 5 deletions actions/setup/js/parse_mcp_gateway_log.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function generateTokenUsageSummary(summary) {
* so callers don't need to chain addRaw() + write() themselves.
* @param {typeof import('@actions/core')} coreObj - The GitHub Actions core object
*/
function writeStepSummaryWithTokenUsage(coreObj) {
async function writeStepSummaryWithTokenUsage(coreObj) {
if (!fs.existsSync(TOKEN_USAGE_PATH)) {
coreObj.debug(`No token-usage.jsonl found at: ${TOKEN_USAGE_PATH}`);
} else {
Expand Down Expand Up @@ -233,7 +233,7 @@ function writeStepSummaryWithTokenUsage(coreObj) {
coreObj.summary.addRaw(timelineMd);
}

coreObj.summary.write();
await coreObj.summary.write();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: await used inside a non-async function — this will throw a SyntaxError at runtime.

writeStepSummaryWithTokenUsage is declared as a plain (non-async) function at line 204:

function writeStepSummaryWithTokenUsage(coreObj) {

Adding await coreObj.summary.write() inside it produces SyntaxError: await is only valid in async functions at parse time. The process will fail before writing the step summary.

Two changes are required:

  1. Mark the function async:
    async function writeStepSummaryWithTokenUsage(coreObj) {
  2. Await all three call sites in main() (~lines 907, 938, 977):
    await writeStepSummaryWithTokenUsage(core);

Without both changes the bug the rule was meant to fix (unawaited core.summary.write()) is traded for an outright syntax crash.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SyntaxError: await inside a non-async function — this module will crash at require-time.

Line 204 declares function writeStepSummaryWithTokenUsage(coreObj) — a regular synchronous function. Using await inside a non-async function is a SyntaxError in Node.js CJS; the module will fail to load entirely, breaking all three callers at lines 907, 938, and 977 in main().

💡 Suggested fix

Make the function async and update all three call sites to await it:

async function writeStepSummaryWithTokenUsage(coreObj) {
  // ... existing body ...
  await coreObj.summary.write();
}

// In main() — all three call sites (lines 907, 938, 977):
await writeStepSummaryWithTokenUsage(core);

Verified with Node.js:

SyntaxError: await is only valid in async functions and the top level bodies of modules

The new ESLint rule correctly refuses to offer the await suggestion when outside an async function. The manual fix was applied anyway, bypassing that guard.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] await is added inside writeStepSummaryWithTokenUsage which is not an async function — this is a syntax error in a CJS module and will crash the Action at runtime.

💡 Correct fix

The ESLint rule's own suggestion logic correctly withholds the await fix outside async contexts; this change should do the same. You need to either make the function async (and propagate the change to all callers), or return the promise:

// Option A — make the function async
async function writeStepSummaryWithTokenUsage(coreObj) {
  // ...
  await coreObj.summary.write();
}
// then await every call site

// Option B — return the Promise (no change to callers needed)
function writeStepSummaryWithTokenUsage(coreObj) {
  // ...
  return coreObj.summary.write();
}

The other four fixes (log_parser_bootstrap.cjs, parse_firewall_logs.cjs, parse_mcp_scripts_logs.cjs, upload_assets.cjs) are all inside async function main() and are correct.

@copilot please address this.

}
Comment on lines 233 to 237

/**
Expand Down Expand Up @@ -904,7 +904,7 @@ async function main() {

setAICreditsRateLimitOutput(core, aiCreditsRateLimitError);
setUnknownModelAICreditsOutput(core, unknownModelAICredits);
writeStepSummaryWithTokenUsage(core);
await writeStepSummaryWithTokenUsage(core);
return;
}
} else {
Expand Down Expand Up @@ -935,7 +935,7 @@ async function main() {
}
setAICreditsRateLimitOutput(core, aiCreditsRateLimitError);
setUnknownModelAICreditsOutput(core, unknownModelAICredits);
writeStepSummaryWithTokenUsage(core);
await writeStepSummaryWithTokenUsage(core);
return;
}

Expand Down Expand Up @@ -974,7 +974,7 @@ async function main() {
core.info("MCP gateway log files are empty or missing");
setAICreditsRateLimitOutput(core, aiCreditsRateLimitError);
setUnknownModelAICreditsOutput(core, unknownModelAICredits);
writeStepSummaryWithTokenUsage(core);
await writeStepSummaryWithTokenUsage(core);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/parse_mcp_scripts_logs.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function main() {

// Generate step summary
const summary = generateMCPScriptsSummary(allLogEntries);
core.summary.addRaw(summary).write();
await core.summary.addRaw(summary).write();
} catch (error) {
core.setFailed(`${ERR_PARSE}: ${getErrorMessage(error)}`);
}
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/upload_assets.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ async function main() {
core.summary.addRaw(`- [\`${asset.fileName}\`](${asset.url}) → \`${asset.targetFileName}\` (${asset.size} bytes)`);
}
}
core.summary.write();
await core.summary.write();
} else {
core.info("No new assets to upload");
}
Expand Down
1 change: 1 addition & 0 deletions eslint-factory/eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = [
"gh-aw-custom/no-unsafe-promise-catch-error-property": "warn",
"gh-aw-custom/prefer-get-error-message": "warn",
"gh-aw-custom/require-async-entrypoint-catch": "warn",
"gh-aw-custom/require-await-core-summary-write": "warn",
"gh-aw-custom/require-json-parse-try-catch": "warn",
"gh-aw-custom/require-parseInt-radix": "warn",
},
Expand Down
2 changes: 2 additions & 0 deletions eslint-factory/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { noUnsafeCatchErrorPropertyRule } from "./rules/no-unsafe-catch-error-pr
import { noUnsafePromiseCatchErrorPropertyRule } from "./rules/no-unsafe-promise-catch-error-property";
import { preferGetErrorMessageRule } from "./rules/prefer-get-error-message";
import { requireAsyncEntrypointCatchRule } from "./rules/require-async-entrypoint-catch";
import { requireAwaitCoreSummaryWriteRule } from "./rules/require-await-core-summary-write";
import { requireJsonParseTryCatchRule } from "./rules/require-json-parse-try-catch";
import { requireParseIntRadixRule } from "./rules/require-parseInt-radix";

Expand All @@ -17,6 +18,7 @@ const plugin = {
"no-unsafe-promise-catch-error-property": noUnsafePromiseCatchErrorPropertyRule,
"prefer-get-error-message": preferGetErrorMessageRule,
"require-async-entrypoint-catch": requireAsyncEntrypointCatchRule,
"require-await-core-summary-write": requireAwaitCoreSummaryWriteRule,
"require-json-parse-try-catch": requireJsonParseTryCatchRule,
"require-parseInt-radix": requireParseIntRadixRule,
},
Expand Down
166 changes: 166 additions & 0 deletions eslint-factory/src/rules/require-await-core-summary-write.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { RuleTester } from "eslint";
import { describe, expect, it } from "vitest";
import { requireAwaitCoreSummaryWriteRule } from "./require-await-core-summary-write";

const cjsRuleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: "commonjs",
},
});

describe("require-await-core-summary-write", () => {
it("uses the correct docs URL", () => {
expect(requireAwaitCoreSummaryWriteRule.meta.docs.url).toBe("https://github.com/github/gh-aw/tree/main/eslint-factory#require-await-core-summary-write");
});

it("valid: awaited calls are not flagged", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [
`async function f() { await core.summary.write(); }`,
`async function f() { await core.summary.addRaw(x).write(); }`,
`async function f() { await core.summary.addHeading("h").addRaw(x).write(); }`,
`async function f() { await coreObj.summary.write(); }`,
],
invalid: [],
});
});

it("valid: returned and assigned calls are not flagged", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [
`async function f() { return core.summary.write(); }`,
`async function f() { const p = core.summary.write(); }`,
`async function f() { return core.summary.addRaw(x).write(); }`,
// assignment expression (without declaration) also propagates the Promise
`async function f() { let p; p = core.summary.write(); }`,
],
invalid: [],
});
});

it("invalid: bare core.summary.write() is flagged", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No test for async arrow function or async function expression in the invalid path — a regression in ASYNC_FUNCTION_TYPES would go undetected.

Every invalid test case uses async function f() {...} (FunctionDeclaration only). ArrowFunctionExpression and FunctionExpression are in ASYNC_FUNCTION_TYPES and assumed to work, but removing either would not break any test.

💡 Suggested fix

Add at least these two invalid cases (one per missing function form):

// async arrow function
{
  code: `const f = async () => { core.summary.write(); };`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `const f = async () => { await core.summary.write(); };` }] }],
},
// async function expression
{
  code: `const f = async function() { core.summary.write(); };`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `const f = async function() { await core.summary.write(); };` }] }],
},

Also worth adding: a non-async arrow const f = () => { core.summary.write(); }; should be flagged with no suggestion (same as the non-async FunctionDeclaration case already tested).

cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [],
invalid: [
{
code: `async function f() { core.summary.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { await core.summary.write(); }` }] }],
},
{
code: `const f = async () => { core.summary.write(); };`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `const f = async () => { await core.summary.write(); };` }] }],
},
{
code: `const f = async function() { core.summary.write(); };`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `const f = async function() { await core.summary.write(); };` }] }],
},
],
});
});

it("invalid: chained core.summary.addRaw(x).write() is flagged", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [],
invalid: [
{
code: `async function f() { core.summary.addRaw(summary).write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { await core.summary.addRaw(summary).write(); }` }] }],
},
{
code: `async function f() { core.summary.addHeading("Title").addRaw(body).write(); }`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The valid: returned and assigned calls are not flagged test includes p = core.summary.write() but the test file declares p nowhere — this would throw a ReferenceError in strict mode and is an invalid test fixture.

💡 Fix the fixture

Either declare p first or use the var keyword so the test fixture is itself valid JS:

// declare p before assignment
`async function f() { let p; p = core.summary.write(); }`
// or use a VariableDeclaration to keep it simple
`async function f() { const p = core.summary.write(); }`

This also means the assignment-expression path is only tested by the const p = case in the same group, which already has full coverage. The undeclared-variable case can be dropped.

@copilot please address this.

errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { await core.summary.addHeading("Title").addRaw(body).write(); }` }] }],
},
],
});
});

it("invalid: coreObj alias and computed access are flagged", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Missing edge-case test: bare core.summary.write() inside a top-level async IIFE (async () => { ... })() — the suggestion should still be offered since the enclosing function is async.

💡 Add the test case
{
  code: `(async () => { core.summary.write(); })()`,
  errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `(async () => { await core.summary.write(); })()` }] }],
},

isInsideAsyncFunction already walks ancestors so this should work out of the box, but an explicit test guards against future regressions.

@copilot please address this.

valid: [],
invalid: [
{
code: `async function f() { coreObj.summary.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { await coreObj.summary.write(); }` }] }],
},
{
code: `async function f() { core.summary["write"](); }`,
errors: [{ messageId: "requireAwait", suggestions: [{ messageId: "addAwait", output: `async function f() { await core.summary["write"](); }` }] }],
},
],
});
});

it("valid: unrelated .write() calls are not flagged", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [`fs.write(fd, buffer);`, `stream.write(data);`, `core.info("hello");`, `foo.bar.write();`, `fs.summary.write();`, `db.summary.write();`, `foo.bar.summary.write();`],
invalid: [],
});
});

it("invalid: flagged outside async function — no suggestion offered", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [],
invalid: [
{
// Top-level call: flagged, but no suggestion (await is not valid here)
code: `core.summary.write();`,
errors: [{ messageId: "requireAwait", suggestions: [] }],
},
{
// Inside a non-async function: flagged, but no suggestion
code: `function f() { core.summary.write(); }`,
errors: [{ messageId: "requireAwait", suggestions: [] }],
},
{
code: `const f = () => { core.summary.write(); };`,
errors: [{ messageId: "requireAwait", suggestions: [] }],
},
],
});
});

it("suggestion: inserts 'await ' before the expression", () => {
cjsRuleTester.run("require-await-core-summary-write", requireAwaitCoreSummaryWriteRule, {
valid: [],
invalid: [
{
code: `async function f() { core.summary.write(); }`,
errors: [
{
messageId: "requireAwait",
suggestions: [{ messageId: "addAwait", output: `async function f() { await core.summary.write(); }` }],
},
],
},
{
code: `async function f() { core.summary.addRaw(summary).write(); }`,
errors: [
{
messageId: "requireAwait",
suggestions: [{ messageId: "addAwait", output: `async function f() { await core.summary.addRaw(summary).write(); }` }],
},
],
},
{
code: `async function f() { coreObj.summary.write(); }`,
errors: [
{
messageId: "requireAwait",
suggestions: [{ messageId: "addAwait", output: `async function f() { await coreObj.summary.write(); }` }],
},
],
},
{
code: `(async () => { core.summary.write(); })()`,
errors: [
{
messageId: "requireAwait",
suggestions: [{ messageId: "addAwait", output: `(async () => { await core.summary.write(); })()` }],
},
],
},
],
});
});
});
Loading
Loading