From ec2f5b72fc07cc0cdd7deedcb436dafbdfae4af4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:36:42 +0000 Subject: [PATCH 1/7] feat(eslint): add prefer-core-logging rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new ESLint rule that flags console.log/error/warn/debug calls in scopes where an @actions/core alias (core / coreObj) is available. Rationale: - console.* bypasses GitHub Actions' secret masking: values passed to console.* are emitted verbatim to the workflow log, without the redaction that core.info/error/warning/debug applies. - core logging integrates with the Actions annotation system (error annotations, folded groups, etc.) while console.* does not. - The pattern appears in 5 locations in actions/setup/js, e.g. generate_workflow_overview.cjs:38 and merge_remote_agent_github_folder.cjs. Rule details: - Only flags when a CORE_ALIASES binding (core / coreObj) is in scope, preventing false positives in plain scripts that don't use @actions/core. - Covers: console.log → core.info, console.error → core.error, console.warn → core.warning, console.debug → core.debug, console.info → core.info. - Provides an auto-fix suggestion for each violation. - 11 unit tests (all passing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eslint-factory/eslint.config.cjs | 1 + eslint-factory/src/index.ts | 2 + .../src/rules/prefer-core-logging.test.ts | 159 ++++++++++++++++++ .../src/rules/prefer-core-logging.ts | 103 ++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 eslint-factory/src/rules/prefer-core-logging.test.ts create mode 100644 eslint-factory/src/rules/prefer-core-logging.ts diff --git a/eslint-factory/eslint.config.cjs b/eslint-factory/eslint.config.cjs index de87fb76c61..67b80f37cc9 100644 --- a/eslint-factory/eslint.config.cjs +++ b/eslint-factory/eslint.config.cjs @@ -31,6 +31,7 @@ module.exports = [ "gh-aw-custom/require-return-after-core-setfailed": "warn", "gh-aw-custom/require-spawnsync-error-check": "warn", "gh-aw-custom/require-new-url-try-catch": "warn", + "gh-aw-custom/prefer-core-logging": "warn", }, }, { diff --git a/eslint-factory/src/index.ts b/eslint-factory/src/index.ts index f1cb9ae035f..9b9339c20dc 100644 --- a/eslint-factory/src/index.ts +++ b/eslint-factory/src/index.ts @@ -17,6 +17,7 @@ import { requireMkdirSyncTryCatchRule } from "./rules/require-mkdirsync-try-catc import { requireReturnAfterCoreSetFailedRule } from "./rules/require-return-after-core-setfailed"; import { requireSpawnSyncErrorCheckRule } from "./rules/require-spawnsync-error-check"; import { requireNewUrlTryCatchRule } from "./rules/require-new-url-try-catch"; +import { preferCoreLoggingRule } from "./rules/prefer-core-logging"; const plugin = { meta: { @@ -43,6 +44,7 @@ const plugin = { "require-return-after-core-setfailed": requireReturnAfterCoreSetFailedRule, "require-spawnsync-error-check": requireSpawnSyncErrorCheckRule, "require-new-url-try-catch": requireNewUrlTryCatchRule, + "prefer-core-logging": preferCoreLoggingRule, }, }; diff --git a/eslint-factory/src/rules/prefer-core-logging.test.ts b/eslint-factory/src/rules/prefer-core-logging.test.ts new file mode 100644 index 00000000000..db00e6055de --- /dev/null +++ b/eslint-factory/src/rules/prefer-core-logging.test.ts @@ -0,0 +1,159 @@ +import { RuleTester } from "eslint"; +import { describe, expect, it } from "vitest"; +import { preferCoreLoggingRule } from "./prefer-core-logging"; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: "commonjs", + }, +}); + +describe("prefer-core-logging", () => { + it("uses the correct docs URL", () => { + expect(preferCoreLoggingRule.meta.docs.url).toBe("https://github.com/github/gh-aw/tree/main/eslint-factory#prefer-core-logging"); + }); + + it("hasSuggestions enabled", () => { + expect(preferCoreLoggingRule.meta.hasSuggestions).toBe(true); + }); + + it("valid: console.log with no core in scope is allowed", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [ + // No core in scope — plain scripts are allowed to use console + `console.log("hello");`, + `console.error("bad thing");`, + `const foo = "bar"; console.log(foo);`, + ], + invalid: [], + }); + }); + + it("valid: core.info calls are fine", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [ + `const core = require("@actions/core"); core.info("hello");`, + `const core = require("@actions/core"); core.error("bad");`, + `const core = require("@actions/core"); core.warning("warn");`, + `const core = require("@actions/core"); core.debug("debug");`, + ], + invalid: [], + }); + }); + + it("valid: console.log in a function where core is not a param and not declared", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [`function helper() { console.log("no core here"); }`], + invalid: [], + }); + }); + + it("invalid: console.log when core is in scope via require", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `const core = require("@actions/core"); console.log("hello");`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `"hello"` }, output: `const core = require("@actions/core"); core.info("hello");` }], + }, + ], + }, + ], + }); + }); + + it("invalid: console.error when core is in scope", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `const core = require("@actions/core"); console.error("bad thing");`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "error", replacement: "core.error" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.error", args: `"bad thing"` }, output: `const core = require("@actions/core"); core.error("bad thing");` }], + }, + ], + }, + ], + }); + }); + + it("invalid: console.warn when core is in scope", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `const core = require("@actions/core"); console.warn("warning");`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "warn", replacement: "core.warning" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.warning", args: `"warning"` }, output: `const core = require("@actions/core"); core.warning("warning");` }], + }, + ], + }, + ], + }); + }); + + it("invalid: console.debug when core is in scope", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `const core = require("@actions/core"); console.debug("verbose");`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "debug", replacement: "core.debug" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.debug", args: `"verbose"` }, output: `const core = require("@actions/core"); core.debug("verbose");` }], + }, + ], + }, + ], + }); + }); + + it("invalid: console.log when core is a function parameter", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `async function run(core) { console.log("done"); }`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `"done"` }, output: `async function run(core) { core.info("done"); }` }], + }, + ], + }, + ], + }); + }); + + it("invalid: console.log with multi-arg call when core is in scope", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `const core = require("@actions/core"); const someVar = 1; console.log("value:", someVar);`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `"value:", someVar` }, output: `const core = require("@actions/core"); const someVar = 1; core.info("value:", someVar);` }], + }, + ], + }, + ], + }); + }); +}); diff --git a/eslint-factory/src/rules/prefer-core-logging.ts b/eslint-factory/src/rules/prefer-core-logging.ts new file mode 100644 index 00000000000..9ebcabd05f8 --- /dev/null +++ b/eslint-factory/src/rules/prefer-core-logging.ts @@ -0,0 +1,103 @@ +import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { CORE_ALIASES } from "./core-aliases"; + +const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); + +// Maps console method → recommended core replacement +const CONSOLE_TO_CORE: Record = { + log: "core.info", + info: "core.info", + warn: "core.warning", + error: "core.error", + debug: "core.debug", +}; + +/** + * Returns the `console` method name if the call expression is a `console.*` + * call that has a known core replacement, otherwise null. + */ +function getConsoleMethod(node: TSESTree.CallExpression): string | null { + const callee = node.callee; + if (callee.type !== AST_NODE_TYPES.MemberExpression) return null; + if (callee.computed) return null; + const obj = callee.object; + const prop = callee.property; + if (obj.type !== AST_NODE_TYPES.Identifier || obj.name !== "console") return null; + if (prop.type !== AST_NODE_TYPES.Identifier) return null; + return prop.name in CONSOLE_TO_CORE ? prop.name : null; +} + +/** + * Walks the scope chain to determine whether any binding for a known + * @actions/core alias (`core`, `coreObj`) is visible at the given node. + * + * Accepted patterns: + * - `const core = require("@actions/core")` + * - `import * as core from "@actions/core"` + * - A function parameter named `core` or `coreObj` (used in github-script style) + */ +function hasCoreInScope(node: TSESTree.Node, sourceCode: TSESLint.SourceCode): boolean { + let scope: TSESLint.Scope.Scope | null = sourceCode.getScope(node); + while (scope) { + for (const variable of scope.variables) { + if (CORE_ALIASES.has(variable.name) && variable.defs.length > 0) { + return true; + } + } + scope = scope.upper; + } + return false; +} + +export const preferCoreLoggingRule = createRule({ + name: "prefer-core-logging", + meta: { + type: "suggestion", + hasSuggestions: true, + docs: { + description: + "Prefer @actions/core logging methods (core.info, core.error, core.warning, core.debug) over console.* in files that have access to @actions/core. " + + "console.* bypasses GitHub Actions' built-in secret masking and structured annotation system; core logging ensures secrets in output are redacted and messages appear correctly in the Actions UI.", + }, + schema: [], + messages: { + preferCoreLogging: + "Use {{replacement}} instead of console.{{method}}() — @actions/core logging masks secrets and integrates with the Actions annotation system. console.* output is not masked.", + replaceWithCoreMethod: "Replace with {{replacement}}({{args}}) — ensure a @actions/core alias (core / coreObj) is in scope.", + }, + }, + defaultOptions: [], + create(context) { + const sourceCode = context.sourceCode; + + return { + CallExpression(node) { + const method = getConsoleMethod(node); + if (!method) return; + + // Only flag when @actions/core alias is demonstrably in scope + if (!hasCoreInScope(node, sourceCode)) return; + + const replacement = CONSOLE_TO_CORE[method]!; + + // Build replacement argument text from original call + const argsText = node.arguments.map(arg => sourceCode.getText(arg)).join(", "); + + context.report({ + node, + messageId: "preferCoreLogging", + data: { method, replacement }, + suggest: [ + { + messageId: "replaceWithCoreMethod", + data: { replacement, args: argsText }, + fix(fixer) { + return fixer.replaceText(node, `${replacement}(${argsText})`); + }, + }, + ], + }); + }, + }; + }, +}); From 978ed10cf59c28b7ee793cb2ff383772e252c9c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:37:32 +0000 Subject: [PATCH 2/7] plan: address feedback to always favor core.* over console.* Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentic-auto-upgrade.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/agentic-auto-upgrade.yml b/.github/workflows/agentic-auto-upgrade.yml index 7035101ee35..e169cea32b6 100644 --- a/.github/workflows/agentic-auto-upgrade.yml +++ b/.github/workflows/agentic-auto-upgrade.yml @@ -34,7 +34,7 @@ name: Agentic Auto-Upgrade on: schedule: - - cron: "21 3 * * 5" # Weekly (auto-upgrade) + - cron: "11 4 * * 6" # Weekly (auto-upgrade) workflow_dispatch: permissions: From b07bd3bad38039720430a58276b49d52f8e2424d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:46:13 +0000 Subject: [PATCH 3/7] feat(eslint): make prefer-core-logging unconditional; fix console.* in setup/js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shim.cjs provides global.core in all Node.js contexts, so console.* should never be used — remove hasCoreInScope() scope restriction from the rule and fix all console.* calls in the files mentioned in the PR. Changes: - prefer-core-logging.ts: remove hasCoreInScope() function and check; flag all console.* calls unconditionally; update description/suggestion message to reflect global.core availability via shim.cjs - prefer-core-logging.test.ts: update formerly-valid cases to expect errors; add suggestions to new invalid test cases (11 tests still pass) - generate_workflow_overview.cjs: console.log → core.info (core is a param) - generate_usage_activity_summary.cjs: require shim.cjs + console.log → core.info - merge_remote_agent_github_folder.cjs: require shim.cjs; remove local coreObj fallback that used console.*; replace all coreObj.* → core.* Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../js/generate_usage_activity_summary.cjs | 4 +- .../setup/js/generate_workflow_overview.cjs | 2 +- .../js/merge_remote_agent_github_folder.cjs | 101 ++++++++---------- .../src/rules/prefer-core-logging.test.ts | 53 +++++++-- .../src/rules/prefer-core-logging.ts | 36 +------ 5 files changed, 97 insertions(+), 99 deletions(-) diff --git a/actions/setup/js/generate_usage_activity_summary.cjs b/actions/setup/js/generate_usage_activity_summary.cjs index 58136adf567..2f0d72b12cc 100644 --- a/actions/setup/js/generate_usage_activity_summary.cjs +++ b/actions/setup/js/generate_usage_activity_summary.cjs @@ -12,6 +12,8 @@ const fs = require("fs"); const { globSync } = require("node:fs"); const path = require("path"); +require("./shim.cjs"); + const SQUID_STATUS_INDEX = 6; const SQUID_DECISION_INDEX = 7; const SQUID_DOMAIN_INDEX = 2; @@ -464,7 +466,7 @@ function main() { // Write summary to file const outputPath = "/tmp/gh-aw/usage/activity/summary.json"; fs.writeFileSync(outputPath, JSON.stringify(summary, null, 2), "utf-8"); - console.log(outputPath); + core.info(outputPath); } // Run main function diff --git a/actions/setup/js/generate_workflow_overview.cjs b/actions/setup/js/generate_workflow_overview.cjs index be0346d20a1..62886973d09 100644 --- a/actions/setup/js/generate_workflow_overview.cjs +++ b/actions/setup/js/generate_workflow_overview.cjs @@ -35,7 +35,7 @@ async function generateWorkflowOverview(core) { const summary = "
\n" + `${summaryLabel}\n\n` + details + "\n" + "
"; await core.summary.addRaw(summary).write(); - console.log("Generated workflow overview in step summary"); + core.info("Generated workflow overview in step summary"); } module.exports = { diff --git a/actions/setup/js/merge_remote_agent_github_folder.cjs b/actions/setup/js/merge_remote_agent_github_folder.cjs index d9ce358fbfd..ad03fab1341 100644 --- a/actions/setup/js/merge_remote_agent_github_folder.cjs +++ b/actions/setup/js/merge_remote_agent_github_folder.cjs @@ -9,7 +9,8 @@ * and merges it into the current repository, failing on conflicts. * * This script runs in a github-script context where core object - * is available globally. Do NOT use npm packages from the actions org. + * is available globally. When run as a plain Node.js process, shim.cjs + * provides global.core. Do NOT use npm packages from the actions org. * * Environment Variables: * - GH_AW_REPOSITORY_IMPORTS: JSON array of repository imports (e.g., '["owner/repo@ref"]') @@ -22,23 +23,11 @@ const fs = require("fs"); const path = require("path"); const { execFileSync } = require("child_process"); +require("./shim.cjs"); + const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_CONFIG, ERR_PARSE, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); -// Get the core object - in github-script context it's global, for testing we create a minimal version -const coreObj = - typeof core !== "undefined" - ? core - : { - info: msg => console.log(msg), - warning: msg => console.warn(msg), - error: msg => console.error(msg), - setFailed: msg => { - console.error(msg); - process.exitCode = 1; - }, - }; - /** * Parse the agent import specification to extract repository details * Format: owner/repo/path@ref or owner/repo@ref or owner/repo/path @@ -50,7 +39,7 @@ function parseAgentImportSpec(importSpec) { return null; } - coreObj.info(`Parsing import spec: ${importSpec}`); + core.info(`Parsing import spec: ${importSpec}`); // Remove section reference if present (file.md#Section) let cleanSpec = importSpec; @@ -66,7 +55,7 @@ function parseAgentImportSpec(importSpec) { // Parse path: owner/repo or owner/repo/path/to/file.md const slashParts = pathPart.split("/"); if (slashParts.length < 2) { - coreObj.warning(`Invalid import spec format: ${importSpec}`); + core.warning(`Invalid import spec format: ${importSpec}`); return null; } @@ -75,11 +64,11 @@ function parseAgentImportSpec(importSpec) { // Check if this is a local import (starts with . or doesn't have owner/repo format) if (owner.startsWith(".") || owner.includes("github/workflows")) { - coreObj.info("Import is local, skipping remote .github folder merge"); + core.info("Import is local, skipping remote .github folder merge"); return null; } - coreObj.info(`Parsed: owner=${owner}, repo=${repo}, ref=${ref}`); + core.info(`Parsed: owner=${owner}, repo=${repo}, ref=${ref}`); return { owner, repo, ref }; } @@ -177,7 +166,7 @@ function validateSafePath(userPath, basePath, name) { * @param {string} tempDir - Temporary directory for checkout */ function sparseCheckoutGithubFolder(owner, repo, ref, tempDir) { - coreObj.info(`Performing sparse checkout of .github folder from ${owner}/${repo}@${ref}`); + core.info(`Performing sparse checkout of .github folder from ${owner}/${repo}@${ref}`); // Validate inputs to prevent command injection validateGitParameter(owner, "owner"); @@ -190,29 +179,29 @@ function sparseCheckoutGithubFolder(owner, repo, ref, tempDir) { try { // Initialize git repository execFileSync("git", ["init"], { cwd: tempDir, stdio: "pipe" }); - coreObj.info("Initialized temporary git repository"); + core.info("Initialized temporary git repository"); // Configure sparse checkout execFileSync("git", ["config", "core.sparseCheckout", "true"], { cwd: tempDir, stdio: "pipe" }); - coreObj.info("Enabled sparse checkout"); + core.info("Enabled sparse checkout"); // Set sparse checkout pattern to only include .github folder const sparseCheckoutFile = path.join(tempDir, ".git", "info", "sparse-checkout"); fs.writeFileSync(sparseCheckoutFile, ".github/\n"); - coreObj.info("Configured sparse checkout pattern: .github/"); + core.info("Configured sparse checkout pattern: .github/"); // Add remote - using execFileSync prevents shell injection execFileSync("git", ["remote", "add", "origin", repoUrl], { cwd: tempDir, stdio: "pipe" }); - coreObj.info(`Added remote: ${repoUrl}`); + core.info(`Added remote: ${repoUrl}`); // Fetch and checkout - using execFileSync with validated ref - coreObj.info(`Fetching ref: ${ref}`); + core.info(`Fetching ref: ${ref}`); execFileSync("git", ["fetch", "--depth", "1", "origin", ref], { cwd: tempDir, stdio: "pipe" }); - coreObj.info("Checking out .github folder"); + core.info("Checking out .github folder"); execFileSync("git", ["checkout", "FETCH_HEAD"], { cwd: tempDir, stdio: "pipe" }); - coreObj.info("Sparse checkout completed successfully"); + core.info("Sparse checkout completed successfully"); } catch (error) { throw new Error(`${ERR_PARSE}: Sparse checkout failed: ${getErrorMessage(error)}`, { cause: error }); } @@ -226,7 +215,7 @@ function sparseCheckoutGithubFolder(owner, repo, ref, tempDir) { * @returns {{merged: number, conflicts: string[]}} */ function mergeGithubFolder(sourcePath, destPath) { - coreObj.info(`Merging .github folder from ${sourcePath} to ${destPath}`); + core.info(`Merging .github folder from ${sourcePath} to ${destPath}`); const conflicts = []; let mergedCount = 0; @@ -236,7 +225,7 @@ function mergeGithubFolder(sourcePath, destPath) { // Get all files from source .github folder const sourceFiles = getAllFiles(sourcePath); - coreObj.info(`Found ${sourceFiles.length} files in source .github folder`); + core.info(`Found ${sourceFiles.length} files in source .github folder`); for (const relativePath of sourceFiles) { // Validate relative path to prevent path traversal @@ -247,7 +236,7 @@ function mergeGithubFolder(sourcePath, destPath) { const topLevelFolder = pathParts[0]; if (!allowedSubfolders.includes(topLevelFolder)) { - coreObj.info(`Skipping file outside allowed subfolders: ${relativePath}`); + core.info(`Skipping file outside allowed subfolders: ${relativePath}`); continue; } @@ -262,21 +251,21 @@ function mergeGithubFolder(sourcePath, destPath) { if (!sourceContent.equals(destContent)) { conflicts.push(relativePath); - coreObj.error(`Conflict detected: ${relativePath}`); + core.error(`Conflict detected: ${relativePath}`); } else { - coreObj.info(`File already exists with same content: ${relativePath}`); + core.info(`File already exists with same content: ${relativePath}`); } } else { // Copy file to destination const destDir = path.dirname(destFile); if (!pathExists(destDir)) { fs.mkdirSync(destDir, { recursive: true }); - coreObj.info(`Created directory: ${path.relative(destPath, destDir)}`); + core.info(`Created directory: ${path.relative(destPath, destDir)}`); } fs.copyFileSync(sourceFile, destFile); mergedCount++; - coreObj.info(`Merged file: ${relativePath}`); + core.info(`Merged file: ${relativePath}`); } } @@ -291,7 +280,7 @@ function mergeGithubFolder(sourcePath, destPath) { * @param {string} workspace - Workspace path */ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { - coreObj.info(`Merging .github folder from ${owner}/${repo}@${ref} into workspace`); + core.info(`Merging .github folder from ${owner}/${repo}@${ref} into workspace`); // Calculate the pre-checked-out folder path // This matches the format generated by the compiler: .github/aw/imports/-- @@ -300,7 +289,7 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { const relativePath = `.github/aw/imports/${owner}-${repo}-${sanitizedRef}`; const checkoutPath = path.join(workspace, relativePath); - coreObj.info(`Looking for pre-checked-out repository at: ${checkoutPath}`); + core.info(`Looking for pre-checked-out repository at: ${checkoutPath}`); // Check if the pre-checked-out folder exists if (!pathExists(checkoutPath)) { @@ -310,7 +299,7 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { // Check if .github folder exists in the checked-out repository const sourceGithubFolder = path.join(checkoutPath, ".github"); if (!pathExists(sourceGithubFolder)) { - coreObj.warning(`Remote repository ${owner}/${repo}@${ref} does not contain a .github folder`); + core.warning(`Remote repository ${owner}/${repo}@${ref} does not contain a .github folder`); return; } @@ -320,24 +309,24 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { // Ensure destination .github folder exists if (!pathExists(destGithubFolder)) { fs.mkdirSync(destGithubFolder, { recursive: true }); - coreObj.info("Created .github folder in workspace"); + core.info("Created .github folder in workspace"); } const { merged, conflicts } = mergeGithubFolder(sourceGithubFolder, destGithubFolder); // Report results if (conflicts.length > 0) { - coreObj.error(`Found ${conflicts.length} file conflicts:`); + core.error(`Found ${conflicts.length} file conflicts:`); for (const conflict of conflicts) { - coreObj.error(` - ${conflict}`); + core.error(` - ${conflict}`); } throw new Error(`${ERR_VALIDATION}: Cannot merge .github folder from ${owner}/${repo}@${ref}: ${conflicts.length} file(s) conflict with existing files`); } if (merged > 0) { - coreObj.info(`Successfully merged ${merged} file(s) from ${owner}/${repo}@${ref}`); + core.info(`Successfully merged ${merged} file(s) from ${owner}/${repo}@${ref}`); } else { - coreObj.info("No new files to merge"); + core.info("No new files to merge"); } } @@ -346,12 +335,12 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) { */ async function main() { try { - coreObj.info("Starting remote .github folder merge"); + core.info("Starting remote .github folder merge"); // Check for repository imports (owner/repo@ref format - merge entire .github folder) const repositoryImportsEnv = process.env.GH_AW_REPOSITORY_IMPORTS; if (repositoryImportsEnv) { - coreObj.info(`Repository imports detected: ${repositoryImportsEnv}`); + core.info(`Repository imports detected: ${repositoryImportsEnv}`); // Parse the JSON array of repository imports let repositoryImports; @@ -373,11 +362,11 @@ async function main() { // Process each repository import for (const repoImport of repositoryImports) { - coreObj.info(`Processing repository import: ${repoImport}`); + core.info(`Processing repository import: ${repoImport}`); const parsed = parseAgentImportSpec(repoImport); if (!parsed) { - coreObj.warning(`Skipping invalid repository import: ${repoImport}`); + core.warning(`Skipping invalid repository import: ${repoImport}`); continue; } @@ -385,7 +374,7 @@ async function main() { await mergeRepositoryGithubFolder(owner, repo, ref, workspace); } - coreObj.info("All repository imports processed successfully"); + core.info("All repository imports processed successfully"); return; } @@ -393,30 +382,30 @@ async function main() { // Get agent file path from environment const agentFile = process.env.GH_AW_AGENT_FILE; if (!agentFile) { - coreObj.info("No GH_AW_AGENT_FILE or GH_AW_REPOSITORY_IMPORTS specified, skipping .github folder merge"); + core.info("No GH_AW_AGENT_FILE or GH_AW_REPOSITORY_IMPORTS specified, skipping .github folder merge"); return; } - coreObj.info(`Agent file: ${agentFile}`); + core.info(`Agent file: ${agentFile}`); // Get agent import specification const importSpec = process.env.GH_AW_AGENT_IMPORT_SPEC; if (!importSpec) { - coreObj.info("No GH_AW_AGENT_IMPORT_SPEC specified, assuming local agent"); + core.info("No GH_AW_AGENT_IMPORT_SPEC specified, assuming local agent"); return; } - coreObj.info(`Agent import spec: ${importSpec}`); + core.info(`Agent import spec: ${importSpec}`); // Parse import specification const parsed = parseAgentImportSpec(importSpec); if (!parsed) { - coreObj.info("Agent is local or import spec is invalid, skipping remote merge"); + core.info("Agent is local or import spec is invalid, skipping remote merge"); return; } const { owner, repo, ref } = parsed; - coreObj.info(`Remote agent detected: ${owner}/${repo}@${ref}`); + core.info(`Remote agent detected: ${owner}/${repo}@${ref}`); // Get workspace path const workspace = process.env.GITHUB_WORKSPACE; @@ -426,17 +415,17 @@ async function main() { await mergeRepositoryGithubFolder(owner, repo, ref, workspace); - coreObj.info("Remote .github folder merge completed successfully"); + core.info("Remote .github folder merge completed successfully"); } catch (error) { const errorMessage = getErrorMessage(error); - coreObj.setFailed(`Failed to merge remote .github folder: ${errorMessage}`); + core.setFailed(`Failed to merge remote .github folder: ${errorMessage}`); } } // Run if executed directly (not imported) if (require.main === module) { main().catch(err => { - coreObj.setFailed(err && err.stack ? err.stack : String(err)); + core.setFailed(err && err.stack ? err.stack : String(err)); }); } diff --git a/eslint-factory/src/rules/prefer-core-logging.test.ts b/eslint-factory/src/rules/prefer-core-logging.test.ts index db00e6055de..1437fd2cd02 100644 --- a/eslint-factory/src/rules/prefer-core-logging.test.ts +++ b/eslint-factory/src/rules/prefer-core-logging.test.ts @@ -18,15 +18,37 @@ describe("prefer-core-logging", () => { expect(preferCoreLoggingRule.meta.hasSuggestions).toBe(true); }); - it("valid: console.log with no core in scope is allowed", () => { + it("invalid: plain console.log with no core in scope is now flagged", () => { ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { - valid: [ - // No core in scope — plain scripts are allowed to use console - `console.log("hello");`, - `console.error("bad thing");`, - `const foo = "bar"; console.log(foo);`, + valid: [], + invalid: [ + { + code: `console.log("hello");`, + errors: [ + { messageId: "preferCoreLogging", data: { method: "log", replacement: "core.info" }, suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `"hello"` }, output: `core.info("hello");` }] }, + ], + }, + { + code: `console.error("bad thing");`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "error", replacement: "core.error" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.error", args: `"bad thing"` }, output: `core.error("bad thing");` }], + }, + ], + }, + { + code: `const foo = "bar"; console.log(foo);`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `foo` }, output: `const foo = "bar"; core.info(foo);` }], + }, + ], + }, ], - invalid: [], }); }); @@ -42,10 +64,21 @@ describe("prefer-core-logging", () => { }); }); - it("valid: console.log in a function where core is not a param and not declared", () => { + it("invalid: console.log in a function where core is not declared", () => { ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { - valid: [`function helper() { console.log("no core here"); }`], - invalid: [], + valid: [], + invalid: [ + { + code: `function helper() { console.log("no core here"); }`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `"no core here"` }, output: `function helper() { core.info("no core here"); }` }], + }, + ], + }, + ], }); }); diff --git a/eslint-factory/src/rules/prefer-core-logging.ts b/eslint-factory/src/rules/prefer-core-logging.ts index 9ebcabd05f8..0bfad619068 100644 --- a/eslint-factory/src/rules/prefer-core-logging.ts +++ b/eslint-factory/src/rules/prefer-core-logging.ts @@ -1,5 +1,4 @@ -import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; -import { CORE_ALIASES } from "./core-aliases"; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); @@ -27,28 +26,6 @@ function getConsoleMethod(node: TSESTree.CallExpression): string | null { return prop.name in CONSOLE_TO_CORE ? prop.name : null; } -/** - * Walks the scope chain to determine whether any binding for a known - * @actions/core alias (`core`, `coreObj`) is visible at the given node. - * - * Accepted patterns: - * - `const core = require("@actions/core")` - * - `import * as core from "@actions/core"` - * - A function parameter named `core` or `coreObj` (used in github-script style) - */ -function hasCoreInScope(node: TSESTree.Node, sourceCode: TSESLint.SourceCode): boolean { - let scope: TSESLint.Scope.Scope | null = sourceCode.getScope(node); - while (scope) { - for (const variable of scope.variables) { - if (CORE_ALIASES.has(variable.name) && variable.defs.length > 0) { - return true; - } - } - scope = scope.upper; - } - return false; -} - export const preferCoreLoggingRule = createRule({ name: "prefer-core-logging", meta: { @@ -56,14 +33,14 @@ export const preferCoreLoggingRule = createRule({ hasSuggestions: true, docs: { description: - "Prefer @actions/core logging methods (core.info, core.error, core.warning, core.debug) over console.* in files that have access to @actions/core. " + + "Prefer @actions/core logging methods (core.info, core.error, core.warning, core.debug) over console.* — " + + "global.core is always available via shim.cjs in Node.js context and via github-script in Actions context. " + "console.* bypasses GitHub Actions' built-in secret masking and structured annotation system; core logging ensures secrets in output are redacted and messages appear correctly in the Actions UI.", }, schema: [], messages: { - preferCoreLogging: - "Use {{replacement}} instead of console.{{method}}() — @actions/core logging masks secrets and integrates with the Actions annotation system. console.* output is not masked.", - replaceWithCoreMethod: "Replace with {{replacement}}({{args}}) — ensure a @actions/core alias (core / coreObj) is in scope.", + preferCoreLogging: "Use {{replacement}} instead of console.{{method}}() — @actions/core logging masks secrets and integrates with the Actions annotation system. console.* output is not masked.", + replaceWithCoreMethod: "Replace with {{replacement}}({{args}}).", }, }, defaultOptions: [], @@ -75,9 +52,6 @@ export const preferCoreLoggingRule = createRule({ const method = getConsoleMethod(node); if (!method) return; - // Only flag when @actions/core alias is demonstrably in scope - if (!hasCoreInScope(node, sourceCode)) return; - const replacement = CONSOLE_TO_CORE[method]!; // Build replacement argument text from original call From 462a4f9e3147c60544c80a7c4dc08872d3fcae6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:02:48 +0000 Subject: [PATCH 4/7] fix(test): update generate_workflow_overview test to check core.info instead of console.log Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentic-auto-upgrade.yml | 2 +- actions/setup/js/generate_workflow_overview.test.cjs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/agentic-auto-upgrade.yml b/.github/workflows/agentic-auto-upgrade.yml index 7035101ee35..e169cea32b6 100644 --- a/.github/workflows/agentic-auto-upgrade.yml +++ b/.github/workflows/agentic-auto-upgrade.yml @@ -34,7 +34,7 @@ name: Agentic Auto-Upgrade on: schedule: - - cron: "21 3 * * 5" # Weekly (auto-upgrade) + - cron: "11 4 * * 6" # Weekly (auto-upgrade) workflow_dispatch: permissions: diff --git a/actions/setup/js/generate_workflow_overview.test.cjs b/actions/setup/js/generate_workflow_overview.test.cjs index c87e666af38..01d8cf30e07 100644 --- a/actions/setup/js/generate_workflow_overview.test.cjs +++ b/actions/setup/js/generate_workflow_overview.test.cjs @@ -135,13 +135,8 @@ describe("generate_workflow_overview.cjs", () => { }; fs.writeFileSync(awInfoPath, JSON.stringify(awInfo)); - // Capture console.log - const consoleSpy = vi.spyOn(console, "log"); - await generateWorkflowOverview(mockCore); - expect(consoleSpy).toHaveBeenCalledWith("Generated workflow overview in step summary"); - - consoleSpy.mockRestore(); + expect(mockCore.info).toHaveBeenCalledWith("Generated workflow overview in step summary"); }); }); From 78cf96d144541270c9175cce83edc8eed88f5918 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:27:39 +0000 Subject: [PATCH 5/7] fix(eslint): correct misleading security claim in prefer-core-logging rule docs Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- eslint-factory/src/rules/prefer-core-logging.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint-factory/src/rules/prefer-core-logging.ts b/eslint-factory/src/rules/prefer-core-logging.ts index 0bfad619068..afaf010640f 100644 --- a/eslint-factory/src/rules/prefer-core-logging.ts +++ b/eslint-factory/src/rules/prefer-core-logging.ts @@ -35,11 +35,11 @@ export const preferCoreLoggingRule = createRule({ description: "Prefer @actions/core logging methods (core.info, core.error, core.warning, core.debug) over console.* — " + "global.core is always available via shim.cjs in Node.js context and via github-script in Actions context. " + - "console.* bypasses GitHub Actions' built-in secret masking and structured annotation system; core logging ensures secrets in output are redacted and messages appear correctly in the Actions UI.", + "core.* methods integrate with the Actions annotation system (errors and warnings appear as file annotations in the UI) and produce structured log output; console.* does not.", }, schema: [], messages: { - preferCoreLogging: "Use {{replacement}} instead of console.{{method}}() — @actions/core logging masks secrets and integrates with the Actions annotation system. console.* output is not masked.", + preferCoreLogging: "Use {{replacement}} instead of console.{{method}}() — @actions/core logging integrates with the Actions annotation system and structured output. global.core is always available via shim.cjs.", replaceWithCoreMethod: "Replace with {{replacement}}({{args}}).", }, }, From 035848390f6d83b68c95a555d88479881c65a69c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:59:13 +0000 Subject: [PATCH 6/7] fix(make): use absolute paths in test-impacted-js to avoid cd drift between setup/js and eslint-factory Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .github/workflows/agentic-auto-upgrade.yml | 2 +- Makefile | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/agentic-auto-upgrade.yml b/.github/workflows/agentic-auto-upgrade.yml index e169cea32b6..7035101ee35 100644 --- a/.github/workflows/agentic-auto-upgrade.yml +++ b/.github/workflows/agentic-auto-upgrade.yml @@ -34,7 +34,7 @@ name: Agentic Auto-Upgrade on: schedule: - - cron: "11 4 * * 6" # Weekly (auto-upgrade) + - cron: "21 3 * * 5" # Weekly (auto-upgrade) workflow_dispatch: permissions: diff --git a/Makefile b/Makefile index 73854ebf738..8addfac07b8 100644 --- a/Makefile +++ b/Makefile @@ -260,13 +260,14 @@ test-impacted-js: build-js fi; \ CHANGED_SETUP_JS_FILES=$$(printf '%s\n' "$$CHANGED_JS_FILES" | grep '^actions/setup/js/' || true); \ CHANGED_ESLINT_FACTORY_FILES=$$(printf '%s\n' "$$CHANGED_JS_FILES" | grep '^eslint-factory/' || true); \ + ROOT=$$(pwd); \ if [ -n "$$CHANGED_SETUP_JS_FILES" ]; then \ echo "Running impacted JavaScript unit tests in actions/setup/js for changed files: $$CHANGED_SETUP_JS_FILES"; \ - cd actions/setup/js && printf '%s\n' "$$CHANGED_SETUP_JS_FILES" | sed 's|^actions/setup/js/||' | tr '\n' '\0' | xargs -0 -r npm run test:js -- --no-file-parallelism --passWithNoTests $(JS_IMPACTED_TEST_EXCLUDES); \ + cd "$$ROOT/actions/setup/js" && printf '%s\n' "$$CHANGED_SETUP_JS_FILES" | sed 's|^actions/setup/js/||' | tr '\n' '\0' | xargs -0 -r npm run test:js -- --no-file-parallelism --passWithNoTests $(JS_IMPACTED_TEST_EXCLUDES); \ fi; \ if [ -n "$$CHANGED_ESLINT_FACTORY_FILES" ]; then \ echo "Running eslint-factory tests for changed files: $$CHANGED_ESLINT_FACTORY_FILES"; \ - cd eslint-factory && npm test; \ + cd "$$ROOT/eslint-factory" && npm test; \ fi # Test impacted Go unit tests only (excluding integration tests) From ac5dd5a41aa5ea5563e99a65cc5618062d290f63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:07:46 +0000 Subject: [PATCH 7/7] merge: sync with origin/main; include no-core-error-then-process-exit rule alongside prefer-core-logging Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .github/workflows/agentic-auto-upgrade.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/agentic-auto-upgrade.yml b/.github/workflows/agentic-auto-upgrade.yml index e169cea32b6..7035101ee35 100644 --- a/.github/workflows/agentic-auto-upgrade.yml +++ b/.github/workflows/agentic-auto-upgrade.yml @@ -34,7 +34,7 @@ name: Agentic Auto-Upgrade on: schedule: - - cron: "11 4 * * 6" # Weekly (auto-upgrade) + - cron: "21 3 * * 5" # Weekly (auto-upgrade) workflow_dispatch: permissions: