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