diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index 3442b14385..978c48c15f 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -17,4 +17,286 @@ jobs:
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
- ${getMaintenanceScript()}
+ // @ts-check
+///
+
+/**
+ * Maximum number of discussions to update per run
+ */
+const MAX_UPDATES_PER_RUN = 100;
+
+/**
+ * Delay between GraphQL API calls in milliseconds to avoid rate limiting
+ */
+const GRAPHQL_DELAY_MS = 500;
+
+/**
+ * Delay execution for a specified number of milliseconds
+ * @param {number} ms - Milliseconds to delay
+ * @returns {Promise}
+ */
+function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Search for open discussions with expiration markers
+ * @param {any} github - GitHub GraphQL instance
+ * @param {string} owner - Repository owner
+ * @param {string} repo - Repository name
+ * @returns {Promise>} Matching discussions
+ */
+async function searchDiscussionsWithExpiration(github, owner, repo) {
+ const discussions = [];
+ let hasNextPage = true;
+ let cursor = null;
+
+ while (hasNextPage) {
+ const query = `
+ query($owner: String!, $repo: String!, $cursor: String) {
+ repository(owner: $owner, name: $repo) {
+ discussions(first: 100, after: $cursor, states: [OPEN]) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ nodes {
+ id
+ number
+ title
+ url
+ body
+ createdAt
+ }
+ }
+ }
+ }
+ `;
+
+ const result = await github.graphql(query, {
+ owner: owner,
+ repo: repo,
+ cursor: cursor,
+ });
+
+ if (!result || !result.repository || !result.repository.discussions) {
+ break;
+ }
+
+ const nodes = result.repository.discussions.nodes || [];
+
+ // Filter for discussions with agentic workflow markers and expiration comments
+ for (const discussion of nodes) {
+ // Check if created by an agentic workflow (body contains "> AI generated by" at start of line)
+ const agenticPattern = /^> AI generated by/m;
+ const isAgenticWorkflow = discussion.body && agenticPattern.test(discussion.body);
+
+ if (!isAgenticWorkflow) {
+ continue;
+ }
+
+ // Check if has expiration marker
+ const expirationPattern = //;
+ const match = discussion.body ? discussion.body.match(expirationPattern) : null;
+
+ if (match) {
+ discussions.push(discussion);
+ }
+ }
+
+ hasNextPage = result.repository.discussions.pageInfo.hasNextPage;
+ cursor = result.repository.discussions.pageInfo.endCursor;
+ }
+
+ return discussions;
+}
+
+/**
+ * Extract expiration date from discussion body
+ * @param {string} body - Discussion body
+ * @returns {Date|null} Expiration date or null if not found/invalid
+ */
+function extractExpirationDate(body) {
+ const expirationPattern = //;
+ const match = body.match(expirationPattern);
+
+ if (!match) {
+ return null;
+ }
+
+ const expirationISO = match[1].trim();
+ const expirationDate = new Date(expirationISO);
+
+ // Validate the date
+ if (isNaN(expirationDate.getTime())) {
+ return null;
+ }
+
+ return expirationDate;
+}
+
+/**
+ * Validate discussion creation date
+ * @param {string} createdAt - ISO 8601 creation date
+ * @returns {boolean} True if valid
+ */
+function validateCreationDate(createdAt) {
+ const creationDate = new Date(createdAt);
+ return !isNaN(creationDate.getTime());
+}
+
+/**
+ * Add comment to a GitHub Discussion using GraphQL
+ * @param {any} github - GitHub GraphQL instance
+ * @param {string} discussionId - Discussion node ID
+ * @param {string} message - Comment body
+ * @returns {Promise<{id: string, url: string}>} Comment details
+ */
+async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+
+ return result.addDiscussionComment.comment;
+}
+
+/**
+ * Close a GitHub Discussion as OUTDATED using GraphQL
+ * @param {any} github - GitHub GraphQL instance
+ * @param {string} discussionId - Discussion node ID
+ * @returns {Promise<{id: string, url: string}>} Discussion details
+ */
+async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+
+ return result.closeDiscussion.discussion;
+}
+
+async function main() {
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+
+ core.info(`Searching for expired discussions in ${owner}/${repo}`);
+
+ // Search for discussions with expiration markers
+ const discussionsWithExpiration = await searchDiscussionsWithExpiration(github, owner, repo);
+
+ if (discussionsWithExpiration.length === 0) {
+ core.info("No discussions with expiration markers found");
+ return;
+ }
+
+ core.info(`Found ${discussionsWithExpiration.length} discussion(s) with expiration markers`);
+
+ // Check which discussions are expired
+ const now = new Date();
+ const expiredDiscussions = [];
+
+ for (const discussion of discussionsWithExpiration) {
+ // Validate creation date
+ if (!validateCreationDate(discussion.createdAt)) {
+ core.warning(`Discussion #${discussion.number} has invalid creation date, skipping`);
+ continue;
+ }
+
+ // Extract and validate expiration date
+ const expirationDate = extractExpirationDate(discussion.body);
+ if (!expirationDate) {
+ core.warning(`Discussion #${discussion.number} has invalid expiration date, skipping`);
+ continue;
+ }
+
+ // Check if expired
+ if (now >= expirationDate) {
+ expiredDiscussions.push({
+ ...discussion,
+ expirationDate: expirationDate,
+ });
+ }
+ }
+
+ if (expiredDiscussions.length === 0) {
+ core.info("No expired discussions found");
+ return;
+ }
+
+ core.info(`Found ${expiredDiscussions.length} expired discussion(s)`);
+
+ // Limit to MAX_UPDATES_PER_RUN
+ const discussionsToClose = expiredDiscussions.slice(0, MAX_UPDATES_PER_RUN);
+
+ if (expiredDiscussions.length > MAX_UPDATES_PER_RUN) {
+ core.warning(`Found ${expiredDiscussions.length} expired discussions, but only closing the first ${MAX_UPDATES_PER_RUN}`);
+ }
+
+ let closedCount = 0;
+ const closedDiscussions = [];
+
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+
+ try {
+ const closingMessage = `This discussion was automatically closed because it expired on ${discussion.expirationDate.toISOString()}.`;
+
+ // Add comment first
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+
+ // Then close the discussion as outdated
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ title: discussion.title,
+ });
+
+ closedCount++;
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ // Continue with other discussions even if one fails
+ }
+
+ // Add delay between GraphQL operations to avoid rate limiting (except for the last item)
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+
+ // Write summary
+ if (closedCount > 0) {
+ let summaryContent = `## Closed Expired Discussions\n\n`;
+ summaryContent += `Closed **${closedCount}** expired discussion(s):\n\n`;
+ for (const closed of closedDiscussions) {
+ summaryContent += `- Discussion #${closed.number}: [${closed.title}](${closed.url})\n`;
+ }
+ await core.summary.addRaw(summaryContent).write();
+ }
+
+ core.info(`Successfully closed ${closedCount} expired discussion(s)`);
+}
+
+await main();
+
diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml
index fd05ead12e..01c4739a42 100644
--- a/.github/workflows/artifacts-summary.lock.yml
+++ b/.github/workflows/artifacts-summary.lock.yml
@@ -5635,12 +5635,381 @@ jobs:
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml
index e0cf3dd218..cacf3a3d6e 100644
--- a/.github/workflows/audit-workflows.lock.yml
+++ b/.github/workflows/audit-workflows.lock.yml
@@ -6385,12 +6385,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml
index 348bd0cd30..465f1f7a6e 100644
--- a/.github/workflows/blog-auditor.lock.yml
+++ b/.github/workflows/blog-auditor.lock.yml
@@ -5495,12 +5495,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml
index 1564de557b..f927703e8f 100644
--- a/.github/workflows/breaking-change-checker.lock.yml
+++ b/.github/workflows/breaking-change-checker.lock.yml
@@ -5689,20 +5689,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml
index cec4bb8e18..4cde120c29 100644
--- a/.github/workflows/ci-doctor.lock.yml
+++ b/.github/workflows/ci-doctor.lock.yml
@@ -6382,20 +6382,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml
index 16420bc847..4e2d321409 100644
--- a/.github/workflows/cli-consistency-checker.lock.yml
+++ b/.github/workflows/cli-consistency-checker.lock.yml
@@ -5680,20 +5680,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml
index 57e7a5d71a..78a7110dbd 100644
--- a/.github/workflows/cli-version-checker.lock.yml
+++ b/.github/workflows/cli-version-checker.lock.yml
@@ -5448,20 +5448,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml
index 7fb344cb39..a5a51ef4e5 100644
--- a/.github/workflows/cloclo.lock.yml
+++ b/.github/workflows/cloclo.lock.yml
@@ -6945,9 +6945,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml
index aa8f312339..617b681665 100644
--- a/.github/workflows/commit-changes-analyzer.lock.yml
+++ b/.github/workflows/commit-changes-analyzer.lock.yml
@@ -5370,12 +5370,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml
index bd53bade63..b0561e1049 100644
--- a/.github/workflows/copilot-agent-analysis.lock.yml
+++ b/.github/workflows/copilot-agent-analysis.lock.yml
@@ -6061,12 +6061,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml
index 3567fd4f73..c600623a8d 100644
--- a/.github/workflows/copilot-pr-merged-report.lock.yml
+++ b/.github/workflows/copilot-pr-merged-report.lock.yml
@@ -7258,12 +7258,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml
index 172048a416..92e34401fb 100644
--- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml
+++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml
@@ -7167,12 +7167,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml
index 7f85fd5fc8..0d76d1b521 100644
--- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml
+++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml
@@ -6248,12 +6248,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml
index 4c927a6b00..dd6103f628 100644
--- a/.github/workflows/copilot-session-insights.lock.yml
+++ b/.github/workflows/copilot-session-insights.lock.yml
@@ -7471,12 +7471,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml
index f98681494d..ef17c22452 100644
--- a/.github/workflows/daily-code-metrics.lock.yml
+++ b/.github/workflows/daily-code-metrics.lock.yml
@@ -6516,12 +6516,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml
index 49d57f4220..65090a7e53 100644
--- a/.github/workflows/daily-copilot-token-report.lock.yml
+++ b/.github/workflows/daily-copilot-token-report.lock.yml
@@ -7340,12 +7340,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml
index f1d8dabada..965debe87d 100644
--- a/.github/workflows/daily-doc-updater.lock.yml
+++ b/.github/workflows/daily-doc-updater.lock.yml
@@ -5218,9 +5218,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml
index c50cbd1742..bce8b56968 100644
--- a/.github/workflows/daily-file-diet.lock.yml
+++ b/.github/workflows/daily-file-diet.lock.yml
@@ -5472,20 +5472,272 @@ jobs:
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml
index 3410f23e3d..eaf3507435 100644
--- a/.github/workflows/daily-firewall-report.lock.yml
+++ b/.github/workflows/daily-firewall-report.lock.yml
@@ -6762,12 +6762,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml
index 12989c5967..b31d140894 100644
--- a/.github/workflows/daily-issues-report.lock.yml
+++ b/.github/workflows/daily-issues-report.lock.yml
@@ -7341,12 +7341,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml
index 5360fce85a..6be6836709 100644
--- a/.github/workflows/daily-multi-device-docs-tester.lock.yml
+++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml
@@ -5082,20 +5082,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml
index 4fe47802a0..754c478f8a 100644
--- a/.github/workflows/daily-news.lock.yml
+++ b/.github/workflows/daily-news.lock.yml
@@ -7229,12 +7229,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml
index a138847db5..e4513ae5e6 100644
--- a/.github/workflows/daily-performance-summary.lock.yml
+++ b/.github/workflows/daily-performance-summary.lock.yml
@@ -8555,12 +8555,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml
index be01995637..10822efc7a 100644
--- a/.github/workflows/daily-repo-chronicle.lock.yml
+++ b/.github/workflows/daily-repo-chronicle.lock.yml
@@ -6773,12 +6773,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml
index 6e4809a74d..ae4fb6b14f 100644
--- a/.github/workflows/daily-team-status.lock.yml
+++ b/.github/workflows/daily-team-status.lock.yml
@@ -5520,12 +5520,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml
index 24d4a6b76c..f0bf92a0cc 100644
--- a/.github/workflows/daily-workflow-updater.lock.yml
+++ b/.github/workflows/daily-workflow-updater.lock.yml
@@ -5717,9 +5717,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml
index a33df07986..3752f4f764 100644
--- a/.github/workflows/deep-report.lock.yml
+++ b/.github/workflows/deep-report.lock.yml
@@ -5988,12 +5988,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml
index 5de8dbb2e7..cbd2449487 100644
--- a/.github/workflows/dependabot-go-checker.lock.yml
+++ b/.github/workflows/dependabot-go-checker.lock.yml
@@ -6295,20 +6295,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml
index d6c0da0f1c..029b4792d8 100644
--- a/.github/workflows/developer-docs-consolidator.lock.yml
+++ b/.github/workflows/developer-docs-consolidator.lock.yml
@@ -6266,12 +6266,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
@@ -6630,9 +6999,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml
index 90b17d458f..a58af0c63c 100644
--- a/.github/workflows/dictation-prompt.lock.yml
+++ b/.github/workflows/dictation-prompt.lock.yml
@@ -5661,9 +5661,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml
index 45f6246388..09f9fd4f52 100644
--- a/.github/workflows/docs-noob-tester.lock.yml
+++ b/.github/workflows/docs-noob-tester.lock.yml
@@ -5698,12 +5698,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml
index 20678fa28f..e6cef1fe1e 100644
--- a/.github/workflows/duplicate-code-detector.lock.yml
+++ b/.github/workflows/duplicate-code-detector.lock.yml
@@ -5265,20 +5265,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml
index fda5447b46..89f3262cce 100644
--- a/.github/workflows/example-workflow-analyzer.lock.yml
+++ b/.github/workflows/example-workflow-analyzer.lock.yml
@@ -4948,12 +4948,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/github-mcp-structural-analysis.lock.yml b/.github/workflows/github-mcp-structural-analysis.lock.yml
index 85c8bb69bb..5682120b06 100644
--- a/.github/workflows/github-mcp-structural-analysis.lock.yml
+++ b/.github/workflows/github-mcp-structural-analysis.lock.yml
@@ -6194,12 +6194,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml
index 46ae165528..10f10e0837 100644
--- a/.github/workflows/github-mcp-tools-report.lock.yml
+++ b/.github/workflows/github-mcp-tools-report.lock.yml
@@ -5973,12 +5973,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
@@ -6337,9 +6706,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml
index 1de68b32c7..376f6331d7 100644
--- a/.github/workflows/glossary-maintainer.lock.yml
+++ b/.github/workflows/glossary-maintainer.lock.yml
@@ -6695,9 +6695,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/go-fan.lock.yml b/.github/workflows/go-fan.lock.yml
index 21d387dd98..4fdd77910d 100644
--- a/.github/workflows/go-fan.lock.yml
+++ b/.github/workflows/go-fan.lock.yml
@@ -5572,12 +5572,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml
index 8c495173f9..569abd7ee2 100644
--- a/.github/workflows/go-logger.lock.yml
+++ b/.github/workflows/go-logger.lock.yml
@@ -5443,9 +5443,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml
index d37359d84f..c6c870bb66 100644
--- a/.github/workflows/go-pattern-detector.lock.yml
+++ b/.github/workflows/go-pattern-detector.lock.yml
@@ -5112,20 +5112,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml
index e3e0e5f384..dbf482fc13 100644
--- a/.github/workflows/instructions-janitor.lock.yml
+++ b/.github/workflows/instructions-janitor.lock.yml
@@ -5208,9 +5208,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml
index 72d626bd8e..296146c4c6 100644
--- a/.github/workflows/issue-arborist.lock.yml
+++ b/.github/workflows/issue-arborist.lock.yml
@@ -5279,12 +5279,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml
index ce7f9dfb7a..390a9b4f05 100644
--- a/.github/workflows/lockfile-stats.lock.yml
+++ b/.github/workflows/lockfile-stats.lock.yml
@@ -5613,12 +5613,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml
index afacbde23d..fa39bf2acc 100644
--- a/.github/workflows/mcp-inspector.lock.yml
+++ b/.github/workflows/mcp-inspector.lock.yml
@@ -6295,12 +6295,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/org-health-report.lock.yml b/.github/workflows/org-health-report.lock.yml
index 6e43d1bd4f..db6fb21f48 100644
--- a/.github/workflows/org-health-report.lock.yml
+++ b/.github/workflows/org-health-report.lock.yml
@@ -7162,12 +7162,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml
index c222c5293c..d7a825f1f4 100644
--- a/.github/workflows/plan.lock.yml
+++ b/.github/workflows/plan.lock.yml
@@ -6770,20 +6770,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml
index 50a97c4340..f568c9c7f5 100644
--- a/.github/workflows/poem-bot.lock.yml
+++ b/.github/workflows/poem-bot.lock.yml
@@ -8420,12 +8420,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
@@ -8687,86 +9056,338 @@ jobs:
} else if (closeOlderEnabled && !hasMatchingCriteria) {
core.warning("close-older-discussions is enabled but no title-prefix or labels are set - skipping close older discussions");
}
- } catch (error) {
- core.error(`β Failed to create discussion "${title}" in ${itemRepo}: ${error instanceof Error ? error.message : String(error)}`);
- throw error;
+ } catch (error) {
+ core.error(`β Failed to create discussion "${title}" in ${itemRepo}: ${error instanceof Error ? error.message : String(error)}`);
+ throw error;
+ }
+ }
+ if (createdDiscussions.length > 0) {
+ let summaryContent = "\n\n## GitHub Discussions\n";
+ for (const discussion of createdDiscussions) {
+ const repoLabel = discussion._repo !== defaultTargetRepo ? ` (${discussion._repo})` : "";
+ summaryContent += `- Discussion #${discussion.number}${repoLabel}: [${discussion.title}](${discussion.url})\n`;
+ }
+ if (closedDiscussionsSummary.length > 0) {
+ summaryContent += "\n### Closed Older Discussions\n";
+ for (const closed of closedDiscussionsSummary) {
+ summaryContent += `- Discussion #${closed.number}: [View](${closed.url}) (marked as outdated)\n`;
+ }
+ }
+ await core.summary.addRaw(summaryContent).write();
+ }
+ core.info(`Successfully created ${createdDiscussions.length} discussion(s)`);
+ }
+ await main();
+
+ create_issue:
+ needs:
+ - agent
+ - detection
+ if: >
+ (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_issue'))) &&
+ (needs.detection.outputs.success == 'true')
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ issues: write
+ timeout-minutes: 10
+ outputs:
+ issue_number: ${{ steps.create_issue.outputs.issue_number }}
+ issue_url: ${{ steps.create_issue.outputs.issue_url }}
+ temporary_id_map: ${{ steps.create_issue.outputs.temporary_id_map }}
+ steps:
+ - name: Download agent output artifact
+ continue-on-error: true
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
+ with:
+ name: agent_output.json
+ path: /tmp/gh-aw/safeoutputs/
+ - name: Setup agent output environment variable
+ run: |
+ mkdir -p /tmp/gh-aw/safeoutputs/
+ find "/tmp/gh-aw/safeoutputs/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
+ - name: Create Output Issue
+ id: create_issue
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_ISSUE_TITLE_PREFIX: "[π POEM-BOT] "
+ GH_AW_ISSUE_LABELS: "poetry,automation,ai-generated"
+ GH_AW_WORKFLOW_NAME: "Poem Bot - A Creative Agentic Workflow"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: "gpt-5"
+ GH_AW_SAFE_OUTPUTS_STAGED: "true"
+ GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e πͺΆ *Verses penned by [{workflow_name}]({run_url})*\",\"runStarted\":\"π Hear ye! The muse stirs! [{workflow_name}]({run_url}) takes quill in hand for this {event_type}...\",\"runSuccess\":\"πͺΆ The poem is writ! [{workflow_name}]({run_url}) has composed verses most fair. Applause! π\",\"runFailure\":\"π Alas! [{workflow_name}]({run_url}) {status}. The muse has fled, leaving verses unsung...\"}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
}
- }
- if (createdDiscussions.length > 0) {
- let summaryContent = "\n\n## GitHub Discussions\n";
- for (const discussion of createdDiscussions) {
- const repoLabel = discussion._repo !== defaultTargetRepo ? ` (${discussion._repo})` : "";
- summaryContent += `- Discussion #${discussion.number}${repoLabel}: [${discussion.title}](${discussion.url})\n`;
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
}
- if (closedDiscussionsSummary.length > 0) {
- summaryContent += "\n### Closed Older Discussions\n";
- for (const closed of closedDiscussionsSummary) {
- summaryContent += `- Discussion #${closed.number}: [View](${closed.url}) (marked as outdated)\n`;
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
}
}
- await core.summary.addRaw(summaryContent).write();
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
}
- core.info(`Successfully created ${createdDiscussions.length} discussion(s)`);
}
- await main();
-
- create_issue:
- needs:
- - agent
- - detection
- if: >
- (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_issue'))) &&
- (needs.detection.outputs.success == 'true')
- runs-on: ubuntu-slim
- permissions:
- contents: read
- issues: write
- timeout-minutes: 10
- outputs:
- issue_number: ${{ steps.create_issue.outputs.issue_number }}
- issue_url: ${{ steps.create_issue.outputs.issue_url }}
- temporary_id_map: ${{ steps.create_issue.outputs.temporary_id_map }}
- steps:
- - name: Download agent output artifact
- continue-on-error: true
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
- with:
- name: agent_output.json
- path: /tmp/gh-aw/safeoutputs/
- - name: Setup agent output environment variable
- run: |
- mkdir -p /tmp/gh-aw/safeoutputs/
- find "/tmp/gh-aw/safeoutputs/" -type f -print
- echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
- - name: Create Output Issue
- id: create_issue
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
- GH_AW_ISSUE_TITLE_PREFIX: "[π POEM-BOT] "
- GH_AW_ISSUE_LABELS: "poetry,automation,ai-generated"
- GH_AW_WORKFLOW_NAME: "Poem Bot - A Creative Agentic Workflow"
- GH_AW_ENGINE_ID: "copilot"
- GH_AW_ENGINE_MODEL: "gpt-5"
- GH_AW_SAFE_OUTPUTS_STAGED: "true"
- GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e πͺΆ *Verses penned by [{workflow_name}]({run_url})*\",\"runStarted\":\"π Hear ye! The muse stirs! [{workflow_name}]({run_url}) takes quill in hand for this {event_type}...\",\"runSuccess\":\"πͺΆ The poem is writ! [{workflow_name}]({run_url}) has composed verses most fair. Applause! π\",\"runFailure\":\"π Alas! [{workflow_name}]({run_url}) {status}. The muse has fled, leaving verses unsung...\"}"
- with:
- github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
- script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
@@ -9514,9 +10135,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml
index b1a70bd2b8..2d572c67ce 100644
--- a/.github/workflows/pr-nitpick-reviewer.lock.yml
+++ b/.github/workflows/pr-nitpick-reviewer.lock.yml
@@ -7391,12 +7391,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml
index cd66dabac7..acacc8a6d8 100644
--- a/.github/workflows/prompt-clustering-analysis.lock.yml
+++ b/.github/workflows/prompt-clustering-analysis.lock.yml
@@ -6825,12 +6825,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml
index 89fbcd6c7a..076980e399 100644
--- a/.github/workflows/python-data-charts.lock.yml
+++ b/.github/workflows/python-data-charts.lock.yml
@@ -7395,12 +7395,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml
index a1eca9e26d..3a4dc88b4c 100644
--- a/.github/workflows/q.lock.yml
+++ b/.github/workflows/q.lock.yml
@@ -7690,9 +7690,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml
index d3f374de18..3c05d0d690 100644
--- a/.github/workflows/repo-tree-map.lock.yml
+++ b/.github/workflows/repo-tree-map.lock.yml
@@ -5692,12 +5692,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml
index a526fec9e9..72f85ba248 100644
--- a/.github/workflows/repository-quality-improver.lock.yml
+++ b/.github/workflows/repository-quality-improver.lock.yml
@@ -6606,12 +6606,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml
index b0c7da2aae..6d18d32c83 100644
--- a/.github/workflows/research.lock.yml
+++ b/.github/workflows/research.lock.yml
@@ -5537,12 +5537,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml
index 524f7ab1eb..53167b7aaa 100644
--- a/.github/workflows/safe-output-health.lock.yml
+++ b/.github/workflows/safe-output-health.lock.yml
@@ -5845,12 +5845,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml
index 41b13a10e4..d8bdcded15 100644
--- a/.github/workflows/schema-consistency-checker.lock.yml
+++ b/.github/workflows/schema-consistency-checker.lock.yml
@@ -5624,12 +5624,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml
index 99e0241236..5ef9d5506a 100644
--- a/.github/workflows/security-fix-pr.lock.yml
+++ b/.github/workflows/security-fix-pr.lock.yml
@@ -5146,9 +5146,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml
index ba3e395eae..c7232adb37 100644
--- a/.github/workflows/semantic-function-refactor.lock.yml
+++ b/.github/workflows/semantic-function-refactor.lock.yml
@@ -5952,20 +5952,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml
index a636d73442..5d93ef8269 100644
--- a/.github/workflows/smoke-claude.lock.yml
+++ b/.github/workflows/smoke-claude.lock.yml
@@ -6846,20 +6846,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml
index 1ebd48babc..0a9ca9359e 100644
--- a/.github/workflows/smoke-codex.lock.yml
+++ b/.github/workflows/smoke-codex.lock.yml
@@ -6628,20 +6628,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml
index 8cd0b448b4..8f972dfd0f 100644
--- a/.github/workflows/smoke-copilot-no-firewall.lock.yml
+++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml
@@ -6911,20 +6911,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml
index 0f57ff09a0..1ac61bd66b 100644
--- a/.github/workflows/smoke-copilot-playwright.lock.yml
+++ b/.github/workflows/smoke-copilot-playwright.lock.yml
@@ -8601,20 +8601,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-copilot-safe-inputs.lock.yml b/.github/workflows/smoke-copilot-safe-inputs.lock.yml
index 99f619d57e..d9f03fa95a 100644
--- a/.github/workflows/smoke-copilot-safe-inputs.lock.yml
+++ b/.github/workflows/smoke-copilot-safe-inputs.lock.yml
@@ -8385,20 +8385,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml
index 437ed8bf6b..79640b2fc0 100644
--- a/.github/workflows/smoke-copilot.lock.yml
+++ b/.github/workflows/smoke-copilot.lock.yml
@@ -7092,20 +7092,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml
index 6097692985..3c255d197c 100644
--- a/.github/workflows/smoke-detector.lock.yml
+++ b/.github/workflows/smoke-detector.lock.yml
@@ -6580,20 +6580,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/spec-kit-executor.lock.yml b/.github/workflows/spec-kit-executor.lock.yml
index f74b31b880..67d3c1721d 100644
--- a/.github/workflows/spec-kit-executor.lock.yml
+++ b/.github/workflows/spec-kit-executor.lock.yml
@@ -6086,6 +6086,19 @@ jobs:
}
return "";
}
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
@@ -6279,6 +6292,7 @@ jobs:
if (trackerIDComment) {
bodyLines.push(trackerIDComment);
}
+ addExpirationComment(bodyLines, "GH_AW_PR_EXPIRES", "Pull Request");
bodyLines.push(``, ``, `> AI generated by [${workflowName}](${runUrl})`, "");
const body = bodyLines.join("\n").trim();
const labelsEnv = process.env.GH_AW_PR_LABELS;
diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml
index b3733a59ce..25b1f471ff 100644
--- a/.github/workflows/stale-repo-identifier.lock.yml
+++ b/.github/workflows/stale-repo-identifier.lock.yml
@@ -7263,20 +7263,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml
index a64abe170c..b8dd151abc 100644
--- a/.github/workflows/static-analysis-report.lock.yml
+++ b/.github/workflows/static-analysis-report.lock.yml
@@ -5649,12 +5649,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml
index 80bf15bba0..4eb4df8496 100644
--- a/.github/workflows/super-linter.lock.yml
+++ b/.github/workflows/super-linter.lock.yml
@@ -5844,20 +5844,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml
index dbeb2cb244..407cb473f5 100644
--- a/.github/workflows/technical-doc-writer.lock.yml
+++ b/.github/workflows/technical-doc-writer.lock.yml
@@ -6959,9 +6959,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/test-discussion-expires.lock.yml b/.github/workflows/test-discussion-expires.lock.yml
index a30d9ee0af..8537cec303 100644
--- a/.github/workflows/test-discussion-expires.lock.yml
+++ b/.github/workflows/test-discussion-expires.lock.yml
@@ -5287,12 +5287,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/test-python-safe-input.lock.yml b/.github/workflows/test-python-safe-input.lock.yml
index a0239fb285..200d8f6b62 100644
--- a/.github/workflows/test-python-safe-input.lock.yml
+++ b/.github/workflows/test-python-safe-input.lock.yml
@@ -6886,20 +6886,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml
index 0494d597f3..be54dccba2 100644
--- a/.github/workflows/tidy.lock.yml
+++ b/.github/workflows/tidy.lock.yml
@@ -6070,9 +6070,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml
index db3bb6227b..4df9852a28 100644
--- a/.github/workflows/typist.lock.yml
+++ b/.github/workflows/typist.lock.yml
@@ -5902,12 +5902,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml
index e20384dbd1..fae4a96fab 100644
--- a/.github/workflows/unbloat-docs.lock.yml
+++ b/.github/workflows/unbloat-docs.lock.yml
@@ -6761,9 +6761,132 @@ jobs:
script: |
const fs = require("fs");
const crypto = require("crypto");
- const { updateActivationComment } = require("./update_activation_comment.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ async function updateActivationComment(github, context, core, itemUrl, itemNumber, itemType = "pull_request") {
+ const itemLabel = itemType === "issue" ? "issue" : "pull request";
+ const linkMessage =
+ itemType === "issue"
+ ? `\n\nβ
Issue created: [#${itemNumber}](${itemUrl})`
+ : `\n\nβ
Pull request created: [#${itemNumber}](${itemUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, linkMessage, itemLabel);
+ }
+ async function updateActivationCommentWithCommit(github, context, core, commitSha, commitUrl) {
+ const shortSha = commitSha.substring(0, 7);
+ const message = `\n\nβ
Commit pushed: [\`${shortSha}\`](${commitUrl})`;
+ await updateActivationCommentWithMessage(github, context, core, message, "commit");
+ }
+ async function updateActivationCommentWithMessage(github, context, core, message, label = "") {
+ const commentId = process.env.GH_AW_COMMENT_ID;
+ const commentRepo = process.env.GH_AW_COMMENT_REPO;
+ if (!commentId) {
+ core.info("No activation comment to update (GH_AW_COMMENT_ID not set)");
+ return;
+ }
+ core.info(`Updating activation comment ${commentId}`);
+ let repoOwner = context.repo.owner;
+ let repoName = context.repo.repo;
+ if (commentRepo) {
+ const parts = commentRepo.split("/");
+ if (parts.length === 2) {
+ repoOwner = parts[0];
+ repoName = parts[1];
+ } else {
+ core.warning(`Invalid comment repo format: ${commentRepo}, expected "owner/repo". Falling back to context.repo.`);
+ }
+ }
+ core.info(`Updating comment in ${repoOwner}/${repoName}`);
+ const isDiscussionComment = commentId.startsWith("DC_");
+ try {
+ if (isDiscussionComment) {
+ const currentComment = await github.graphql(
+ `
+ query($commentId: ID!) {
+ node(id: $commentId) {
+ ... on DiscussionComment {
+ body
+ }
+ }
+ }`,
+ { commentId: commentId }
+ );
+ if (!currentComment?.node?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted or is inaccessible");
+ return;
+ }
+ const currentBody = currentComment.node.body;
+ const updatedBody = currentBody + message;
+ const result = await github.graphql(
+ `
+ mutation($commentId: ID!, $body: String!) {
+ updateDiscussionComment(input: { commentId: $commentId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { commentId: commentId, body: updatedBody }
+ );
+ const comment = result.updateDiscussionComment.comment;
+ const successMessage = label
+ ? `Successfully updated discussion comment with ${label} link`
+ : "Successfully updated discussion comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${comment.id}`);
+ core.info(`Comment URL: ${comment.url}`);
+ } else {
+ const currentComment = await github.request("GET /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ if (!currentComment?.data?.body) {
+ core.warning("Unable to fetch current comment body, comment may have been deleted");
+ return;
+ }
+ const currentBody = currentComment.data.body;
+ const updatedBody = currentBody + message;
+ const response = await github.request("PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", {
+ owner: repoOwner,
+ repo: repoName,
+ comment_id: parseInt(commentId, 10),
+ body: updatedBody,
+ headers: {
+ Accept: "application/vnd.github+json",
+ },
+ });
+ const successMessage = label ? `Successfully updated comment with ${label} link` : "Successfully updated comment";
+ core.info(successMessage);
+ core.info(`Comment ID: ${response.data.id}`);
+ core.info(`Comment URL: ${response.data.html_url}`);
+ }
+ } catch (error) {
+ core.warning(`Failed to update activation comment: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
function generatePatchPreview(patchContent) {
if (!patchContent || !patchContent.trim()) {
return "";
diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml
index d54967c5c5..da2c84fbd1 100644
--- a/.github/workflows/video-analyzer.lock.yml
+++ b/.github/workflows/video-analyzer.lock.yml
@@ -5881,20 +5881,272 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { sanitizeLabelContent } = require("./sanitize_label_content.cjs");
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { generateStagedPreview } = require("./staged_preview.cjs");
- const { generateFooter } = require("./generate_footer.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const {
- generateTemporaryId,
- isTemporaryId,
- normalizeTemporaryId,
- replaceTemporaryIdReferences,
- serializeTemporaryIdMap,
- } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ function sanitizeLabelContent(content) {
+ if (!content || typeof content !== "string") {
+ return "";
+ }
+ let sanitized = content.trim();
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, "");
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
+ sanitized = sanitized.replace(
+ /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g,
+ (_m, p1, p2) => `${p1}\`@${p2}\``
+ );
+ sanitized = sanitized.replace(/[<>&'"]/g, "");
+ return sanitized.trim();
+ }
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ async function generateStagedPreview(options) {
+ const { title, description, items, renderItem } = options;
+ let summaryContent = `## π Staged Mode: ${title} Preview\n\n`;
+ summaryContent += `${description}\n\n`;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ summaryContent += renderItem(item, i);
+ summaryContent += "---\n\n";
+ }
+ try {
+ await core.summary.addRaw(summaryContent).write();
+ core.info(summaryContent);
+ core.info(`π ${title} preview written to step summary`);
+ } catch (error) {
+ core.setFailed(error instanceof Error ? error : String(error));
+ }
+ }
+ function generateXMLMarker(workflowName, runUrl) {
+ const engineId = process.env.GH_AW_ENGINE_ID || "";
+ const engineVersion = process.env.GH_AW_ENGINE_VERSION || "";
+ const engineModel = process.env.GH_AW_ENGINE_MODEL || "";
+ const trackerId = process.env.GH_AW_TRACKER_ID || "";
+ const parts = [];
+ parts.push(`agentic-workflow: ${workflowName}`);
+ if (trackerId) {
+ parts.push(`tracker-id: ${trackerId}`);
+ }
+ if (engineId) {
+ parts.push(`engine: ${engineId}`);
+ }
+ if (engineVersion) {
+ parts.push(`version: ${engineVersion}`);
+ }
+ if (engineModel) {
+ parts.push(`model: ${engineModel}`);
+ }
+ parts.push(`run: ${runUrl}`);
+ return ``;
+ }
+ function generateFooter(
+ workflowName,
+ runUrl,
+ workflowSource,
+ workflowSourceURL,
+ triggeringIssueNumber,
+ triggeringPRNumber,
+ triggeringDiscussionNumber
+ ) {
+ let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`;
+ if (triggeringIssueNumber) {
+ footer += ` for #${triggeringIssueNumber}`;
+ } else if (triggeringPRNumber) {
+ footer += ` for #${triggeringPRNumber}`;
+ } else if (triggeringDiscussionNumber) {
+ footer += ` for discussion #${triggeringDiscussionNumber}`;
+ }
+ if (workflowSource && workflowSourceURL) {
+ footer += `\n>\n> To add this workflow in your repository, run \`gh aw add ${workflowSource}\`. See [usage guide](https://githubnext.github.io/gh-aw/tools/cli/).`;
+ }
+ footer += "\n\n" + generateXMLMarker(workflowName, runUrl);
+ footer += "\n";
+ return footer;
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function main() {
core.setOutput("issue_number", "");
core.setOutput("issue_url", "");
diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml
index c3bc1d5e52..36c211bb4b 100644
--- a/.github/workflows/weekly-issue-summary.lock.yml
+++ b/.github/workflows/weekly-issue-summary.lock.yml
@@ -6630,12 +6630,381 @@ jobs:
with:
github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
- const { loadAgentOutput } = require("./load_agent_output.cjs");
- const { getTrackerID } = require("./get_tracker_id.cjs");
- const { closeOlderDiscussions } = require("./close_older_discussions.cjs");
- const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs");
- const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs");
- const { addExpirationComment } = require("./expiration_helpers.cjs");
+ const fs = require("fs");
+ const crypto = require("crypto");
+ const MAX_LOG_CONTENT_LENGTH = 10000;
+ function truncateForLogging(content) {
+ if (content.length <= MAX_LOG_CONTENT_LENGTH) {
+ return content;
+ }
+ return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`;
+ }
+ function loadAgentOutput() {
+ const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT;
+ if (!agentOutputFile) {
+ core.info("No GH_AW_AGENT_OUTPUT environment variable found");
+ return { success: false };
+ }
+ let outputContent;
+ try {
+ outputContent = fs.readFileSync(agentOutputFile, "utf8");
+ } catch (error) {
+ const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ return { success: false, error: errorMessage };
+ }
+ if (outputContent.trim() === "") {
+ core.info("Agent output content is empty");
+ return { success: false };
+ }
+ core.info(`Agent output content length: ${outputContent.length}`);
+ let validatedOutput;
+ try {
+ validatedOutput = JSON.parse(outputContent);
+ } catch (error) {
+ const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`;
+ core.error(errorMessage);
+ core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`);
+ return { success: false, error: errorMessage };
+ }
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No valid items found in agent output");
+ core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`);
+ return { success: false };
+ }
+ return { success: true, items: validatedOutput.items };
+ }
+ function getTrackerID(format) {
+ const trackerID = process.env.GH_AW_TRACKER_ID || "";
+ if (trackerID) {
+ core.info(`Tracker ID: ${trackerID}`);
+ return format === "markdown" ? `\n\n` : trackerID;
+ }
+ return "";
+ }
+ function getMessages() {
+ const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ if (!messagesEnv) {
+ return null;
+ }
+ try {
+ return JSON.parse(messagesEnv);
+ } catch (error) {
+ core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`);
+ return null;
+ }
+ }
+ function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+ }
+ function toSnakeCase(obj) {
+ const result = {};
+ for (const [key, value] of Object.entries(obj)) {
+ const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
+ result[snakeKey] = value;
+ result[key] = value;
+ }
+ return result;
+ }
+ function getCloseOlderDiscussionMessage(ctx) {
+ const messages = getMessages();
+ const templateContext = toSnakeCase(ctx);
+ const defaultMessage = `β Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}).
+ πΊοΈ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**.
+ Fair winds, matey! π΄ββ οΈ`;
+ return messages?.closeOlderDiscussion
+ ? renderTemplate(messages.closeOlderDiscussion, templateContext)
+ : renderTemplate(defaultMessage, templateContext);
+ }
+ const MAX_CLOSE_COUNT = 10;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+ async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) {
+ let searchQuery = `repo:${owner}/${repo} is:open`;
+ if (titlePrefix) {
+ const escapedPrefix = titlePrefix.replace(/"/g, '\\"');
+ searchQuery += ` in:title "${escapedPrefix}"`;
+ }
+ if (labels && labels.length > 0) {
+ for (const label of labels) {
+ const escapedLabel = label.replace(/"/g, '\\"');
+ searchQuery += ` label:"${escapedLabel}"`;
+ }
+ }
+ const result = await github.graphql(
+ `
+ query($searchTerms: String!, $first: Int!) {
+ search(query: $searchTerms, type: DISCUSSION, first: $first) {
+ nodes {
+ ... on Discussion {
+ id
+ number
+ title
+ url
+ category {
+ id
+ }
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ closed
+ }
+ }
+ }
+ }`,
+ { searchTerms: searchQuery, first: 50 }
+ );
+ if (!result || !result.search || !result.search.nodes) {
+ return [];
+ }
+ return result.search.nodes
+ .filter(
+ d => {
+ if (!d || d.number === excludeNumber || d.closed) {
+ return false;
+ }
+ if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) {
+ return false;
+ }
+ if (labels && labels.length > 0) {
+ const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || [];
+ const hasAllLabels = labels.every(label => discussionLabels.includes(label));
+ if (!hasAllLabels) {
+ return false;
+ }
+ }
+ if (categoryId && (!d.category || d.category.id !== categoryId)) {
+ return false;
+ }
+ return true;
+ }
+ )
+ .map(
+ d => ({
+ id: d.id,
+ number: d.number,
+ title: d.title,
+ url: d.url,
+ })
+ );
+ }
+ async function addDiscussionComment(github, discussionId, message) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!, $body: String!) {
+ addDiscussionComment(input: { discussionId: $dId, body: $body }) {
+ comment {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId, body: message }
+ );
+ return result.addDiscussionComment.comment;
+ }
+ async function closeDiscussionAsOutdated(github, discussionId) {
+ const result = await github.graphql(
+ `
+ mutation($dId: ID!) {
+ closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) {
+ discussion {
+ id
+ url
+ }
+ }
+ }`,
+ { dId: discussionId }
+ );
+ return result.closeDiscussion.discussion;
+ }
+ async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) {
+ const searchCriteria = [];
+ if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`);
+ if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`);
+ core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`);
+ const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number);
+ if (olderDiscussions.length === 0) {
+ core.info("No older discussions found to close");
+ return [];
+ }
+ core.info(`Found ${olderDiscussions.length} older discussion(s) to close`);
+ const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT);
+ if (olderDiscussions.length > MAX_CLOSE_COUNT) {
+ core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`);
+ }
+ const closedDiscussions = [];
+ for (let i = 0; i < discussionsToClose.length; i++) {
+ const discussion = discussionsToClose[i];
+ try {
+ const closingMessage = getCloseOlderDiscussionMessage({
+ newDiscussionUrl: newDiscussion.url,
+ newDiscussionNumber: newDiscussion.number,
+ workflowName,
+ runUrl,
+ });
+ core.info(`Adding closing comment to discussion #${discussion.number}`);
+ await addDiscussionComment(github, discussion.id, closingMessage);
+ core.info(`Closing discussion #${discussion.number} as outdated`);
+ await closeDiscussionAsOutdated(github, discussion.id);
+ closedDiscussions.push({
+ number: discussion.number,
+ url: discussion.url,
+ });
+ core.info(`β Closed discussion #${discussion.number}: ${discussion.url}`);
+ } catch (error) {
+ core.error(`β Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ return closedDiscussions;
+ }
+ const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi;
+ function generateTemporaryId() {
+ return "aw_" + crypto.randomBytes(6).toString("hex");
+ }
+ function isTemporaryId(value) {
+ if (typeof value === "string") {
+ return /^aw_[0-9a-f]{12}$/i.test(value);
+ }
+ return false;
+ }
+ function normalizeTemporaryId(tempId) {
+ return String(tempId).toLowerCase();
+ }
+ function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const resolved = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (resolved !== undefined) {
+ if (currentRepo && resolved.repo === currentRepo) {
+ return `#${resolved.number}`;
+ }
+ return `${resolved.repo}#${resolved.number}`;
+ }
+ return match;
+ });
+ }
+ function replaceTemporaryIdReferencesLegacy(text, tempIdMap) {
+ return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
+ const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId));
+ if (issueNumber !== undefined) {
+ return `#${issueNumber}`;
+ }
+ return match;
+ });
+ }
+ function loadTemporaryIdMap() {
+ const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP;
+ if (!mapJson || mapJson === "{}") {
+ return new Map();
+ }
+ try {
+ const mapObject = JSON.parse(mapJson);
+ const result = new Map();
+ for (const [key, value] of Object.entries(mapObject)) {
+ const normalizedKey = normalizeTemporaryId(key);
+ if (typeof value === "number") {
+ const contextRepo = `${context.repo.owner}/${context.repo.repo}`;
+ result.set(normalizedKey, { repo: contextRepo, number: value });
+ } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) {
+ result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) });
+ }
+ }
+ return result;
+ } catch (error) {
+ if (typeof core !== "undefined") {
+ core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return new Map();
+ }
+ }
+ function resolveIssueNumber(value, temporaryIdMap) {
+ if (value === undefined || value === null) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" };
+ }
+ const valueStr = String(value);
+ if (isTemporaryId(valueStr)) {
+ const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr));
+ if (resolvedPair !== undefined) {
+ return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null };
+ }
+ return {
+ resolved: null,
+ wasTemporaryId: true,
+ errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`,
+ };
+ }
+ const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10);
+ if (isNaN(issueNumber) || issueNumber <= 0) {
+ return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` };
+ }
+ const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : "";
+ return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null };
+ }
+ function serializeTemporaryIdMap(tempIdMap) {
+ const obj = Object.fromEntries(tempIdMap);
+ return JSON.stringify(obj);
+ }
+ function parseAllowedRepos() {
+ const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS;
+ const set = new Set();
+ if (allowedReposEnv) {
+ allowedReposEnv
+ .split(",")
+ .map(repo => repo.trim())
+ .filter(repo => repo)
+ .forEach(repo => set.add(repo));
+ }
+ return set;
+ }
+ function getDefaultTargetRepo() {
+ const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG;
+ if (targetRepoSlug) {
+ return targetRepoSlug;
+ }
+ return `${context.repo.owner}/${context.repo.repo}`;
+ }
+ function validateRepo(repo, defaultRepo, allowedRepos) {
+ if (repo === defaultRepo) {
+ return { valid: true, error: null };
+ }
+ if (allowedRepos.has(repo)) {
+ return { valid: true, error: null };
+ }
+ return {
+ valid: false,
+ error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
+ };
+ }
+ function parseRepoSlug(repoSlug) {
+ const parts = repoSlug.split("/");
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
+ return null;
+ }
+ return { owner: parts[0], repo: parts[1] };
+ }
+ function addExpirationComment(bodyLines, envVarName, entityType) {
+ const expiresEnv = process.env[envVarName];
+ if (expiresEnv) {
+ const expiresDays = parseInt(expiresEnv, 10);
+ if (!isNaN(expiresDays) && expiresDays > 0) {
+ const expirationDate = new Date();
+ expirationDate.setDate(expirationDate.getDate() + expiresDays);
+ const expirationISO = expirationDate.toISOString();
+ bodyLines.push(``);
+ core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`);
+ }
+ }
+ }
async function fetchRepoDiscussionInfo(owner, repo) {
const repositoryQuery = `
query($owner: String!, $repo: String!) {
diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go
index 49616c4e73..e64b5164a7 100644
--- a/pkg/workflow/js.go
+++ b/pkg/workflow/js.go
@@ -171,6 +171,9 @@ var messagesCloseDiscussionScript string
//go:embed js/close_older_discussions.cjs
var closeOlderDiscussionsScript string
+//go:embed js/expiration_helpers.cjs
+var expirationHelpersScript string
+
//go:embed js/get_repository_url.cjs
var getRepositoryUrlScript string
@@ -256,59 +259,60 @@ var safeOutputsToolsLoaderScript string
// The keys are the relative paths from the js directory
func GetJavaScriptSources() map[string]string {
return map[string]string{
- "sanitize_content.cjs": sanitizeContentScript,
- "sanitize_label_content.cjs": sanitizeLabelContentScript,
- "sanitize_workflow_name.cjs": sanitizeWorkflowNameScript,
- "load_agent_output.cjs": loadAgentOutputScript,
- "staged_preview.cjs": stagedPreviewScript,
- "assign_agent_helpers.cjs": assignAgentHelpersScript,
- "safe_output_helpers.cjs": safeOutputHelpersScript,
- "safe_output_validator.cjs": safeOutputValidatorScript,
- "safe_output_processor.cjs": safeOutputProcessorScript,
- "temporary_id.cjs": temporaryIdScript,
- "is_truthy.cjs": isTruthyScript,
- "log_parser_bootstrap.cjs": logParserBootstrapScript,
- "log_parser_shared.cjs": logParserSharedScript,
- "update_activation_comment.cjs": updateActivationCommentScript,
- "generate_footer.cjs": generateFooterScript,
- "get_tracker_id.cjs": getTrackerIDScript,
- "messages.cjs": messagesScript,
- "messages_core.cjs": messagesCoreScript,
- "messages_footer.cjs": messagesFooterScript,
- "messages_staged.cjs": messagesStagedScript,
- "messages_run_status.cjs": messagesRunStatusScript,
- "messages_close_discussion.cjs": messagesCloseDiscussionScript,
- "close_older_discussions.cjs": closeOlderDiscussionsScript,
- "get_repository_url.cjs": getRepositoryUrlScript,
- "check_permissions_utils.cjs": checkPermissionsUtilsScript,
- "normalize_branch_name.cjs": normalizeBranchNameScript,
- "estimate_tokens.cjs": estimateTokensScript,
- "generate_compact_schema.cjs": generateCompactSchemaScript,
- "write_large_content_to_file.cjs": writeLargeContentToFileScript,
- "get_current_branch.cjs": getCurrentBranchScript,
- "get_base_branch.cjs": getBaseBranchScript,
- "generate_git_patch.cjs": generateGitPatchJSScript,
- "update_runner.cjs": updateRunnerScript,
+ "sanitize_content.cjs": sanitizeContentScript,
+ "sanitize_label_content.cjs": sanitizeLabelContentScript,
+ "sanitize_workflow_name.cjs": sanitizeWorkflowNameScript,
+ "load_agent_output.cjs": loadAgentOutputScript,
+ "staged_preview.cjs": stagedPreviewScript,
+ "assign_agent_helpers.cjs": assignAgentHelpersScript,
+ "safe_output_helpers.cjs": safeOutputHelpersScript,
+ "safe_output_validator.cjs": safeOutputValidatorScript,
+ "safe_output_processor.cjs": safeOutputProcessorScript,
+ "temporary_id.cjs": temporaryIdScript,
+ "is_truthy.cjs": isTruthyScript,
+ "log_parser_bootstrap.cjs": logParserBootstrapScript,
+ "log_parser_shared.cjs": logParserSharedScript,
+ "update_activation_comment.cjs": updateActivationCommentScript,
+ "generate_footer.cjs": generateFooterScript,
+ "get_tracker_id.cjs": getTrackerIDScript,
+ "messages.cjs": messagesScript,
+ "messages_core.cjs": messagesCoreScript,
+ "messages_footer.cjs": messagesFooterScript,
+ "messages_staged.cjs": messagesStagedScript,
+ "messages_run_status.cjs": messagesRunStatusScript,
+ "messages_close_discussion.cjs": messagesCloseDiscussionScript,
+ "close_older_discussions.cjs": closeOlderDiscussionsScript,
+ "expiration_helpers.cjs": expirationHelpersScript,
+ "get_repository_url.cjs": getRepositoryUrlScript,
+ "check_permissions_utils.cjs": checkPermissionsUtilsScript,
+ "normalize_branch_name.cjs": normalizeBranchNameScript,
+ "estimate_tokens.cjs": estimateTokensScript,
+ "generate_compact_schema.cjs": generateCompactSchemaScript,
+ "write_large_content_to_file.cjs": writeLargeContentToFileScript,
+ "get_current_branch.cjs": getCurrentBranchScript,
+ "get_base_branch.cjs": getBaseBranchScript,
+ "generate_git_patch.cjs": generateGitPatchJSScript,
+ "update_runner.cjs": updateRunnerScript,
"update_pr_description_helpers.cjs": updatePRDescriptionHelpersScript,
- "read_buffer.cjs": readBufferScript,
- "mcp_server_core.cjs": mcpServerCoreScript,
- "mcp_server.cjs": mcpServerScriptSource,
- "mcp_http_transport.cjs": mcpHTTPTransportScriptSource,
- "mcp_logger.cjs": mcpLoggerScriptSource,
- "safe_inputs_mcp_server.cjs": safeInputsMCPServerScript,
- "safe_inputs_mcp_server_http.cjs": safeInputsMCPServerHTTPScript,
- "safe_inputs_config_loader.cjs": safeInputsConfigLoaderScript,
- "safe_inputs_tool_factory.cjs": safeInputsToolFactoryScript,
- "safe_inputs_validation.cjs": safeInputsValidationScript,
- "mcp_handler_shell.cjs": mcpHandlerShellScript,
- "mcp_handler_python.cjs": mcpHandlerPythonScript,
- "safe_output_type_validator.cjs": safeOutputTypeValidatorScript,
- "repo_helpers.cjs": repoHelpersScript,
- "safe_outputs_config.cjs": safeOutputsConfigScript,
- "safe_outputs_append.cjs": safeOutputsAppendScript,
- "safe_outputs_handlers.cjs": safeOutputsHandlersScript,
- "safe_outputs_tools_loader.cjs": safeOutputsToolsLoaderScript,
- "safe_outputs_mcp_server.cjs": safeOutputsMCPServerScriptSource,
+ "read_buffer.cjs": readBufferScript,
+ "mcp_server_core.cjs": mcpServerCoreScript,
+ "mcp_server.cjs": mcpServerScriptSource,
+ "mcp_http_transport.cjs": mcpHTTPTransportScriptSource,
+ "mcp_logger.cjs": mcpLoggerScriptSource,
+ "safe_inputs_mcp_server.cjs": safeInputsMCPServerScript,
+ "safe_inputs_mcp_server_http.cjs": safeInputsMCPServerHTTPScript,
+ "safe_inputs_config_loader.cjs": safeInputsConfigLoaderScript,
+ "safe_inputs_tool_factory.cjs": safeInputsToolFactoryScript,
+ "safe_inputs_validation.cjs": safeInputsValidationScript,
+ "mcp_handler_shell.cjs": mcpHandlerShellScript,
+ "mcp_handler_python.cjs": mcpHandlerPythonScript,
+ "safe_output_type_validator.cjs": safeOutputTypeValidatorScript,
+ "repo_helpers.cjs": repoHelpersScript,
+ "safe_outputs_config.cjs": safeOutputsConfigScript,
+ "safe_outputs_append.cjs": safeOutputsAppendScript,
+ "safe_outputs_handlers.cjs": safeOutputsHandlersScript,
+ "safe_outputs_tools_loader.cjs": safeOutputsToolsLoaderScript,
+ "safe_outputs_mcp_server.cjs": safeOutputsMCPServerScriptSource,
}
}
diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go
index 0c9d4eddb9..86d57e164f 100644
--- a/pkg/workflow/maintenance_workflow.go
+++ b/pkg/workflow/maintenance_workflow.go
@@ -35,7 +35,8 @@ func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir s
maintenanceLog.Print("Generating maintenance workflow for expired discussions")
// Create the maintenance workflow content
- content := `name: Agentics Maintenance
+ script := getMaintenanceScript()
+ content := fmt.Sprintf(`name: Agentics Maintenance
on:
schedule:
@@ -54,8 +55,8 @@ jobs:
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
- ${getMaintenanceScript()}
-`
+ %s
+`, script)
// Write the maintenance workflow file
maintenanceFile := filepath.Join(workflowDir, "agentics-maintenance.yml")