diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index 978c48c15f..bcebc75c79 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -17,286 +17,179 @@ jobs:
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
- // @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
+ const MAX_UPDATES_PER_RUN = 100;
+ const GRAPHQL_DELAY_MS = 500;
+ function delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
}
- nodes {
- id
- number
- title
- url
- body
- createdAt
+ 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 || [];
+ for (const discussion of nodes) {
+ const agenticPattern = /^> AI generated by/m;
+ const isAgenticWorkflow = discussion.body && agenticPattern.test(discussion.body);
+ if (!isAgenticWorkflow) {
+ continue;
+ }
+ 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;
}
- }
- }
- }
- `;
-
- 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();
-
+ function extractExpirationDate(body) {
+ const expirationPattern = //;
+ const match = body.match(expirationPattern);
+ if (!match) {
+ return null;
+ }
+ const expirationISO = match[1].trim();
+ const expirationDate = new Date(expirationISO);
+ if (isNaN(expirationDate.getTime())) {
+ return null;
+ }
+ return expirationDate;
+ }
+ function validateCreationDate(createdAt) {
+ const creationDate = new Date(createdAt);
+ return !isNaN(creationDate.getTime());
+ }
+ 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 main() {
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ core.info(`Searching for expired discussions in ${owner}/${repo}`);
+ 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`);
+ const now = new Date();
+ const expiredDiscussions = [];
+ for (const discussion of discussionsWithExpiration) {
+ if (!validateCreationDate(discussion.createdAt)) {
+ core.warning(`Discussion #${discussion.number} has invalid creation date, skipping`);
+ continue;
+ }
+ const expirationDate = extractExpirationDate(discussion.body);
+ if (!expirationDate) {
+ core.warning(`Discussion #${discussion.number} has invalid expiration date, skipping`);
+ continue;
+ }
+ 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)`);
+ 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()}.`;
+ 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,
+ 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)}`);
+ }
+ if (i < discussionsToClose.length - 1) {
+ await delay(GRAPHQL_DELAY_MS);
+ }
+ }
+ 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/release.lock.yml b/.github/workflows/release.lock.yml
index 160a26701e..a73e060474 100644
--- a/.github/workflows/release.lock.yml
+++ b/.github/workflows/release.lock.yml
@@ -6119,19 +6119,19 @@ jobs:
- name: Download Go modules
run: go mod download
- name: Generate SBOM (SPDX format)
- uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0
+ uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0.20.10
with:
artifact-name: sbom.spdx.json
format: spdx-json
output-file: sbom.spdx.json
- name: Generate SBOM (CycloneDX format)
- uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0
+ uses: anchore/sbom-action@fbfd9c6c189226748411491745178e0c2017392d # v0.20.10
with:
artifact-name: sbom.cdx.json
format: cyclonedx-json
output-file: sbom.cdx.json
- name: Upload SBOM artifacts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: sbom-artifacts
path: |
diff --git a/pkg/workflow/js/close_expired_discussions.cjs b/pkg/workflow/js/close_expired_discussions.cjs
index 7af39b7a6c..8cf285b39d 100644
--- a/pkg/workflow/js/close_expired_discussions.cjs
+++ b/pkg/workflow/js/close_expired_discussions.cjs
@@ -1,5 +1,5 @@
// @ts-check
-///
+//
/**
* Maximum number of discussions to update per run
diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go
index 86d57e164f..eb1cc910a2 100644
--- a/pkg/workflow/maintenance_workflow.go
+++ b/pkg/workflow/maintenance_workflow.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"github.com/githubnext/gh-aw/pkg/logger"
)
@@ -34,9 +35,10 @@ func GenerateMaintenanceWorkflow(workflowDataList []*WorkflowData, workflowDir s
maintenanceLog.Print("Generating maintenance workflow for expired discussions")
- // Create the maintenance workflow content
- script := getMaintenanceScript()
- content := fmt.Sprintf(`name: Agentics Maintenance
+ // Create the maintenance workflow content using strings.Builder
+ var yaml strings.Builder
+
+ yaml.WriteString(`name: Agentics Maintenance
on:
schedule:
@@ -55,8 +57,13 @@ jobs:
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
- %s
-`, script)
+`)
+
+ // Add the JavaScript script with proper indentation
+ script := getMaintenanceScript()
+ WriteJavaScriptToYAML(&yaml, script)
+
+ content := yaml.String()
// Write the maintenance workflow file
maintenanceFile := filepath.Join(workflowDir, "agentics-maintenance.yml")