From 6a2face42912190d1fc10ba451efe30b34fd99a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:08:17 +0000 Subject: [PATCH 01/16] Initial plan From e32a367701c30be8c73b38f93de375bc08edb65e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:27:09 +0000 Subject: [PATCH 02/16] Add workflow_dispatch operation input and run_operation job to agentic maintenance workflow Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 57 ++++++++++++ pkg/workflow/maintenance_workflow.go | 101 +++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index c4ffc44828c..81226b0efcc 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -37,6 +37,16 @@ on: schedule: - cron: "37 */2 * * *" # Every 2 hours (based on minimum expires: 1 days) workflow_dispatch: + inputs: + operation: + description: 'Optional maintenance operation to run' + required: false + type: choice + default: '' + options: + - '' + - 'disable all agentic workflows' + - 'enable all agentic workflows' permissions: {} @@ -88,6 +98,53 @@ jobs: const { main } = require('/opt/gh-aw/actions/close_expired_pull_requests.cjs'); await main(); + run_operation: + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && !github.event.repository.fork }} + runs-on: ubuntu-slim + permissions: + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Setup Scripts + uses: ./actions/setup + with: + destination: /opt/gh-aw/actions + + - name: Check admin/maintainer permissions + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_team_member.cjs'); + await main(); + + - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: go.mod + cache: true + + - name: Build gh-aw + run: make build + + - name: Disable all agentic workflows + if: ${{ github.event.inputs.operation == 'disable all agentic workflows' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gh-aw disable + + - name: Enable all agentic workflows + if: ${{ github.event.inputs.operation == 'enable all agentic workflows' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gh-aw enable + compile-workflows: if: ${{ !github.event.repository.fork }} runs-on: ubuntu-slim diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index b4b83a369fe..92e564eb2e1 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -135,6 +135,16 @@ on: schedule: - cron: "` + cronSchedule + `" # ` + scheduleDesc + ` (based on minimum expires: ` + strconv.Itoa(minExpiresDays) + ` days) workflow_dispatch: + inputs: + operation: + description: 'Optional maintenance operation to run' + required: false + type: choice + default: '' + options: + - '' + - 'disable all agentic workflows' + - 'enable all agentic workflows' permissions: {} @@ -212,6 +222,97 @@ jobs: await main(); `) + // Add run_operation job (always included; skipped via 'if' when not triggered by workflow_dispatch with operation) + yaml.WriteString(` + run_operation: + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && !github.event.repository.fork }} + runs-on: ubuntu-slim + permissions: + actions: write + steps: +`) + + if actionMode == ActionModeDev { + yaml.WriteString(` - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + +`) + } else { + yaml.WriteString(` - name: Checkout workflows + uses: ` + GetActionPin("actions/checkout") + ` + with: + sparse-checkout: | + .github/workflows + persist-credentials: false + +`) + } + + yaml.WriteString(` - name: Setup Scripts + uses: ` + setupActionRef + ` + with: + destination: /opt/gh-aw/actions + + - name: Check admin/maintainer permissions + uses: ` + GetActionPin("actions/github-script") + ` + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_team_member.cjs'); + await main(); + +`) + + if actionMode == ActionModeDev { + yaml.WriteString(` - name: Setup Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: go.mod + cache: true + + - name: Build gh-aw + run: make build + + - name: Disable all agentic workflows + if: ${{ github.event.inputs.operation == 'disable all agentic workflows' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gh-aw disable + + - name: Enable all agentic workflows + if: ${{ github.event.inputs.operation == 'enable all agentic workflows' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gh-aw enable +`) + } else { + extensionRef := version + if actionTag != "" { + extensionRef = actionTag + } + yaml.WriteString(` - name: Install gh-aw extension + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh extension install github/gh-aw@` + extensionRef + ` + + - name: Disable all agentic workflows + if: ${{ github.event.inputs.operation == 'disable all agentic workflows' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh aw disable + + - name: Enable all agentic workflows + if: ${{ github.event.inputs.operation == 'enable all agentic workflows' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh aw enable +`) + } + // Add compile-workflows and zizmor-scan jobs only in dev mode // These jobs are specific to the gh-aw repository and require go.mod, make build, etc. // User repositories won't have these dependencies, so we skip them in release mode From 4312bc5fa35dc5f842dc510b95fc585823acecb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:39:37 +0000 Subject: [PATCH 03/16] Add contents: read permission to run_operation job Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 1 + pkg/workflow/maintenance_workflow.go | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 81226b0efcc..a5257ade102 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -103,6 +103,7 @@ jobs: runs-on: ubuntu-slim permissions: actions: write + contents: read steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 92e564eb2e1..1aad0fdb09b 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -229,6 +229,7 @@ jobs: runs-on: ubuntu-slim permissions: actions: write + contents: read steps: `) From 738624ef12ff1c2cbfa5d122fcb3b654b70c7d57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:54:35 +0000 Subject: [PATCH 04/16] Resolve checkout and setup-go action pins in run_operation job Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 4 ++-- pkg/workflow/maintenance_workflow.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index a5257ade102..57ab99decb6 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -106,7 +106,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -126,7 +126,7 @@ jobs: await main(); - name: Setup Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version-file: go.mod cache: true diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 1aad0fdb09b..b453df95424 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -235,7 +235,7 @@ jobs: if actionMode == ActionModeDev { yaml.WriteString(` - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: ` + GetActionPin("actions/checkout") + ` with: persist-credentials: false @@ -270,7 +270,7 @@ jobs: if actionMode == ActionModeDev { yaml.WriteString(` - name: Setup Go - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + uses: ` + GetActionPin("actions/setup-go") + ` with: go-version-file: go.mod cache: true From 4e4a131f48586003c46c145b5ca0d5fa2364006d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:09:46 +0000 Subject: [PATCH 05/16] docs: add manual maintenance operations section to ephemerals guide Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/guides/ephemerals.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/src/content/docs/guides/ephemerals.md b/docs/src/content/docs/guides/ephemerals.md index 3af41a9a3a6..0f5a6a870b0 100644 --- a/docs/src/content/docs/guides/ephemerals.md +++ b/docs/src/content/docs/guides/ephemerals.md @@ -87,6 +87,21 @@ The maintenance workflow searches for items with this expiration format (checked See [Safe Outputs Reference](/gh-aw/reference/safe-outputs/) for complete documentation. +### Manual Maintenance Operations + +The generated `agentics-maintenance.yml` workflow also supports manual bulk operations via `workflow_dispatch`. Admin or maintainer users can trigger it from the GitHub Actions UI or the CLI to disable or enable all agentic workflows in the repository at once. + +```bash +# Disable all agentic workflows +gh aw run agentics-maintenance --raw-field operation="disable all agentic workflows" + +# Enable all agentic workflows +gh aw run agentics-maintenance --raw-field operation="enable all agentic workflows" +``` + +> [!NOTE] +> The role check uses `check_team_member.cjs` to verify the triggering user is an admin or maintainer. Manual operations are not available on forks. Dispatching without selecting an operation does nothing. + ### Close Older Issues Automatically close older issues with the same workflow-id marker when creating new ones. This keeps your issues focused on the latest information. From ab141523e26306bc147cc74f662b1960d37e3983 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:17:28 +0000 Subject: [PATCH 06/16] docs: remove code block and note from manual maintenance operations section Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/guides/ephemerals.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/src/content/docs/guides/ephemerals.md b/docs/src/content/docs/guides/ephemerals.md index 0f5a6a870b0..faa712098ca 100644 --- a/docs/src/content/docs/guides/ephemerals.md +++ b/docs/src/content/docs/guides/ephemerals.md @@ -89,18 +89,7 @@ See [Safe Outputs Reference](/gh-aw/reference/safe-outputs/) for complete docume ### Manual Maintenance Operations -The generated `agentics-maintenance.yml` workflow also supports manual bulk operations via `workflow_dispatch`. Admin or maintainer users can trigger it from the GitHub Actions UI or the CLI to disable or enable all agentic workflows in the repository at once. - -```bash -# Disable all agentic workflows -gh aw run agentics-maintenance --raw-field operation="disable all agentic workflows" - -# Enable all agentic workflows -gh aw run agentics-maintenance --raw-field operation="enable all agentic workflows" -``` - -> [!NOTE] -> The role check uses `check_team_member.cjs` to verify the triggering user is an admin or maintainer. Manual operations are not available on forks. Dispatching without selecting an operation does nothing. +The generated `agentics-maintenance.yml` workflow also supports manual bulk operations via `workflow_dispatch`. Admin or maintainer users can trigger it from the GitHub Actions UI or the CLI to disable or enable all agentic workflows in the repository at once. The operation is restricted to admin and maintainer roles and is not available on forks. ### Close Older Issues From 18df722527dce54c933caea32ebd9736f07694b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:46:18 +0000 Subject: [PATCH 07/16] feat: add update and upgrade operations to agentic maintenance workflow Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 55 ++- .../setup/js/run_operation_update_upgrade.cjs | 186 +++++++++++ .../js/run_operation_update_upgrade.test.cjs | 316 ++++++++++++++++++ pkg/workflow/maintenance_workflow.go | 88 ++++- 4 files changed, 642 insertions(+), 3 deletions(-) create mode 100644 actions/setup/js/run_operation_update_upgrade.cjs create mode 100644 actions/setup/js/run_operation_update_upgrade.test.cjs diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 57ab99decb6..ba2025e6a17 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -47,6 +47,8 @@ on: - '' - 'disable all agentic workflows' - 'enable all agentic workflows' + - 'update' + - 'upgrade' permissions: {} @@ -99,7 +101,7 @@ jobs: await main(); run_operation: - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && !github.event.repository.fork }} + if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'disable all agentic workflows' || github.event.inputs.operation == 'enable all agentic workflows') && !github.event.repository.fork }} runs-on: ubuntu-slim permissions: actions: write @@ -146,6 +148,57 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./gh-aw enable + run_update_upgrade_operation: + if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'update' || github.event.inputs.operation == 'upgrade') && !github.event.repository.fork }} + runs-on: ubuntu-slim + permissions: + actions: write + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Scripts + uses: ./actions/setup + with: + destination: /opt/gh-aw/actions + + - name: Check admin/maintainer permissions + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_team_member.cjs'); + await main(); + + - name: Setup Go + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: go.mod + cache: true + + - name: Build gh-aw + run: make build + + - name: Run update/upgrade operation + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_OPERATION: ${{ github.event.inputs.operation }} + GH_AW_CMD_PREFIX: ./gh-aw + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/run_operation_update_upgrade.cjs'); + await main(); + compile-workflows: if: ${{ !github.event.repository.fork }} runs-on: ubuntu-slim diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs new file mode 100644 index 00000000000..1464ead9cfe --- /dev/null +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -0,0 +1,186 @@ +// @ts-check +/// + +const { getErrorMessage } = require("./error_helpers.cjs"); + +/** + * Returns true when the given file path should be excluded from the PR. + * .github/workflows/*.yml files (including .lock.yml) are excluded because + * the github-actions bot cannot modify workflow files directly. + * + * @param {string} file - Relative path of the file + * @returns {boolean} + */ +function isExcludedWorkflowFile(file) { + return /^\.github\/workflows\/[^/]+\.yml$/.test(file); +} + +/** + * Format a UTC Date as YYYY-MM-DD-HH-MM-SS for use in branch names. + * Colons are not allowed in artifact filenames or branch names on some systems. + * + * @param {Date} date + * @returns {string} + */ +function formatTimestamp(date) { + /** @param {number} n */ + const pad = n => String(n).padStart(2, "0"); + return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}-${pad(date.getUTCHours())}-${pad(date.getUTCMinutes())}-${pad(date.getUTCSeconds())}`; +} + +/** + * Run 'gh aw update' or 'gh aw upgrade', then create a pull request with the + * resulting non-workflow-YAML changes if any exist. + * + * .github/workflows/*.yml files (including compiled .lock.yml files) are + * excluded from the PR because the github-actions bot cannot modify workflow + * files directly. The PR body instructs reviewers to recompile lock files + * after merging. + * + * Required environment variables: + * GH_TOKEN - GitHub token for gh CLI auth and git push + * GH_AW_OPERATION - 'update' or 'upgrade' + * GH_AW_CMD_PREFIX - Command prefix: './gh-aw' (dev) or 'gh aw' (release) + * + * @returns {Promise} + */ +async function main() { + const operation = process.env.GH_AW_OPERATION; + if (operation !== "update" && operation !== "upgrade") { + core.info(`Skipping: operation '${operation}' is not 'update' or 'upgrade'`); + return; + } + + const isUpgrade = operation === "upgrade"; + const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw"; + const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean); + + // Run gh aw update or gh aw upgrade + const fullCmd = [bin, ...prefixArgs, operation].join(" "); + core.info(`Running: ${fullCmd}`); + await exec.exec(bin, [...prefixArgs, operation]); + + // Check for changed files + const { stdout: statusOutput } = await exec.getExecOutput("git", ["status", "--porcelain"]); + + // Parse changed files - filter out .github/workflows/*.yml (including .lock.yml) + // git status --porcelain format: "XY path" (X and Y are 1-char each at positions 0-1, + // position 2 is a space, filename starts at position 3). Do NOT trim the full line + // before slicing or the positional indices shift. + const changedFiles = statusOutput + .split("\n") + .filter(line => line.length > 2) + .map(line => { + // "XY path" or "XY old -> new" for renames + const path = line.slice(3).trim(); + return path.includes(" -> ") ? (path.split(" -> ").at(-1) ?? path) : path; + }) + .filter(file => file.length > 0 && !isExcludedWorkflowFile(file)); + + if (changedFiles.length === 0) { + core.info("✓ No changes detected (excluding compiled workflow files) - nothing to create a PR for"); + return; + } + + core.info(`Found ${changedFiles.length} changed file(s) to include in PR:`); + for (const f of changedFiles) { + core.info(` ${f}`); + } + + // Configure git identity + await exec.exec("git", ["config", "user.email", "github-actions[bot]@users.noreply.github.com"]); + await exec.exec("git", ["config", "user.name", "github-actions[bot]"]); + + // Create a new branch with a filesystem-safe timestamp (no colons) + const branchName = `aw/${operation}-${formatTimestamp(new Date())}`; + core.info(`Creating branch: ${branchName}`); + await exec.exec("git", ["checkout", "-b", branchName]); + + // Stage only the non-yml files + for (const file of changedFiles) { + try { + await exec.exec("git", ["add", "--", file]); + } catch (error) { + core.warning(`Failed to stage '${file}': ${getErrorMessage(error)}`); + } + } + + // Verify staged content + const { stdout: stagedOutput } = await exec.getExecOutput("git", ["diff", "--cached", "--name-only"]); + if (!stagedOutput.trim()) { + core.info("✓ No staged changes after filtering workflow files - nothing to commit"); + return; + } + + const stagedFiles = stagedOutput + .split("\n") + .map(f => f.trim()) + .filter(Boolean); + + // Commit the changes + const commitMessage = isUpgrade ? "chore: upgrade agentic workflows" : "chore: update agentic workflows"; + await exec.exec("git", ["commit", "-m", commitMessage]); + + // Push to the new branch using a token-authenticated remote + const owner = context.repo.owner; + const repo = context.repo.repo; + const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || ""; + const remoteUrl = `https://x-access-token:${token}@github.com/${owner}/${repo}.git`; + + try { + await exec.exec("git", ["remote", "remove", "aw-push"]); + } catch { + // Remote doesn't exist yet - that's fine + } + await exec.exec("git", ["remote", "add", "aw-push", remoteUrl]); + + try { + await exec.exec("git", ["push", "aw-push", branchName]); + } finally { + // Always clean up the temporary remote + try { + await exec.exec("git", ["remote", "remove", "aw-push"]); + } catch { + // Non-fatal + } + } + + // Build PR title and body + const prTitle = isUpgrade ? "[aw] Upgrade available" : "[aw] Updates available"; + const fileList = stagedFiles.map(f => `- \`${f}\``).join("\n"); + const operationLabel = isUpgrade ? "Upgrade" : "Update"; + const prBody = `## Agentic Workflows ${operationLabel} + +The \`gh aw ${operation}\` command was run automatically and produced the following changes: + +${fileList} + +### ⚠️ Lock Files Need Recompilation + +The compiled workflow files (\`.github/workflows/*.yml\`) were **not included** in this PR because the \`github-actions\` bot cannot modify workflow files directly. + +After merging this PR, **recompile the lock files** using one of these methods: + +1. **Via @copilot**: Add a comment \`@copilot compile agentic workflows\` on this PR +2. **Via CLI**: Run \`gh aw compile --validate\` in your local checkout after merging +`; + + // Create the PR using gh CLI + core.info(`Creating PR: "${prTitle}"`); + const { stdout: prOutput } = await exec.getExecOutput("gh", ["pr", "create", "--title", prTitle, "--body", prBody, "--head", branchName], { + env: { ...process.env, GH_TOKEN: token }, + }); + + const prUrl = prOutput.trim(); + core.info(`✓ Created PR: ${prUrl}`); + core.notice(`Created PR: ${prUrl}`); + + await core.summary + .addHeading(prTitle, 2) + .addRaw(`Pull request created: [${prUrl}](${prUrl})\n\n`) + .addRaw(`**Changed files included in PR:**\n\n${fileList}\n\n`) + .addRaw(`> **Note**: The \`.github/workflows/*.yml\` lock files were excluded. Recompile them after merging via \`@copilot compile agentic workflows\` or \`gh aw compile\`.`) + .write(); +} + +module.exports = { main, isExcludedWorkflowFile, formatTimestamp }; diff --git a/actions/setup/js/run_operation_update_upgrade.test.cjs b/actions/setup/js/run_operation_update_upgrade.test.cjs new file mode 100644 index 00000000000..4593e6cd598 --- /dev/null +++ b/actions/setup/js/run_operation_update_upgrade.test.cjs @@ -0,0 +1,316 @@ +// @ts-check +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; + +/** Environment variables managed by tests */ +const TEST_ENV_VARS = ["GH_AW_OPERATION", "GH_AW_CMD_PREFIX", "GH_TOKEN", "GITHUB_TOKEN"]; + +describe("run_operation_update_upgrade", () => { + let mockCore; + let mockGithub; + let mockContext; + let mockExec; + let originalGlobals; + let originalEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + + // 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(), + notice: vi.fn(), + summary: { + addHeading: vi.fn().mockReturnThis(), + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }, + }; + + // Setup mock github + mockGithub = {}; + + // Setup mock context + mockContext = { + repo: { + owner: "testowner", + repo: "testrepo", + }, + }; + + // Setup mock exec module + mockExec = { + exec: vi.fn().mockResolvedValue(0), + getExecOutput: vi.fn(), + }; + + // Set globals for the module + global.core = mockCore; + global.github = mockGithub; + global.context = mockContext; + global.exec = mockExec; + }); + + afterEach(() => { + // Restore environment variables + for (const key of TEST_ENV_VARS) { + if (originalEnv[key] !== undefined) { + process.env[key] = originalEnv[key]; + } else { + delete process.env[key]; + } + } + + // Restore original globals + global.core = originalGlobals.core; + global.github = originalGlobals.github; + global.context = originalGlobals.context; + global.exec = originalGlobals.exec; + + vi.clearAllMocks(); + }); + + describe("isExcludedWorkflowFile", () => { + it("excludes .github/workflows/*.yml files", async () => { + const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); + expect(isExcludedWorkflowFile(".github/workflows/my-workflow.yml")).toBe(true); + expect(isExcludedWorkflowFile(".github/workflows/agentics-maintenance.yml")).toBe(true); + expect(isExcludedWorkflowFile(".github/workflows/my-workflow.lock.yml")).toBe(true); + }); + + it("does not exclude .md workflow source files", async () => { + const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); + expect(isExcludedWorkflowFile(".github/workflows/my-workflow.md")).toBe(false); + }); + + it("does not exclude files in other directories", async () => { + const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); + expect(isExcludedWorkflowFile(".github/agents/agentic-workflows.agent.md")).toBe(false); + expect(isExcludedWorkflowFile(".github/aw/actions-lock.json")).toBe(false); + }); + + it("does not exclude nested paths", async () => { + const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); + expect(isExcludedWorkflowFile(".github/workflows/subdir/file.yml")).toBe(false); + }); + }); + + describe("formatTimestamp", () => { + it("formats a date as YYYY-MM-DD-HH-MM-SS", async () => { + const { formatTimestamp } = await import("./run_operation_update_upgrade.cjs"); + const date = new Date("2026-03-03T03:17:06.000Z"); + expect(formatTimestamp(date)).toBe("2026-03-03-03-17-06"); + }); + + it("pads single-digit values with zeros", async () => { + const { formatTimestamp } = await import("./run_operation_update_upgrade.cjs"); + const date = new Date("2026-01-05T09:05:03.000Z"); + expect(formatTimestamp(date)).toBe("2026-01-05-09-05-03"); + }); + }); + + describe("main - skips non-update/upgrade operations", () => { + it("skips when operation is not set", async () => { + delete process.env.GH_AW_OPERATION; + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping")); + expect(mockExec.exec).not.toHaveBeenCalled(); + }); + + it("skips when operation is 'disable all agentic workflows'", async () => { + process.env.GH_AW_OPERATION = "disable all agentic workflows"; + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping")); + expect(mockExec.exec).not.toHaveBeenCalled(); + }); + }); + + describe("main - no changes after command", () => { + it("finishes without creating PR when no files changed", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + // git status shows no changes + mockExec.getExecOutput = vi.fn().mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 }); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No changes detected")); + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update"]); + }); + + it("finishes without PR when only workflow yml files changed", async () => { + process.env.GH_AW_OPERATION = "upgrade"; + process.env.GH_AW_CMD_PREFIX = "./gh-aw"; + process.env.GH_TOKEN = "test-token"; + + // git status shows only compiled workflow files + mockExec.getExecOutput = vi.fn().mockResolvedValue({ + stdout: " M .github/workflows/my-workflow.lock.yml\n M .github/workflows/agentics-maintenance.yml\n", + stderr: "", + exitCode: 0, + }); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No changes detected")); + }); + }); + + describe("main - creates PR when non-yml files changed", () => { + it("creates PR for update operation with non-yml changes", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + const getExecOutputMock = vi.fn(); + // git status + getExecOutputMock.mockResolvedValueOnce({ + stdout: " M .github/workflows/my-workflow.md\n M .github/workflows/my-workflow.lock.yml\n", + stderr: "", + exitCode: 0, + }); + // git diff --cached --name-only + getExecOutputMock.mockResolvedValueOnce({ + stdout: ".github/workflows/my-workflow.md\n", + stderr: "", + exitCode: 0, + }); + // gh pr create + getExecOutputMock.mockResolvedValueOnce({ + stdout: "https://github.com/testowner/testrepo/pull/1\n", + stderr: "", + exitCode: 0, + }); + mockExec.getExecOutput = getExecOutputMock; + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + // Verify gh aw update was run + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update"]); + // Verify branch was created + expect(mockExec.exec).toHaveBeenCalledWith("git", expect.arrayContaining(["checkout", "-b", expect.stringContaining("aw/update-")])); + // Verify files were staged (excluding yml) + expect(mockExec.exec).toHaveBeenCalledWith("git", ["add", "--", ".github/workflows/my-workflow.md"]); + // Verify commit was made + expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: update agentic workflows"]); + // Verify PR title + expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Updates available"]), expect.anything()); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Created PR")); + }); + + it("creates PR for upgrade operation with correct title", async () => { + process.env.GH_AW_OPERATION = "upgrade"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + const getExecOutputMock = vi.fn(); + // git status + getExecOutputMock.mockResolvedValueOnce({ + stdout: " M .github/agents/agentic-workflows.agent.md\n M .github/workflows/agentics-maintenance.yml\n", + stderr: "", + exitCode: 0, + }); + // git diff --cached --name-only + getExecOutputMock.mockResolvedValueOnce({ + stdout: ".github/agents/agentic-workflows.agent.md\n", + stderr: "", + exitCode: 0, + }); + // gh pr create + getExecOutputMock.mockResolvedValueOnce({ + stdout: "https://github.com/testowner/testrepo/pull/2\n", + stderr: "", + exitCode: 0, + }); + mockExec.getExecOutput = getExecOutputMock; + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + // Verify gh aw upgrade was run + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "upgrade"]); + // Verify correct commit message + expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: upgrade agentic workflows"]); + // Verify PR title is "[aw] Upgrade available" + expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Upgrade available"]), expect.anything()); + }); + + it("uses ./gh-aw as binary in dev mode", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "./gh-aw"; + process.env.GH_TOKEN = "test-token"; + + const getExecOutputMock = vi.fn(); + getExecOutputMock + .mockResolvedValueOnce({ stdout: " M .github/workflows/my-workflow.md\n", stderr: "", exitCode: 0 }) + .mockResolvedValueOnce({ stdout: ".github/workflows/my-workflow.md\n", stderr: "", exitCode: 0 }) + .mockResolvedValueOnce({ stdout: "https://github.com/testowner/testrepo/pull/3\n", stderr: "", exitCode: 0 }); + mockExec.getExecOutput = getExecOutputMock; + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + // Verify binary is ./gh-aw (no prefix args) + expect(mockExec.exec).toHaveBeenCalledWith("./gh-aw", ["update"]); + }); + }); + + describe("main - handles errors", () => { + it("propagates error when command fails", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + mockExec.exec = vi.fn().mockRejectedValue(new Error("Command failed")); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await expect(main()).rejects.toThrow("Command failed"); + }); + + it("warns and continues when staging a file fails", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + const getExecOutputMock = vi.fn(); + getExecOutputMock + .mockResolvedValueOnce({ + stdout: " M .github/workflows/my-workflow.md\n?? .github/aw/actions-lock.json\n", + stderr: "", + exitCode: 0, + }) + .mockResolvedValueOnce({ stdout: ".github/aw/actions-lock.json\n", stderr: "", exitCode: 0 }) + .mockResolvedValueOnce({ stdout: "https://github.com/testowner/testrepo/pull/4\n", stderr: "", exitCode: 0 }); + mockExec.getExecOutput = getExecOutputMock; + + // git add fails for the first file, succeeds for others + mockExec.exec = vi.fn().mockImplementation(async (cmd, args) => { + if (cmd === "git" && args[0] === "add" && args[2] === ".github/workflows/my-workflow.md") { + throw new Error("git add failed"); + } + return 0; + }); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to stage")); + }); + }); +}); diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index b453df95424..cc7cd8d704a 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -145,6 +145,8 @@ on: - '' - 'disable all agentic workflows' - 'enable all agentic workflows' + - 'update' + - 'upgrade' permissions: {} @@ -222,10 +224,10 @@ jobs: await main(); `) - // Add run_operation job (always included; skipped via 'if' when not triggered by workflow_dispatch with operation) + // Add run_operation job for disable/enable (skipped via 'if' for other operations) yaml.WriteString(` run_operation: - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && !github.event.repository.fork }} + if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'disable all agentic workflows' || github.event.inputs.operation == 'enable all agentic workflows') && !github.event.repository.fork }} runs-on: ubuntu-slim permissions: actions: write @@ -314,6 +316,88 @@ jobs: `) } + // Add run_update_upgrade_operation job for update/upgrade operations (both dev and release modes) + yaml.WriteString(` + run_update_upgrade_operation: + if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'update' || github.event.inputs.operation == 'upgrade') && !github.event.repository.fork }} + runs-on: ubuntu-slim + permissions: + actions: write + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: ` + GetActionPin("actions/checkout") + ` + with: + persist-credentials: false + + - name: Setup Scripts + uses: ` + setupActionRef + ` + with: + destination: /opt/gh-aw/actions + + - name: Check admin/maintainer permissions + uses: ` + GetActionPin("actions/github-script") + ` + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_team_member.cjs'); + await main(); + +`) + + if actionMode == ActionModeDev { + yaml.WriteString(` - name: Setup Go + uses: ` + GetActionPin("actions/setup-go") + ` + with: + go-version-file: go.mod + cache: true + + - name: Build gh-aw + run: make build + + - name: Run update/upgrade operation + uses: ` + GetActionPin("actions/github-script") + ` + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_OPERATION: ${{ github.event.inputs.operation }} + GH_AW_CMD_PREFIX: ./gh-aw + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/run_operation_update_upgrade.cjs'); + await main(); +`) + } else { + extensionRef := version + if actionTag != "" { + extensionRef = actionTag + } + yaml.WriteString(` - name: Install gh-aw extension + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh extension install github/gh-aw@` + extensionRef + ` + + - name: Run update/upgrade operation + uses: ` + GetActionPin("actions/github-script") + ` + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_OPERATION: ${{ github.event.inputs.operation }} + GH_AW_CMD_PREFIX: gh aw + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/run_operation_update_upgrade.cjs'); + await main(); +`) + } + // Add compile-workflows and zizmor-scan jobs only in dev mode // These jobs are specific to the gh-aw repository and require go.mod, make build, etc. // User repositories won't have these dependencies, so we skip them in release mode From 611fc225a013b21ba0a0162c1747b7088c53a953 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:59:14 +0000 Subject: [PATCH 08/16] refactor: handle all operations in JS and merge run jobs into one Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 52 +-------- .../setup/js/run_operation_update_upgrade.cjs | 38 +++++-- .../js/run_operation_update_upgrade.test.cjs | 53 +++++++++- pkg/workflow/maintenance_workflow.go | 100 +----------------- 4 files changed, 86 insertions(+), 157 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index ba2025e6a17..da5e2214d5a 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -101,55 +101,7 @@ jobs: await main(); run_operation: - if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'disable all agentic workflows' || github.event.inputs.operation == 'enable all agentic workflows') && !github.event.repository.fork }} - runs-on: ubuntu-slim - permissions: - actions: write - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Scripts - uses: ./actions/setup - with: - destination: /opt/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_team_member.cjs'); - await main(); - - - name: Setup Go - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 - with: - go-version-file: go.mod - cache: true - - - name: Build gh-aw - run: make build - - - name: Disable all agentic workflows - if: ${{ github.event.inputs.operation == 'disable all agentic workflows' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gh-aw disable - - - name: Enable all agentic workflows - if: ${{ github.event.inputs.operation == 'enable all agentic workflows' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gh-aw enable - - run_update_upgrade_operation: - if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'update' || github.event.inputs.operation == 'upgrade') && !github.event.repository.fork }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && !github.event.repository.fork }} runs-on: ubuntu-slim permissions: actions: write @@ -185,7 +137,7 @@ jobs: - name: Build gh-aw run: make build - - name: Run update/upgrade operation + - name: Run operation uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index 1464ead9cfe..7d6c1618bc8 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -29,32 +29,52 @@ function formatTimestamp(date) { } /** - * Run 'gh aw update' or 'gh aw upgrade', then create a pull request with the - * resulting non-workflow-YAML changes if any exist. + * Run 'gh aw update', 'gh aw upgrade', 'gh aw disable', or 'gh aw enable', + * creating a pull request when needed for update/upgrade operations. * - * .github/workflows/*.yml files (including compiled .lock.yml files) are - * excluded from the PR because the github-actions bot cannot modify workflow - * files directly. The PR body instructs reviewers to recompile lock files + * For update/upgrade: .github/workflows/*.yml files (including compiled .lock.yml + * files) are excluded from the PR because the github-actions bot cannot modify + * workflow files directly. The PR body instructs reviewers to recompile lock files * after merging. * + * For disable/enable: simply runs the command; no PR is created. + * * Required environment variables: * GH_TOKEN - GitHub token for gh CLI auth and git push - * GH_AW_OPERATION - 'update' or 'upgrade' + * GH_AW_OPERATION - 'update', 'upgrade', 'disable all agentic workflows', + * or 'enable all agentic workflows' * GH_AW_CMD_PREFIX - Command prefix: './gh-aw' (dev) or 'gh aw' (release) * * @returns {Promise} */ async function main() { const operation = process.env.GH_AW_OPERATION; - if (operation !== "update" && operation !== "upgrade") { - core.info(`Skipping: operation '${operation}' is not 'update' or 'upgrade'`); + if (!operation) { + core.info("Skipping: no operation specified"); return; } - const isUpgrade = operation === "upgrade"; const cmdPrefixStr = process.env.GH_AW_CMD_PREFIX || "gh aw"; const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean); + // Handle enable/disable operations: run the command and finish (no PR needed) + if (operation === "disable all agentic workflows" || operation === "enable all agentic workflows") { + const subCmd = operation === "disable all agentic workflows" ? "disable" : "enable"; + const fullCmd = [bin, ...prefixArgs, subCmd].join(" "); + core.info(`Running: ${fullCmd}`); + await exec.exec(bin, [...prefixArgs, subCmd]); + core.info(`✓ All agentic workflows have been ${subCmd}d`); + return; + } + + // For update/upgrade, validate operation and proceed with PR creation if files changed + if (operation !== "update" && operation !== "upgrade") { + core.info(`Skipping: unknown operation '${operation}'`); + return; + } + + const isUpgrade = operation === "upgrade"; + // Run gh aw update or gh aw upgrade const fullCmd = [bin, ...prefixArgs, operation].join(" "); core.info(`Running: ${fullCmd}`); diff --git a/actions/setup/js/run_operation_update_upgrade.test.cjs b/actions/setup/js/run_operation_update_upgrade.test.cjs index 4593e6cd598..5854896f684 100644 --- a/actions/setup/js/run_operation_update_upgrade.test.cjs +++ b/actions/setup/js/run_operation_update_upgrade.test.cjs @@ -127,8 +127,8 @@ describe("run_operation_update_upgrade", () => { expect(mockExec.exec).not.toHaveBeenCalled(); }); - it("skips when operation is 'disable all agentic workflows'", async () => { - process.env.GH_AW_OPERATION = "disable all agentic workflows"; + it("skips when operation is unknown", async () => { + process.env.GH_AW_OPERATION = "unknown-operation"; const { main } = await import("./run_operation_update_upgrade.cjs"); await main(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Skipping")); @@ -136,6 +136,55 @@ describe("run_operation_update_upgrade", () => { }); }); + describe("main - disable/enable operations", () => { + it("runs gh aw disable and finishes without PR", async () => { + process.env.GH_AW_OPERATION = "disable all agentic workflows"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "disable"]); + expect(mockExec.exec).toHaveBeenCalledTimes(1); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("disabled")); + expect(mockExec.getExecOutput).not.toHaveBeenCalled(); + }); + + it("runs gh aw enable and finishes without PR", async () => { + process.env.GH_AW_OPERATION = "enable all agentic workflows"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "enable"]); + expect(mockExec.exec).toHaveBeenCalledTimes(1); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("enabled")); + expect(mockExec.getExecOutput).not.toHaveBeenCalled(); + }); + + it("runs ./gh-aw disable in dev mode", async () => { + process.env.GH_AW_OPERATION = "disable all agentic workflows"; + process.env.GH_AW_CMD_PREFIX = "./gh-aw"; + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockExec.exec).toHaveBeenCalledWith("./gh-aw", ["disable"]); + expect(mockExec.exec).toHaveBeenCalledTimes(1); + }); + + it("propagates error when disable command fails", async () => { + process.env.GH_AW_OPERATION = "disable all agentic workflows"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + + mockExec.exec = vi.fn().mockRejectedValue(new Error("Command failed")); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await expect(main()).rejects.toThrow("Command failed"); + }); + }); + describe("main - no changes after command", () => { it("finishes without creating PR when no files changed", async () => { process.env.GH_AW_OPERATION = "update"; diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index cc7cd8d704a..08b34bc8d22 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -224,102 +224,10 @@ jobs: await main(); `) - // Add run_operation job for disable/enable (skipped via 'if' for other operations) + // Add unified run_operation job for all dispatch operations yaml.WriteString(` run_operation: - if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'disable all agentic workflows' || github.event.inputs.operation == 'enable all agentic workflows') && !github.event.repository.fork }} - runs-on: ubuntu-slim - permissions: - actions: write - contents: read - steps: -`) - - if actionMode == ActionModeDev { - yaml.WriteString(` - name: Checkout repository - uses: ` + GetActionPin("actions/checkout") + ` - with: - persist-credentials: false - -`) - } else { - yaml.WriteString(` - name: Checkout workflows - uses: ` + GetActionPin("actions/checkout") + ` - with: - sparse-checkout: | - .github/workflows - persist-credentials: false - -`) - } - - yaml.WriteString(` - name: Setup Scripts - uses: ` + setupActionRef + ` - with: - destination: /opt/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: ` + GetActionPin("actions/github-script") + ` - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_team_member.cjs'); - await main(); - -`) - - if actionMode == ActionModeDev { - yaml.WriteString(` - name: Setup Go - uses: ` + GetActionPin("actions/setup-go") + ` - with: - go-version-file: go.mod - cache: true - - - name: Build gh-aw - run: make build - - - name: Disable all agentic workflows - if: ${{ github.event.inputs.operation == 'disable all agentic workflows' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gh-aw disable - - - name: Enable all agentic workflows - if: ${{ github.event.inputs.operation == 'enable all agentic workflows' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gh-aw enable -`) - } else { - extensionRef := version - if actionTag != "" { - extensionRef = actionTag - } - yaml.WriteString(` - name: Install gh-aw extension - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh extension install github/gh-aw@` + extensionRef + ` - - - name: Disable all agentic workflows - if: ${{ github.event.inputs.operation == 'disable all agentic workflows' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh aw disable - - - name: Enable all agentic workflows - if: ${{ github.event.inputs.operation == 'enable all agentic workflows' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh aw enable -`) - } - - // Add run_update_upgrade_operation job for update/upgrade operations (both dev and release modes) - yaml.WriteString(` - run_update_upgrade_operation: - if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.operation == 'update' || github.event.inputs.operation == 'upgrade') && !github.event.repository.fork }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && !github.event.repository.fork }} runs-on: ubuntu-slim permissions: actions: write @@ -358,7 +266,7 @@ jobs: - name: Build gh-aw run: make build - - name: Run update/upgrade operation + - name: Run operation uses: ` + GetActionPin("actions/github-script") + ` env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -382,7 +290,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh extension install github/gh-aw@` + extensionRef + ` - - name: Run update/upgrade operation + - name: Run operation uses: ` + GetActionPin("actions/github-script") + ` env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f02d4a7e9b40612a5b011c0d5378f879cff7af83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:09:14 +0000 Subject: [PATCH 09/16] fix: check exec.exec exit code and simplify enable/disable option names Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentics-maintenance.yml | 4 +-- .../setup/js/run_operation_update_upgrade.cjs | 20 ++++++++----- .../js/run_operation_update_upgrade.test.cjs | 29 ++++++++++++++++--- pkg/workflow/maintenance_workflow.go | 4 +-- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index da5e2214d5a..e5af70fd633 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -45,8 +45,8 @@ on: default: '' options: - '' - - 'disable all agentic workflows' - - 'enable all agentic workflows' + - 'disable' + - 'enable' - 'update' - 'upgrade' diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index 7d6c1618bc8..ddd5fe4ffbe 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -41,8 +41,7 @@ function formatTimestamp(date) { * * Required environment variables: * GH_TOKEN - GitHub token for gh CLI auth and git push - * GH_AW_OPERATION - 'update', 'upgrade', 'disable all agentic workflows', - * or 'enable all agentic workflows' + * GH_AW_OPERATION - 'update', 'upgrade', 'disable', or 'enable' * GH_AW_CMD_PREFIX - Command prefix: './gh-aw' (dev) or 'gh aw' (release) * * @returns {Promise} @@ -58,12 +57,14 @@ async function main() { const [bin, ...prefixArgs] = cmdPrefixStr.split(" ").filter(Boolean); // Handle enable/disable operations: run the command and finish (no PR needed) - if (operation === "disable all agentic workflows" || operation === "enable all agentic workflows") { - const subCmd = operation === "disable all agentic workflows" ? "disable" : "enable"; - const fullCmd = [bin, ...prefixArgs, subCmd].join(" "); + if (operation === "disable" || operation === "enable") { + const fullCmd = [bin, ...prefixArgs, operation].join(" "); core.info(`Running: ${fullCmd}`); - await exec.exec(bin, [...prefixArgs, subCmd]); - core.info(`✓ All agentic workflows have been ${subCmd}d`); + const exitCode = await exec.exec(bin, [...prefixArgs, operation]); + if (exitCode !== 0) { + throw new Error(`Command '${fullCmd}' failed with exit code ${exitCode}`); + } + core.info(`✓ All agentic workflows have been ${operation}d`); return; } @@ -78,7 +79,10 @@ async function main() { // Run gh aw update or gh aw upgrade const fullCmd = [bin, ...prefixArgs, operation].join(" "); core.info(`Running: ${fullCmd}`); - await exec.exec(bin, [...prefixArgs, operation]); + const exitCode = await exec.exec(bin, [...prefixArgs, operation]); + if (exitCode !== 0) { + throw new Error(`Command '${fullCmd}' failed with exit code ${exitCode}`); + } // Check for changed files const { stdout: statusOutput } = await exec.getExecOutput("git", ["status", "--porcelain"]); diff --git a/actions/setup/js/run_operation_update_upgrade.test.cjs b/actions/setup/js/run_operation_update_upgrade.test.cjs index 5854896f684..2bc4c3b4c2b 100644 --- a/actions/setup/js/run_operation_update_upgrade.test.cjs +++ b/actions/setup/js/run_operation_update_upgrade.test.cjs @@ -138,7 +138,7 @@ describe("run_operation_update_upgrade", () => { describe("main - disable/enable operations", () => { it("runs gh aw disable and finishes without PR", async () => { - process.env.GH_AW_OPERATION = "disable all agentic workflows"; + process.env.GH_AW_OPERATION = "disable"; process.env.GH_AW_CMD_PREFIX = "gh aw"; const { main } = await import("./run_operation_update_upgrade.cjs"); @@ -151,7 +151,7 @@ describe("run_operation_update_upgrade", () => { }); it("runs gh aw enable and finishes without PR", async () => { - process.env.GH_AW_OPERATION = "enable all agentic workflows"; + process.env.GH_AW_OPERATION = "enable"; process.env.GH_AW_CMD_PREFIX = "gh aw"; const { main } = await import("./run_operation_update_upgrade.cjs"); @@ -164,7 +164,7 @@ describe("run_operation_update_upgrade", () => { }); it("runs ./gh-aw disable in dev mode", async () => { - process.env.GH_AW_OPERATION = "disable all agentic workflows"; + process.env.GH_AW_OPERATION = "disable"; process.env.GH_AW_CMD_PREFIX = "./gh-aw"; const { main } = await import("./run_operation_update_upgrade.cjs"); @@ -175,7 +175,7 @@ describe("run_operation_update_upgrade", () => { }); it("propagates error when disable command fails", async () => { - process.env.GH_AW_OPERATION = "disable all agentic workflows"; + process.env.GH_AW_OPERATION = "disable"; process.env.GH_AW_CMD_PREFIX = "gh aw"; mockExec.exec = vi.fn().mockRejectedValue(new Error("Command failed")); @@ -183,6 +183,16 @@ describe("run_operation_update_upgrade", () => { const { main } = await import("./run_operation_update_upgrade.cjs"); await expect(main()).rejects.toThrow("Command failed"); }); + + it("throws when disable exits with non-zero code", async () => { + process.env.GH_AW_OPERATION = "disable"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + + mockExec.exec = vi.fn().mockResolvedValue(1); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await expect(main()).rejects.toThrow("exit code 1"); + }); }); describe("main - no changes after command", () => { @@ -332,6 +342,17 @@ describe("run_operation_update_upgrade", () => { await expect(main()).rejects.toThrow("Command failed"); }); + it("throws when update exits with non-zero code", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + mockExec.exec = vi.fn().mockResolvedValue(1); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await expect(main()).rejects.toThrow("exit code 1"); + }); + it("warns and continues when staging a file fails", async () => { process.env.GH_AW_OPERATION = "update"; process.env.GH_AW_CMD_PREFIX = "gh aw"; diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go index 08b34bc8d22..05f921554d9 100644 --- a/pkg/workflow/maintenance_workflow.go +++ b/pkg/workflow/maintenance_workflow.go @@ -143,8 +143,8 @@ on: default: '' options: - '' - - 'disable all agentic workflows' - - 'enable all agentic workflows' + - 'disable' + - 'enable' - 'update' - 'upgrade' From 4d4289afb98335cb5e1a0153eb446e5b32aa066f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:35:41 +0000 Subject: [PATCH 10/16] feat: merge main, use --no-compile for update/upgrade, remove lock.yml filter Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/run_operation_update_upgrade.cjs | 44 ++++--------- .../js/run_operation_update_upgrade.test.cjs | 65 ++++--------------- 2 files changed, 25 insertions(+), 84 deletions(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index ddd5fe4ffbe..da5dfbb4517 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -3,18 +3,6 @@ const { getErrorMessage } = require("./error_helpers.cjs"); -/** - * Returns true when the given file path should be excluded from the PR. - * .github/workflows/*.yml files (including .lock.yml) are excluded because - * the github-actions bot cannot modify workflow files directly. - * - * @param {string} file - Relative path of the file - * @returns {boolean} - */ -function isExcludedWorkflowFile(file) { - return /^\.github\/workflows\/[^/]+\.yml$/.test(file); -} - /** * Format a UTC Date as YYYY-MM-DD-HH-MM-SS for use in branch names. * Colons are not allowed in artifact filenames or branch names on some systems. @@ -32,10 +20,9 @@ function formatTimestamp(date) { * Run 'gh aw update', 'gh aw upgrade', 'gh aw disable', or 'gh aw enable', * creating a pull request when needed for update/upgrade operations. * - * For update/upgrade: .github/workflows/*.yml files (including compiled .lock.yml - * files) are excluded from the PR because the github-actions bot cannot modify - * workflow files directly. The PR body instructs reviewers to recompile lock files - * after merging. + * For update/upgrade: runs with --no-compile so lock files are not modified. + * A pull request is opened for any changed files. The PR body instructs + * reviewers to recompile lock files after merging. * * For disable/enable: simply runs the command; no PR is created. * @@ -76,10 +63,10 @@ async function main() { const isUpgrade = operation === "upgrade"; - // Run gh aw update or gh aw upgrade - const fullCmd = [bin, ...prefixArgs, operation].join(" "); + // Run gh aw update or gh aw upgrade (--no-compile: do not touch lock files) + const fullCmd = [bin, ...prefixArgs, operation, "--no-compile"].join(" "); core.info(`Running: ${fullCmd}`); - const exitCode = await exec.exec(bin, [...prefixArgs, operation]); + const exitCode = await exec.exec(bin, [...prefixArgs, operation, "--no-compile"]); if (exitCode !== 0) { throw new Error(`Command '${fullCmd}' failed with exit code ${exitCode}`); } @@ -87,10 +74,9 @@ async function main() { // Check for changed files const { stdout: statusOutput } = await exec.getExecOutput("git", ["status", "--porcelain"]); - // Parse changed files - filter out .github/workflows/*.yml (including .lock.yml) - // git status --porcelain format: "XY path" (X and Y are 1-char each at positions 0-1, - // position 2 is a space, filename starts at position 3). Do NOT trim the full line - // before slicing or the positional indices shift. + // Parse changed files from git status --porcelain format: "XY path" + // X and Y are 1-char each at positions 0-1, position 2 is a space, + // filename starts at position 3. Do NOT trim the full line before slicing. const changedFiles = statusOutput .split("\n") .filter(line => line.length > 2) @@ -99,10 +85,10 @@ async function main() { const path = line.slice(3).trim(); return path.includes(" -> ") ? (path.split(" -> ").at(-1) ?? path) : path; }) - .filter(file => file.length > 0 && !isExcludedWorkflowFile(file)); + .filter(file => file.length > 0); if (changedFiles.length === 0) { - core.info("✓ No changes detected (excluding compiled workflow files) - nothing to create a PR for"); + core.info("✓ No changes detected - nothing to create a PR for"); return; } @@ -175,14 +161,12 @@ async function main() { const operationLabel = isUpgrade ? "Upgrade" : "Update"; const prBody = `## Agentic Workflows ${operationLabel} -The \`gh aw ${operation}\` command was run automatically and produced the following changes: +The \`gh aw ${operation} --no-compile\` command was run automatically and produced the following changes: ${fileList} ### ⚠️ Lock Files Need Recompilation -The compiled workflow files (\`.github/workflows/*.yml\`) were **not included** in this PR because the \`github-actions\` bot cannot modify workflow files directly. - After merging this PR, **recompile the lock files** using one of these methods: 1. **Via @copilot**: Add a comment \`@copilot compile agentic workflows\` on this PR @@ -203,8 +187,8 @@ After merging this PR, **recompile the lock files** using one of these methods: .addHeading(prTitle, 2) .addRaw(`Pull request created: [${prUrl}](${prUrl})\n\n`) .addRaw(`**Changed files included in PR:**\n\n${fileList}\n\n`) - .addRaw(`> **Note**: The \`.github/workflows/*.yml\` lock files were excluded. Recompile them after merging via \`@copilot compile agentic workflows\` or \`gh aw compile\`.`) + .addRaw(`> **Note**: Recompile lock files after merging via \`@copilot compile agentic workflows\` or \`gh aw compile\`.`) .write(); } -module.exports = { main, isExcludedWorkflowFile, formatTimestamp }; +module.exports = { main, formatTimestamp }; diff --git a/actions/setup/js/run_operation_update_upgrade.test.cjs b/actions/setup/js/run_operation_update_upgrade.test.cjs index 2bc4c3b4c2b..a9d310de4f2 100644 --- a/actions/setup/js/run_operation_update_upgrade.test.cjs +++ b/actions/setup/js/run_operation_update_upgrade.test.cjs @@ -79,31 +79,6 @@ describe("run_operation_update_upgrade", () => { vi.clearAllMocks(); }); - describe("isExcludedWorkflowFile", () => { - it("excludes .github/workflows/*.yml files", async () => { - const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); - expect(isExcludedWorkflowFile(".github/workflows/my-workflow.yml")).toBe(true); - expect(isExcludedWorkflowFile(".github/workflows/agentics-maintenance.yml")).toBe(true); - expect(isExcludedWorkflowFile(".github/workflows/my-workflow.lock.yml")).toBe(true); - }); - - it("does not exclude .md workflow source files", async () => { - const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); - expect(isExcludedWorkflowFile(".github/workflows/my-workflow.md")).toBe(false); - }); - - it("does not exclude files in other directories", async () => { - const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); - expect(isExcludedWorkflowFile(".github/agents/agentic-workflows.agent.md")).toBe(false); - expect(isExcludedWorkflowFile(".github/aw/actions-lock.json")).toBe(false); - }); - - it("does not exclude nested paths", async () => { - const { isExcludedWorkflowFile } = await import("./run_operation_update_upgrade.cjs"); - expect(isExcludedWorkflowFile(".github/workflows/subdir/file.yml")).toBe(false); - }); - }); - describe("formatTimestamp", () => { it("formats a date as YYYY-MM-DD-HH-MM-SS", async () => { const { formatTimestamp } = await import("./run_operation_update_upgrade.cjs"); @@ -208,30 +183,12 @@ describe("run_operation_update_upgrade", () => { await main(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No changes detected")); - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update"]); - }); - - it("finishes without PR when only workflow yml files changed", async () => { - process.env.GH_AW_OPERATION = "upgrade"; - process.env.GH_AW_CMD_PREFIX = "./gh-aw"; - process.env.GH_TOKEN = "test-token"; - - // git status shows only compiled workflow files - mockExec.getExecOutput = vi.fn().mockResolvedValue({ - stdout: " M .github/workflows/my-workflow.lock.yml\n M .github/workflows/agentics-maintenance.yml\n", - stderr: "", - exitCode: 0, - }); - - const { main } = await import("./run_operation_update_upgrade.cjs"); - await main(); - - expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No changes detected")); + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update", "--no-compile"]); }); }); - describe("main - creates PR when non-yml files changed", () => { - it("creates PR for update operation with non-yml changes", async () => { + describe("main - creates PR when files changed", () => { + it("creates PR for update operation with changes", async () => { process.env.GH_AW_OPERATION = "update"; process.env.GH_AW_CMD_PREFIX = "gh aw"; process.env.GH_TOKEN = "test-token"; @@ -239,7 +196,7 @@ describe("run_operation_update_upgrade", () => { const getExecOutputMock = vi.fn(); // git status getExecOutputMock.mockResolvedValueOnce({ - stdout: " M .github/workflows/my-workflow.md\n M .github/workflows/my-workflow.lock.yml\n", + stdout: " M .github/workflows/my-workflow.md\n", stderr: "", exitCode: 0, }); @@ -260,11 +217,11 @@ describe("run_operation_update_upgrade", () => { const { main } = await import("./run_operation_update_upgrade.cjs"); await main(); - // Verify gh aw update was run - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update"]); + // Verify gh aw update --no-compile was run + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update", "--no-compile"]); // Verify branch was created expect(mockExec.exec).toHaveBeenCalledWith("git", expect.arrayContaining(["checkout", "-b", expect.stringContaining("aw/update-")])); - // Verify files were staged (excluding yml) + // Verify file was staged expect(mockExec.exec).toHaveBeenCalledWith("git", ["add", "--", ".github/workflows/my-workflow.md"]); // Verify commit was made expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: update agentic workflows"]); @@ -302,8 +259,8 @@ describe("run_operation_update_upgrade", () => { const { main } = await import("./run_operation_update_upgrade.cjs"); await main(); - // Verify gh aw upgrade was run - expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "upgrade"]); + // Verify gh aw upgrade --no-compile was run + expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "upgrade", "--no-compile"]); // Verify correct commit message expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: upgrade agentic workflows"]); // Verify PR title is "[aw] Upgrade available" @@ -325,8 +282,8 @@ describe("run_operation_update_upgrade", () => { const { main } = await import("./run_operation_update_upgrade.cjs"); await main(); - // Verify binary is ./gh-aw (no prefix args) - expect(mockExec.exec).toHaveBeenCalledWith("./gh-aw", ["update"]); + // Verify binary is ./gh-aw (no prefix args) with --no-compile + expect(mockExec.exec).toHaveBeenCalledWith("./gh-aw", ["update", "--no-compile"]); }); }); From 15a9b7f926230bfdbc9a74a5d2c870852ed8e8fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:50:10 +0000 Subject: [PATCH 11/16] feat: label PR with agentic-workflows when creating update/upgrade PR Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_operation_update_upgrade.cjs | 2 +- actions/setup/js/run_operation_update_upgrade.test.cjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index da5dfbb4517..85b1e6eefd0 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -175,7 +175,7 @@ After merging this PR, **recompile the lock files** using one of these methods: // Create the PR using gh CLI core.info(`Creating PR: "${prTitle}"`); - const { stdout: prOutput } = await exec.getExecOutput("gh", ["pr", "create", "--title", prTitle, "--body", prBody, "--head", branchName], { + const { stdout: prOutput } = await exec.getExecOutput("gh", ["pr", "create", "--title", prTitle, "--body", prBody, "--head", branchName, "--label", "agentic-workflows"], { env: { ...process.env, GH_TOKEN: token }, }); diff --git a/actions/setup/js/run_operation_update_upgrade.test.cjs b/actions/setup/js/run_operation_update_upgrade.test.cjs index a9d310de4f2..e3a6d1a38ed 100644 --- a/actions/setup/js/run_operation_update_upgrade.test.cjs +++ b/actions/setup/js/run_operation_update_upgrade.test.cjs @@ -226,7 +226,7 @@ describe("run_operation_update_upgrade", () => { // Verify commit was made expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: update agentic workflows"]); // Verify PR title - expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Updates available"]), expect.anything()); + expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Updates available", "--label", "agentic-workflows"]), expect.anything()); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Created PR")); }); @@ -264,7 +264,7 @@ describe("run_operation_update_upgrade", () => { // Verify correct commit message expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: upgrade agentic workflows"]); // Verify PR title is "[aw] Upgrade available" - expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Upgrade available"]), expect.anything()); + expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Upgrade available", "--label", "agentic-workflows"]), expect.anything()); }); it("uses ./gh-aw as binary in dev mode", async () => { From cb8b36f3cd769fcee550bbe4417fb31cc565de35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:59:11 +0000 Subject: [PATCH 12/16] fix: replace .at(-1) with es2020-compatible index access Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_operation_update_upgrade.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index 85b1e6eefd0..bdedc5bf672 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -83,7 +83,8 @@ async function main() { .map(line => { // "XY path" or "XY old -> new" for renames const path = line.slice(3).trim(); - return path.includes(" -> ") ? (path.split(" -> ").at(-1) ?? path) : path; + const parts = path.split(" -> "); + return path.includes(" -> ") ? (parts[parts.length - 1] ?? path) : path; }) .filter(file => file.length > 0); From 6b3190319cabc117cfc290d530566eb88a83feee Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Mar 2026 05:03:43 +0000 Subject: [PATCH 13/16] Update run_operation_update_upgrade.cjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- actions/setup/js/run_operation_update_upgrade.cjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index bdedc5bf672..34e97738893 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -136,7 +136,14 @@ async function main() { const owner = context.repo.owner; const repo = context.repo.repo; const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || ""; - const remoteUrl = `https://x-access-token:${token}@github.com/${owner}/${repo}.git`; + const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + let githubHost; + try { + githubHost = new URL(githubServerUrl).hostname || "github.com"; + } catch { + githubHost = "github.com"; + } + const remoteUrl = `https://x-access-token:${token}@${githubHost}/${owner}/${repo}.git`; try { await exec.exec("git", ["remote", "remove", "aw-push"]); From 9fbc884df44fd369847186428aa12ef60a322147 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Tue, 3 Mar 2026 05:04:09 +0000 Subject: [PATCH 14/16] Update run_operation_update_upgrade.cjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- actions/setup/js/run_operation_update_upgrade.cjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index 34e97738893..c0aeb6d269a 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -135,7 +135,12 @@ async function main() { // Push to the new branch using a token-authenticated remote const owner = context.repo.owner; const repo = context.repo.repo; - const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || ""; + const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; + if (!token) { + throw new Error( + "Missing GitHub token: set GH_TOKEN or GITHUB_TOKEN to push changes and create a pull request for agentic workflow update/upgrade operations." + ); + } const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; let githubHost; try { From e9e17e8cb7d4cf72b01feed63730547aa0240386 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:05:36 +0000 Subject: [PATCH 15/16] fix: filter out .github/workflows/*.yml files from PR staging Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/run_operation_update_upgrade.cjs | 25 +++++++++++++++---- .../js/run_operation_update_upgrade.test.cjs | 20 +++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index c0aeb6d269a..12cee675f2c 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -93,8 +93,23 @@ async function main() { return; } - core.info(`Found ${changedFiles.length} changed file(s) to include in PR:`); - for (const f of changedFiles) { + // Exclude .github/workflows/*.yml files: they cannot be modified by the + // GitHub Actions bot and including them would cause the PR checks to fail. + const filesToStage = changedFiles.filter(file => { + const lower = file.toLowerCase(); + return !( + lower.startsWith(".github/workflows/") && + (lower.endsWith(".yml") || lower.endsWith(".yaml")) + ); + }); + + if (filesToStage.length === 0) { + core.info("✓ No non-workflow files changed - nothing to create a PR for"); + return; + } + + core.info(`Found ${filesToStage.length} file(s) to include in PR:`); + for (const f of filesToStage) { core.info(` ${f}`); } @@ -107,8 +122,8 @@ async function main() { core.info(`Creating branch: ${branchName}`); await exec.exec("git", ["checkout", "-b", branchName]); - // Stage only the non-yml files - for (const file of changedFiles) { + // Stage non-workflow-yml files only + for (const file of filesToStage) { try { await exec.exec("git", ["add", "--", file]); } catch (error) { @@ -119,7 +134,7 @@ async function main() { // Verify staged content const { stdout: stagedOutput } = await exec.getExecOutput("git", ["diff", "--cached", "--name-only"]); if (!stagedOutput.trim()) { - core.info("✓ No staged changes after filtering workflow files - nothing to commit"); + core.info("✓ No staged changes - nothing to commit"); return; } diff --git a/actions/setup/js/run_operation_update_upgrade.test.cjs b/actions/setup/js/run_operation_update_upgrade.test.cjs index e3a6d1a38ed..2a7d05d8d96 100644 --- a/actions/setup/js/run_operation_update_upgrade.test.cjs +++ b/actions/setup/js/run_operation_update_upgrade.test.cjs @@ -185,6 +185,24 @@ describe("run_operation_update_upgrade", () => { expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No changes detected")); expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update", "--no-compile"]); }); + + it("finishes without PR when only workflow yml files changed", async () => { + process.env.GH_AW_OPERATION = "update"; + process.env.GH_AW_CMD_PREFIX = "gh aw"; + process.env.GH_TOKEN = "test-token"; + + mockExec.getExecOutput = vi.fn().mockResolvedValueOnce({ + stdout: " M .github/workflows/agentics-maintenance.yml\n", + stderr: "", + exitCode: 0, + }); + + const { main } = await import("./run_operation_update_upgrade.cjs"); + await main(); + + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No non-workflow files changed")); + expect(mockExec.exec).not.toHaveBeenCalledWith("git", expect.arrayContaining(["add"])); + }); }); describe("main - creates PR when files changed", () => { @@ -265,6 +283,8 @@ describe("run_operation_update_upgrade", () => { expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: upgrade agentic workflows"]); // Verify PR title is "[aw] Upgrade available" expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Upgrade available", "--label", "agentic-workflows"]), expect.anything()); + // Verify workflow yml was NOT staged + expect(mockExec.exec).not.toHaveBeenCalledWith("git", ["add", "--", ".github/workflows/agentics-maintenance.yml"]); }); it("uses ./gh-aw as binary in dev mode", async () => { From 1f8fcfc1c2c22748eccebe996edee2eb8157c6fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:11:54 +0000 Subject: [PATCH 16/16] fix: run prettier on run_operation_update_upgrade.cjs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_operation_update_upgrade.cjs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index 12cee675f2c..4098e9ab2bb 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -97,10 +97,7 @@ async function main() { // GitHub Actions bot and including them would cause the PR checks to fail. const filesToStage = changedFiles.filter(file => { const lower = file.toLowerCase(); - return !( - lower.startsWith(".github/workflows/") && - (lower.endsWith(".yml") || lower.endsWith(".yaml")) - ); + return !(lower.startsWith(".github/workflows/") && (lower.endsWith(".yml") || lower.endsWith(".yaml"))); }); if (filesToStage.length === 0) { @@ -152,9 +149,7 @@ async function main() { const repo = context.repo.repo; const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; if (!token) { - throw new Error( - "Missing GitHub token: set GH_TOKEN or GITHUB_TOKEN to push changes and create a pull request for agentic workflow update/upgrade operations." - ); + throw new Error("Missing GitHub token: set GH_TOKEN or GITHUB_TOKEN to push changes and create a pull request for agentic workflow update/upgrade operations."); } const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; let githubHost;