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/close_entity_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/// <reference types="@actions/github-script" />

const { loadAgentOutput } = require("./load_agent_output.cjs");
const { generateFooter } = require("./generate_footer.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
const { getRepositoryUrl } = require("./get_repository_url.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
Expand Down Expand Up @@ -57,7 +57,7 @@ function buildCommentBody(body, triggeringIssueNumber, triggeringPRNumber) {
const workflowSourceURL = process.env.GH_AW_WORKFLOW_SOURCE_URL || "";
const runUrl = buildRunUrl();

return body.trim() + getTrackerID("markdown") + generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined);
return body.trim() + getTrackerID("markdown") + generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/close_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

const { getErrorMessage } = require("./error_helpers.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
const { generateFooter } = require("./generate_footer.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");

/**
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
Expand Down Expand Up @@ -321,7 +321,7 @@ function buildCommentBody(body, triggeringIssueNumber, triggeringPRNumber) {
const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com";
const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;

return body.trim() + getTrackerID("markdown") + generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined);
return body.trim() + getTrackerID("markdown") + generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined);
}

module.exports = { main };
5 changes: 3 additions & 2 deletions actions/setup/js/create_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ function resetIssuesToAssignCopilot() {

const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs");
const { generateFooter, generateWorkflowIdMarker } = require("./generate_footer.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { generateWorkflowIdMarker } = require("./generate_footer.cjs");
const { getTrackerID } = require("./get_tracker_id.cjs");
const { generateTemporaryId, isTemporaryId, normalizeTemporaryId, replaceTemporaryIdReferences } = require("./temporary_id.cjs");
const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
Expand Down Expand Up @@ -444,7 +445,7 @@ async function main(config = {}) {
// Generate footer and add expiration using helper
// When footer is disabled, only add XML markers (no visible footer content)
if (includeFooter) {
const footer = addExpirationToFooter(generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd(), expiresHours, "Issue");
const footer = addExpirationToFooter(generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber).trimEnd(), expiresHours, "Issue");
bodyLines.push(``, ``, footer);
}

Expand Down
57 changes: 0 additions & 57 deletions actions/setup/js/generate_footer.cjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// @ts-check
/// <reference types="@actions/github-script" />

const fs = require("fs");
const { getMissingInfoSections } = require("./missing_messages_helper.cjs");
const { getBlockedDomains, generateBlockedDomainsSection } = require("./firewall_blocked_domains.cjs");

/**
* Generates a standalone workflow-id XML comment marker for searchability.
* This marker enables finding all items (issues, discussions, PRs, comments)
Expand Down Expand Up @@ -81,58 +77,6 @@ function generateXMLMarker(workflowName, runUrl) {
return `<!-- ${parts.join(", ")} -->`;
}

/**
* Generate footer with AI attribution and workflow installation instructions
* @param {string} workflowName - Name of the workflow
* @param {string} runUrl - URL of the workflow run
* @param {string} workflowSource - Source of the workflow (owner/repo/path@ref)
* @param {string} workflowSourceURL - GitHub URL for the workflow source
* @param {number|undefined} triggeringIssueNumber - Issue number that triggered this workflow
* @param {number|undefined} triggeringPRNumber - Pull request number that triggered this workflow
* @param {number|undefined} triggeringDiscussionNumber - Discussion number that triggered this workflow
* @returns {string} Footer text
*/
function generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber) {
let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;

// Add reference to triggering issue/PR/discussion if available
if (triggeringIssueNumber) {
footer += ` for #${triggeringIssueNumber}`;
} else if (triggeringPRNumber) {
footer += ` for #${triggeringPRNumber}`;
} else if (triggeringDiscussionNumber) {
footer += ` for discussion #${triggeringDiscussionNumber}`;
}

if (workflowSource && workflowSourceURL) {
// Load workflow install note template
const promptsDir = process.env.GH_AW_PROMPTS_DIR || "/opt/gh-aw/prompts";
const installNoteTemplatePath = `${promptsDir}/workflow_install_note.md`;
const installNoteTemplate = fs.readFileSync(installNoteTemplatePath, "utf8");
const installNote = installNoteTemplate.replace("{workflow_source}", workflowSource);
footer += `\n>\n> ${installNote}`;
}

// Add missing tools and data sections if available
const missingInfoSections = getMissingInfoSections();
if (missingInfoSections) {
footer += missingInfoSections;
}

// Add firewall blocked domains section if any domains were blocked
const blockedDomains = getBlockedDomains();
const blockedDomainsSection = generateBlockedDomainsSection(blockedDomains);
if (blockedDomainsSection) {
footer += blockedDomainsSection;
}

// Add XML comment marker for traceability
footer += "\n\n" + generateXMLMarker(workflowName, runUrl);

footer += "\n";
return footer;
}

/**
* Generate footer for expired entity closing comments
* @param {string} workflowName - Name of the workflow
Expand All @@ -154,7 +98,6 @@ function generateExpiredEntityFooter(workflowName, runUrl, workflowId) {
}

module.exports = {
generateFooter,
generateXMLMarker,
generateWorkflowIdMarker,
getWorkflowIdMarkerContent,
Expand Down
148 changes: 1 addition & 147 deletions actions/setup/js/generate_footer.test.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "fs";
import path from "path";
import os from "os";
import { describe, it, expect, beforeEach, vi } from "vitest";

// Mock the global objects that GitHub Actions provides
const mockCore = {
Expand Down Expand Up @@ -35,31 +32,11 @@ global.core = mockCore;
global.context = mockContext;

describe("generate_footer.cjs", () => {
let generateFooter;
let generateXMLMarker;
let generateWorkflowIdMarker;
let getWorkflowIdMarkerContent;
let testPromptsDir;
let originalEnv;

beforeEach(async () => {
// Save original environment
originalEnv = process.env.GH_AW_PROMPTS_DIR;

// Set up test prompts directory
testPromptsDir = path.join(os.tmpdir(), "gh-aw-test-footer", "prompts");
if (!fs.existsSync(testPromptsDir)) {
fs.mkdirSync(testPromptsDir, { recursive: true });
}

// Create the workflow install note template
const templatePath = path.join(testPromptsDir, "workflow_install_note.md");
const templateContent = "To add this workflow in your repository, run `gh aw add {workflow_source}`. See [usage guide](https://github.github.com/gh-aw/guides/packaging-imports/).";
fs.writeFileSync(templatePath, templateContent, "utf8");

// Set environment to use test directory
process.env.GH_AW_PROMPTS_DIR = testPromptsDir;

// Reset mocks
vi.clearAllMocks();
// Clear env vars
Expand All @@ -71,134 +48,11 @@ describe("generate_footer.cjs", () => {

// Dynamic import to get fresh module state
const module = await import("./generate_footer.cjs");
generateFooter = module.generateFooter;
generateXMLMarker = module.generateXMLMarker;
generateWorkflowIdMarker = module.generateWorkflowIdMarker;
getWorkflowIdMarkerContent = module.getWorkflowIdMarkerContent;
});

afterEach(() => {
// Restore original environment
if (originalEnv !== undefined) {
process.env.GH_AW_PROMPTS_DIR = originalEnv;
} else {
delete process.env.GH_AW_PROMPTS_DIR;
}

// Clean up test directory
if (testPromptsDir && fs.existsSync(testPromptsDir)) {
fs.rmSync(testPromptsDir, { recursive: true, force: true });
}
});

describe("generateFooter", () => {
it("should generate basic footer with workflow name and run URL", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).toContain("> AI generated by [Test Workflow](https://github.com/test/repo/actions/runs/123)");
expect(result).not.toContain("for #");
expect(result).not.toContain("gh aw add");
});

it("should include issue number reference when provided", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", 42, undefined, undefined);

expect(result).toContain("> AI generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for #42");
});

it("should include PR number reference when provided", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, 99, undefined);

expect(result).toContain("> AI generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for #99");
});

it("should include discussion number reference when provided", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, 7);

expect(result).toContain("> AI generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) for discussion #7");
});

it("should prioritize issue number over PR number", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", 42, 99, undefined);

expect(result).toContain("for #42");
expect(result).not.toContain("for #99");
});

it("should prioritize PR number over discussion number", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, 99, 7);

expect(result).toContain("for #99");
expect(result).not.toContain("for discussion #7");
});

it("should include workflow installation instructions when source is provided", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "owner/repo/workflow.md@main", "https://github.com/owner/repo/blob/main/workflow.md", undefined, undefined, undefined);

expect(result).toContain("gh aw add owner/repo/workflow.md@main");
expect(result).toContain("See [usage guide](https://github.github.com/gh-aw/guides/packaging-imports/)");
});

it("should not include installation instructions when source is empty", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).not.toContain("gh aw add");
});

it("should include both issue reference and installation instructions", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "owner/repo/workflow.md@main", "https://github.com/owner/repo/blob/main/workflow.md", 42, undefined, undefined);

expect(result).toContain("for #42");
expect(result).toContain("gh aw add owner/repo/workflow.md@main");
});

it("should start and end with newlines", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).toMatch(/^\n\n/);
expect(result).toMatch(/\n$/);
});

it("should handle special characters in workflow name", () => {
const result = generateFooter("Test Workflow (v2) [beta]", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).toContain("> AI generated by [Test Workflow (v2) [beta]](https://github.com/test/repo/actions/runs/123)");
});

it("should include XML comment marker for traceability", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).toContain("<!-- gh-aw-agentic-workflow: Test Workflow");
expect(result).toContain("run: https://github.com/test/repo/actions/runs/123 -->");
});

it("should include engine metadata in XML marker when env vars are set", async () => {
// Set env vars before re-importing
process.env.GH_AW_ENGINE_ID = "copilot";
process.env.GH_AW_ENGINE_VERSION = "1.0.0";
process.env.GH_AW_ENGINE_MODEL = "gpt-5";

// Re-import to pick up new env vars
vi.resetModules();
const freshModule = await import("./generate_footer.cjs");

const result = freshModule.generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).toContain("<!-- gh-aw-agentic-workflow: Test Workflow");
expect(result).toContain("engine: copilot");
expect(result).toContain("version: 1.0.0");
expect(result).toContain("model: gpt-5");
expect(result).toContain("run: https://github.com/test/repo/actions/runs/123 -->");
});

it("should not include blocked domains section when no firewall logs exist", () => {
const result = generateFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "", "", undefined, undefined, undefined);

expect(result).not.toContain("⚠️ Firewall blocked");
expect(result).not.toContain("<details>");
});
});

describe("generateXMLMarker", () => {
it("should generate basic XML marker with workflow name and run URL", () => {
const result = generateXMLMarker("Test Workflow", "https://github.com/test/repo/actions/runs/123");
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/mark_pull_request_as_ready_for_review.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
*/

const { generateFooter } = require("./generate_footer.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

Expand Down Expand Up @@ -171,7 +171,7 @@ async function main(config = {}) {
const triggeringDiscussionNumber = context.payload?.discussion?.number;

const sanitizedReason = sanitizeContent(item.reason);
const footer = generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
const footer = generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, triggeringDiscussionNumber);
const commentBody = `${sanitizedReason}\n\n${footer}`;

await addPullRequestComment(github, context.repo.owner, context.repo.repo, prNumber, commentBody);
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/pr_review_buffer.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* await buffer.submitReview();
*/

const { generateFooter } = require("./generate_footer.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
Expand Down Expand Up @@ -185,7 +185,7 @@ function createReviewBuffer() {
// Add footer to review body if enabled and we have footer context.
// Footer is always added (even for body-less reviews) to track which workflow submitted the review.
if (includeFooter && footerContext) {
body += generateFooter(
body += generateFooterWithMessages(
footerContext.workflowName,
footerContext.runUrl,
footerContext.workflowSource,
Expand Down
Loading