diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 89e6705a8d..3c03dae2e9 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -95,9 +95,13 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + issues: write steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Setup Go uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 @@ -113,17 +117,19 @@ jobs: ./gh-aw compile --validate --verbose echo "✓ All workflows compiled successfully" - - name: Check for out-of-sync workflows - run: | - if git diff --exit-code .github/workflows/*.lock.yml; then - echo "✓ All workflow lock files are up to date" - else - echo "::error::Some workflow lock files are out of sync. Run 'make recompile' locally." - echo "::group::Diff of out-of-sync files" - git diff .github/workflows/*.lock.yml - echo "::endgroup::" - exit 1 - fi + - name: Setup Scripts + uses: ./actions/setup + with: + destination: /tmp/gh-aw/actions + + - name: Check for out-of-sync workflows and create issue if needed + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/tmp/gh-aw/actions/check_workflow_recompile_needed.cjs'); + await main(); zizmor-scan: runs-on: ubuntu-latest diff --git a/actions/setup/js/check_workflow_recompile_needed.cjs b/actions/setup/js/check_workflow_recompile_needed.cjs new file mode 100644 index 0000000000..678d389c8c --- /dev/null +++ b/actions/setup/js/check_workflow_recompile_needed.cjs @@ -0,0 +1,181 @@ +// @ts-check +/// + +const { getErrorMessage } = require("./error_helpers.cjs"); + +/** + * Check if workflows need recompilation and create an issue if needed. + * This script: + * 1. Checks if there are out-of-sync workflow lock files + * 2. Searches for existing open issues about recompiling workflows + * 3. If workflows are out of sync and no issue exists, creates a new issue with agentic instructions + * + * @returns {Promise} + */ +async function main() { + const owner = context.repo.owner; + const repo = context.repo.repo; + + core.info("Checking for out-of-sync workflow lock files"); + + // Execute git diff to check for changes in lock files + let diffOutput = ""; + let hasChanges = false; + + try { + // Run git diff to check if there are any changes in lock files + await exec.exec("git", ["diff", "--exit-code", ".github/workflows/*.lock.yml"], { + ignoreReturnCode: true, + listeners: { + stdout: data => { + diffOutput += data.toString(); + }, + stderr: data => { + diffOutput += data.toString(); + }, + }, + }); + + // If git diff exits with code 0, there are no changes + // If it exits with code 1, there are changes + // We need to check if there's actual diff output + hasChanges = diffOutput.trim().length > 0; + } catch (error) { + core.error(`Failed to check for workflow changes: ${getErrorMessage(error)}`); + throw error; + } + + if (!hasChanges) { + core.info("✓ All workflow lock files are up to date"); + return; + } + + core.info("⚠ Detected out-of-sync workflow lock files"); + + // Capture the actual diff for the issue body + let detailedDiff = ""; + try { + await exec.exec("git", ["diff", ".github/workflows/*.lock.yml"], { + listeners: { + stdout: data => { + detailedDiff += data.toString(); + }, + }, + }); + } catch (error) { + core.warning(`Could not capture detailed diff: ${getErrorMessage(error)}`); + } + + // Search for existing open issue about workflow recompilation + const issueTitle = "Workflows need recompilation"; + const searchQuery = `repo:${owner}/${repo} is:issue is:open in:title "${issueTitle}"`; + + core.info(`Searching for existing issue with title: "${issueTitle}"`); + + try { + const searchResult = await github.rest.search.issuesAndPullRequests({ + q: searchQuery, + per_page: 1, + }); + + if (searchResult.data.total_count > 0) { + const existingIssue = searchResult.data.items[0]; + core.info(`Found existing issue #${existingIssue.number}: ${existingIssue.html_url}`); + core.info("Skipping issue creation (avoiding duplicate)"); + + // Add a comment to the existing issue with the new workflow run info + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${context.runId}` : `${githubServer}/${owner}/${repo}/actions/runs/${context.runId}`; + const commentBody = `Workflows are still out of sync as of ${new Date().toISOString()}.\n\nSee [workflow run](${runUrl}) for details.`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: existingIssue.number, + body: commentBody, + }); + + core.info(`✓ Added comment to existing issue #${existingIssue.number}`); + return; + } + } catch (error) { + core.error(`Failed to search for existing issues: ${getErrorMessage(error)}`); + throw error; + } + + // No existing issue found, create a new one + core.info("No existing issue found, creating a new issue with agentic instructions"); + + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${context.runId}` : `${githubServer}/${owner}/${repo}/actions/runs/${context.runId}`; + + // Build the issue body with agentic instructions + const issueBody = `## Problem + +The workflow lock files (\`.lock.yml\`) are out of sync with their source markdown files (\`.md\`). This means the workflows that run in GitHub Actions are not using the latest configuration. + +## What needs to be done + +The workflows need to be recompiled to regenerate the lock files from the markdown sources. + +## Instructions for GitHub Copilot + +Please recompile all workflows by running the following command: + +\`\`\`bash +make recompile +\`\`\` + +This will: +1. Build the latest version of \`gh-aw\` +2. Compile all workflow markdown files to YAML lock files +3. Ensure all workflows are up to date + +After recompiling, commit the changes with a message like: +\`\`\` +Recompile workflows to update lock files +\`\`\` + +## Detected Changes + +The following workflow lock files have changes: + +
+View diff + +\`\`\`diff +${detailedDiff.substring(0, 50000)}${detailedDiff.length > 50000 ? "\n\n... (diff truncated)" : ""} +\`\`\` + +
+ +## References + +- **Failed Check:** [Workflow Run](${runUrl}) +- **Repository:** ${owner}/${repo} + +--- + +> This issue was automatically created by the agentics maintenance workflow. +`; + + try { + const newIssue = await github.rest.issues.create({ + owner, + repo, + title: issueTitle, + body: issueBody, + labels: ["maintenance", "workflows"], + }); + + core.info(`✓ Created issue #${newIssue.data.number}: ${newIssue.data.html_url}`); + + // Write to job summary + await core.summary.addHeading("Workflow Recompilation Needed", 2).addRaw(`Created issue [#${newIssue.data.number}](${newIssue.data.html_url}) to track workflow recompilation.`).write(); + } catch (error) { + core.error(`Failed to create issue: ${getErrorMessage(error)}`); + throw error; + } +} + +module.exports = { main }; diff --git a/actions/setup/js/check_workflow_recompile_needed.test.cjs b/actions/setup/js/check_workflow_recompile_needed.test.cjs new file mode 100644 index 0000000000..24ac927b54 --- /dev/null +++ b/actions/setup/js/check_workflow_recompile_needed.test.cjs @@ -0,0 +1,187 @@ +// @ts-check +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +describe("check_workflow_recompile_needed", () => { + let mockCore; + let mockGithub; + let mockContext; + let mockExec; + let originalGlobals; + + beforeEach(() => { + // Save original globals + originalGlobals = { + core: global.core, + github: global.github, + context: global.context, + exec: global.exec, + }; + + // Setup mock core module + mockCore = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + summary: { + addHeading: vi.fn().mockReturnThis(), + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }, + }; + + // Setup mock github module + mockGithub = { + rest: { + search: { + issuesAndPullRequests: vi.fn(), + }, + issues: { + create: vi.fn(), + createComment: vi.fn(), + }, + }, + }; + + // Setup mock context + mockContext = { + repo: { + owner: "testowner", + repo: "testrepo", + }, + runId: 123456, + payload: { + repository: { + html_url: "https://github.com/testowner/testrepo", + }, + }, + }; + + // Setup mock exec module + mockExec = { + exec: vi.fn(), + }; + + // Set globals for the module + global.core = mockCore; + global.github = mockGithub; + global.context = mockContext; + global.exec = mockExec; + }); + + afterEach(() => { + // Restore original globals + global.core = originalGlobals.core; + global.github = originalGlobals.github; + global.context = originalGlobals.context; + global.exec = originalGlobals.exec; + }); + + it("should report no changes when workflows are up to date", async () => { + // Mock exec to return no changes (empty diff output) + mockExec.exec.mockResolvedValue(0); + + const { main } = await import("./check_workflow_recompile_needed.cjs"); + await main(); + + expect(mockCore.info).toHaveBeenCalledWith("✓ All workflow lock files are up to date"); + expect(mockGithub.rest.search.issuesAndPullRequests).not.toHaveBeenCalled(); + }); + + it("should add comment to existing issue when workflows are out of sync", async () => { + // Mock exec to return changes (non-empty diff output) + mockExec.exec + .mockImplementationOnce(async (cmd, args, options) => { + if (options?.listeners?.stdout) { + options.listeners.stdout(Buffer.from("diff content")); + } + return 1; // Non-zero exit code indicates changes + }) + .mockImplementationOnce(async (cmd, args, options) => { + if (options?.listeners?.stdout) { + options.listeners.stdout(Buffer.from("detailed diff content")); + } + return 0; + }); + + // Mock search to return existing issue + mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({ + data: { + total_count: 1, + items: [ + { + number: 42, + html_url: "https://github.com/testowner/testrepo/issues/42", + }, + ], + }, + }); + + mockGithub.rest.issues.createComment.mockResolvedValue({}); + + const { main } = await import("./check_workflow_recompile_needed.cjs"); + await main(); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Found existing issue")); + expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith({ + owner: "testowner", + repo: "testrepo", + issue_number: 42, + body: expect.stringContaining("Workflows are still out of sync"), + }); + expect(mockGithub.rest.issues.create).not.toHaveBeenCalled(); + }); + + it("should create new issue when workflows are out of sync and no issue exists", async () => { + // Mock exec to return changes (non-empty diff output) + mockExec.exec + .mockImplementationOnce(async (cmd, args, options) => { + if (options?.listeners?.stdout) { + options.listeners.stdout(Buffer.from("diff content")); + } + return 1; + }) + .mockImplementationOnce(async (cmd, args, options) => { + if (options?.listeners?.stdout) { + options.listeners.stdout(Buffer.from("detailed diff content")); + } + return 0; + }); + + // Mock search to return no existing issue + mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({ + data: { + total_count: 0, + items: [], + }, + }); + + mockGithub.rest.issues.create.mockResolvedValue({ + data: { + number: 43, + html_url: "https://github.com/testowner/testrepo/issues/43", + }, + }); + + const { main } = await import("./check_workflow_recompile_needed.cjs"); + await main(); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No existing issue found")); + expect(mockGithub.rest.issues.create).toHaveBeenCalledWith({ + owner: "testowner", + repo: "testrepo", + title: "Workflows need recompilation", + body: expect.stringContaining("Instructions for GitHub Copilot"), + labels: ["maintenance", "workflows"], + }); + }); + + it("should handle errors gracefully", async () => { + // Mock exec to throw error + mockExec.exec.mockRejectedValue(new Error("Git command failed")); + + const { main } = await import("./check_workflow_recompile_needed.cjs"); + + await expect(main()).rejects.toThrow("Git command failed"); + expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("Failed to check for workflow changes")); + }); +}); diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index b08304896b..2b90b9358f 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -204,10 +204,32 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + issues: write steps: - - name: Checkout repository +`) + + // Checkout step - different behavior based on mode + if actionMode == ActionModeDev { + // Dev mode: checkout entire repository (no sparse checkout, but no credentials) + yaml.WriteString(` - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + +`) + } else { + // Release mode: sparse checkout of .github folder only + yaml.WriteString(` - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: | + .github + persist-credentials: false +`) + } + + yaml.WriteString(` - name: Setup Go uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: @@ -222,17 +244,19 @@ jobs: ./gh-aw compile --validate --verbose echo "✓ All workflows compiled successfully" - - name: Check for out-of-sync workflows - run: | - if git diff --exit-code .github/workflows/*.lock.yml; then - echo "✓ All workflow lock files are up to date" - else - echo "::error::Some workflow lock files are out of sync. Run 'make recompile' locally." - echo "::group::Diff of out-of-sync files" - git diff .github/workflows/*.lock.yml - echo "::endgroup::" - exit 1 - fi + - name: Setup Scripts + uses: ` + setupActionRef + ` + with: + destination: /tmp/gh-aw/actions + + - name: Check for out-of-sync workflows and create issue if needed + uses: ` + GetActionPin("actions/github-script") + ` + with: + script: | + const { setupGlobals } = require('/tmp/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/tmp/gh-aw/actions/check_workflow_recompile_needed.cjs'); + await main(); zizmor-scan: runs-on: ubuntu-latest