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: 1 addition & 1 deletion .github/workflows/agentic-auto-upgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion actions/setup/js/generate_usage_activity_summary.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -468,7 +470,7 @@ function main() {
} catch (err) {
throw new Error(`Failed to write file ${outputPath}: ${String(err)}`, { cause: err });
}
console.log(outputPath);
core.info(outputPath);
}

// Run main function
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/generate_workflow_overview.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function generateWorkflowOverview(core) {
const summary = "<details>\n" + `<summary>${summaryLabel}</summary>\n\n` + details + "\n" + "</details>";

await core.summary.addRaw(summary).write();
console.log("Generated workflow overview in step summary");
core.info("Generated workflow overview in step summary");
}

module.exports = {
Expand Down
7 changes: 1 addition & 6 deletions actions/setup/js/generate_workflow_overview.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
101 changes: 45 additions & 56 deletions actions/setup/js/merge_remote_agent_github_folder.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]')
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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;
}

Expand All @@ -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 };
}

Expand Down Expand Up @@ -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");
Expand All @@ -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 });
}
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;
}

Expand All @@ -268,9 +257,9 @@ 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
Expand All @@ -281,12 +270,12 @@ function mergeGithubFolder(sourcePath, destPath) {
} catch (err) {
throw new Error(`Failed to create directory ${destDir}: ${String(err)}`, { cause: err });
}
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}`);
}
}

Expand All @@ -301,7 +290,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/<owner>-<repo>-<sanitized-ref>
Expand All @@ -310,7 +299,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)) {
Expand All @@ -320,7 +309,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;
}

Expand All @@ -334,24 +323,24 @@ async function mergeRepositoryGithubFolder(owner, repo, ref, workspace) {
} catch (err) {
throw new Error(`Failed to create directory ${destGithubFolder}: ${String(err)}`, { cause: err });
}
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");
}
}

Expand All @@ -360,12 +349,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;
Expand All @@ -387,50 +376,50 @@ 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;
}

const { owner, repo, ref } = parsed;
await mergeRepositoryGithubFolder(owner, repo, ref, workspace);
}

coreObj.info("All repository imports processed successfully");
core.info("All repository imports processed successfully");
return;
}

// Legacy path: Handle agent file imports (for backward compatibility)
// 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;
Expand All @@ -440,17 +429,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));
});
}

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 @@ -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",
"gh-aw-custom/no-core-error-then-process-exit": "warn",
},
},
Expand Down
Loading
Loading