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
30 changes: 27 additions & 3 deletions actions/setup/js/safe_output_unified_handler_manager.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const PROJECT_RELATED_TYPES = new Set(Object.keys(PROJECT_HANDLER_MAP));
/**
* Load configuration for safe outputs
* Reads configuration from both GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG and GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG
* Automatically splits project handlers from regular config if they're in the wrong place
* @returns {{regular: Object, project: Object}} Safe outputs configuration for regular and project handlers
*/
function loadConfig() {
Expand All @@ -163,20 +164,36 @@ function loadConfig() {
if (process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG) {
try {
const config = JSON.parse(process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG);
core.info(`Loaded regular handler config: ${JSON.stringify(config)}`);
core.info(`Loaded config from GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${JSON.stringify(config)}`);

// Normalize config keys: convert hyphens to underscores
Object.assign(regular, Object.fromEntries(Object.entries(config).map(([k, v]) => [k.replace(/-/g, "_"), v])));
const normalizedEntries = Object.entries(config).map(([k, v]) => [k.replace(/-/g, "_"), v]);

// Automatically split project handlers from regular handlers
// Project handlers (update_project, create_project, create_project_status_update) require
// a separate Octokit client authenticated with GH_AW_PROJECT_GITHUB_TOKEN because they need
// Projects permissions that differ from regular handler permissions. This auto-split ensures
// backward compatibility with the Go compiler which puts all handlers in a unified config.
for (const [key, value] of normalizedEntries) {
if (PROJECT_RELATED_TYPES.has(key)) {
project[key] = value;
core.info(`Auto-moved ${key} from unified config to project config (requires project token)`);
} else {
regular[key] = value;
}
}
} catch (error) {
throw new Error(`Failed to parse GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ${getErrorMessage(error)}`);
}
}

// Load project handler config
// Load project handler config (if explicitly provided, merge with auto-split handlers)
if (process.env.GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG) {
try {
const config = JSON.parse(process.env.GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG);
core.info(`Loaded project handler config: ${JSON.stringify(config)}`);
// Normalize config keys: convert hyphens to underscores
// Explicitly provided project config takes precedence over auto-split config
Object.assign(project, Object.fromEntries(Object.entries(config).map(([k, v]) => [k.replace(/-/g, "_"), v])));
} catch (error) {
throw new Error(`Failed to parse GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: ${getErrorMessage(error)}`);
Expand All @@ -188,6 +205,13 @@ function loadConfig() {
throw new Error("At least one of GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG or GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG environment variables is required");
}

const regularCount = Object.keys(regular).length;
const projectCount = Object.keys(project).length;
core.info(`Configuration loaded: ${regularCount} regular handler${regularCount === 1 ? "" : "s"}, ${projectCount} project handler${projectCount === 1 ? "" : "s"}`);
if (projectCount > 0) {
core.info(`Project handlers: ${Object.keys(project).join(", ")}`);
}

return { regular, project };
}

Expand Down
110 changes: 110 additions & 0 deletions actions/setup/js/safe_output_unified_handler_manager.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,116 @@ describe("Unified Safe Output Handler Manager", () => {
expect(config.regular).toHaveProperty("create_issue");
expect(config.regular).not.toHaveProperty("create-issue");
});

it("should automatically split project handlers from unified config", () => {
// Simulate Go compiler putting all handlers in one config
process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG = JSON.stringify({
create_issue: { max: 5 },
add_comment: {},
update_project: { max: 20, project: "https://github.com/orgs/test/projects/1" },
create_project: { max: 1 },
create_project_status_update: { max: 1, project: "https://github.com/orgs/test/projects/1" },
});

const config = loadConfig();

// Regular handlers should stay in regular config
expect(config.regular).toHaveProperty("create_issue");
expect(config.regular).toHaveProperty("add_comment");
expect(config.regular).not.toHaveProperty("update_project");
expect(config.regular).not.toHaveProperty("create_project");
expect(config.regular).not.toHaveProperty("create_project_status_update");

// Project handlers should be moved to project config
expect(config.project).toHaveProperty("update_project");
expect(config.project).toHaveProperty("create_project");
expect(config.project).toHaveProperty("create_project_status_update");
expect(config.project.update_project).toEqual({ max: 20, project: "https://github.com/orgs/test/projects/1" });
});

it("should handle hyphenated project handler names and split correctly", () => {
// Test with hyphenated names (common in YAML/JSON configs)
process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG = JSON.stringify({
"create-issue": { max: 5 },
"update-project": { max: 20 },
"create-project-status-update": { max: 1 },
});

const config = loadConfig();

// Check normalization and splitting
expect(config.regular).toHaveProperty("create_issue");
expect(config.regular).not.toHaveProperty("update_project");
expect(config.project).toHaveProperty("update_project");
expect(config.project).toHaveProperty("create_project_status_update");
});

it("should prioritize explicit project config over auto-split handlers", () => {
// Both configs provided - explicit project config should take precedence
process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG = JSON.stringify({
create_issue: { max: 5 },
update_project: { max: 20, project: "url1" },
});
process.env.GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG = JSON.stringify({
update_project: { max: 50, project: "url2" }, // Should override auto-split config
create_project: { max: 1 },
});

const config = loadConfig();

expect(config.regular).toHaveProperty("create_issue");
expect(config.project).toHaveProperty("update_project");
expect(config.project).toHaveProperty("create_project");
// Explicit config should take precedence
expect(config.project.update_project).toEqual({ max: 50, project: "url2" });
});

it("should handle smoke-project workflow scenario correctly", () => {
// Simulate the exact config from smoke-project workflow
// where Go compiler puts all handlers (including project handlers) in one config
process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG = JSON.stringify({
add_comment: { hide_older_comments: true, max: 2 },
add_labels: { allowed: ["smoke-project"], "target-repo": "github-agentic-workflows/demo-repository" },
create_issue: { close_older_issues: true, expires: 2, group: true, max: 1, "target-repo": "github-agentic-workflows/demo-repository" },
create_project_status_update: { "github-token": "***", max: 1, project: "https://github.com/orgs/github-agentic-workflows/projects/1" },
missing_data: {},
missing_tool: {},
remove_labels: { allowed: ["smoke-project"], "target-repo": "github-agentic-workflows/demo-repository" },
update_project: {
"github-token": "***",
max: 20,
project: "https://github.com/orgs/github-agentic-workflows/projects/1",
views: [
{ name: "Smoke Test Board", layout: "board", filter: "is:open" },
{ name: "Smoke Test Table", layout: "table" },
],
},
});

const config = loadConfig();

// Regular handlers should stay in regular config
expect(config.regular).toHaveProperty("add_comment");
expect(config.regular).toHaveProperty("add_labels");
expect(config.regular).toHaveProperty("create_issue");
expect(config.regular).toHaveProperty("missing_data");
expect(config.regular).toHaveProperty("missing_tool");
expect(config.regular).toHaveProperty("remove_labels");

// Project handlers should be automatically moved to project config
expect(config.project).toHaveProperty("update_project");
expect(config.project).toHaveProperty("create_project_status_update");

// Regular handlers should NOT contain project handlers
expect(config.regular).not.toHaveProperty("update_project");
expect(config.regular).not.toHaveProperty("create_project_status_update");

// Verify project handler configs are intact
expect(config.project.update_project).toHaveProperty("max", 20);
expect(config.project.update_project).toHaveProperty("project");
expect(config.project.update_project).toHaveProperty("views");
expect(config.project.create_project_status_update).toHaveProperty("max", 1);
});
});

describe("setupProjectGitHubClient", () => {
Expand Down
152 changes: 152 additions & 0 deletions pkg/cli/templates/create-agentic-workflow.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading