Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ba26b50
feat: add disable_agentic_workflow label-triggered job to maintenance…
Copilot Apr 30, 2026
f932e1b
fix: address code review feedback - improve regex security and env is…
Copilot Apr 30, 2026
574f10f
fix: contents read-only, create disable label, remove label after suc…
Copilot Apr 30, 2026
3dc6fe8
feat: add disable_label_trigger config field to aw.json maintenance
Copilot Apr 30, 2026
ba82e06
feat: move disable label creation to disable_agentic_workflow and add…
Copilot Apr 30, 2026
9b92f2f
fix: address code review feedback in disable_agentic_workflow.cjs
Copilot Apr 30, 2026
09370b8
feat: rename disable_label_trigger to label_trigger_disable with defa…
Copilot Apr 30, 2026
6660c5d
docs: clarify label_trigger_disable field and test assertion messages
Copilot Apr 30, 2026
fa01332
feat: rename to label_triggers, issues-only trigger, add label_apply_…
Copilot Apr 30, 2026
17ab2ae
fix: improve comment wording in test files per code review
Copilot Apr 30, 2026
a6324c3
refactor: rename disable job to label_..., use REST API, extract shar…
Copilot Apr 30, 2026
3b0b43a
Merge branch 'main' into copilot/update-agentic-maintenance-action
pelikhan Apr 30, 2026
930050b
docs(adr): add draft ADR-29269 for label-triggered maintenance jobs
github-actions[bot] Apr 30, 2026
41d5f81
fix: centralize workflow ID extraction, fix lockFileName normalizatio…
Copilot Apr 30, 2026
c7e9f58
Merge branch 'main' into copilot/update-agentic-maintenance-action
pelikhan Apr 30, 2026
4929821
fix: add explicit permission gate on label-triggered operation steps
Copilot Apr 30, 2026
e3b1c8c
fix: make label_triggers opt-in (default false), enable in this repo'…
Copilot Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/agentics-maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ on:
- main
paths:
- '.github/workflows/*.md'
issues:
types: [labeled]
workflow_dispatch:
inputs:
operation:
Expand Down Expand Up @@ -554,6 +556,94 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/run_validate_workflows.cjs');
await main();

label_disable_agentic_workflow:
if: ${{ (!(github.event.repository.fork)) && github.event_name == 'issues' && github.event.label.name == 'agentic-workflows:disable' }}
runs-on: ubuntu-slim
permissions:
actions: write
contents: read
issues: write
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
actions
persist-credentials: false

- name: Setup Scripts
uses: ./actions/setup
with:
destination: ${{ runner.temp }}/gh-aw/actions

- name: Check admin/maintainer permissions
id: check_permissions
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot make sure the member check is on a mode that fails the job.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4929821. Added id: check_permissions to the permission check step and if: ${{ steps.check_permissions.outcome == 'success' }} to the operation step in both label_disable_agentic_workflow and label_apply_safe_outputs jobs. This makes the gate explicit — the disable/apply step is skipped unless the member check step itself succeeds, regardless of any continue-on-error setting that might be applied in the future.

await main();

- name: Disable agentic workflow
if: ${{ steps.check_permissions.outcome == 'success' }}
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/disable_agentic_workflow.cjs');
await main();

label_apply_safe_outputs:
if: ${{ (!(github.event.repository.fork)) && github.event_name == 'issues' && github.event.label.name == 'agentic-workflows:apply-safe-outputs' }}
runs-on: ubuntu-slim
permissions:
actions: read
contents: write
discussions: write
issues: write
pull-requests: write
steps:
- name: Checkout actions folder
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
actions
persist-credentials: false

- name: Setup Scripts
uses: ./actions/setup
with:
destination: ${{ runner.temp }}/gh-aw/actions

- name: Check admin/maintainer permissions
id: check_permissions
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
await main();

- name: Apply safe outputs from referenced run
if: ${{ steps.check_permissions.outcome == 'success' }}
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/label_apply_safe_outputs.cjs');
await main();

compile-workflows:
if: ${{ (!(github.event.repository.fork)) && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }}
runs-on: ubuntu-slim
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/aw.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"maintenance": {
"action_failure_issue_expires": 12
"action_failure_issue_expires": 12,
"label_triggers": true
}
}
3 changes: 2 additions & 1 deletion actions/setup/js/create_labels.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,13 @@ async function main() {
core.info(`ℹ️ Label already exists: ${labelName}`);
skipped++;
} else {
const color = deterministicLabelColor(labelName);
try {
await github.rest.issues.createLabel({
owner,
repo,
name: labelName,
color: deterministicLabelColor(labelName),
color,
description: "",
});
core.info(`✅ Created label: ${labelName}`);
Expand Down
6 changes: 3 additions & 3 deletions actions/setup/js/create_labels.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,14 @@ describe("main", () => {
stderr: "",
});

// Default: repo has one existing label
// Default: repo has "bug"
mockGithub.paginate.mockResolvedValue([{ name: "bug" }]);
mockGithub.rest.issues.createLabel.mockResolvedValue({});
});

it("creates labels that are missing from the repository", async () => {
await main();

expect(mockGithub.rest.issues.createLabel).toHaveBeenCalledTimes(2);
const names = mockGithub.rest.issues.createLabel.mock.calls.map(c => c[0].name);
expect(names).toContain("enhancement");
expect(names).toContain("docs");
Expand Down Expand Up @@ -137,12 +136,13 @@ describe("main", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("already existed"));
});

it("does nothing when no labels are found", async () => {
it("does nothing when no workflow labels are found", async () => {
mockExec.getExecOutput.mockResolvedValue({
exitCode: 0,
stdout: JSON.stringify([{ labels: [] }, {}]),
stderr: "",
});
mockGithub.paginate.mockResolvedValue([]);

await main();

Expand Down
94 changes: 94 additions & 0 deletions actions/setup/js/disable_agentic_workflow.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// @ts-check
/// <reference types="@actions/github-script" />

const { getErrorMessage } = require("./error_helpers.cjs");
const { ERR_NOT_FOUND } = require("./error_codes.cjs");
const { ensureLabelExists, validateLabeledIssueEvent, removeLabelSafely } = require("./label_trigger_helpers.cjs");
const { extractWorkflowId, isValidWorkflowId } = require("./generate_footer.cjs");

const DISABLE_LABEL = "agentic-workflows:disable";
const DISABLE_LABEL_COLOR = "8250df"; // GitHub purple
const DISABLE_LABEL_DESCRIPTION = "Disable the agentic workflow that created this issue";

/**
* Disable an agentic workflow when the "agentic-workflows:disable" label is applied to an issue.
*
* Reads the labeled issue body to extract the workflow_id from XML comment markers,
* disables the corresponding agentic workflow via the GitHub REST API, and posts a comment
* confirming the action.
*
* @returns {Promise<void>}
*/
async function main() {
const ctx = validateLabeledIssueEvent(DISABLE_LABEL);
if (!ctx) return;

const { owner, repo, issueNumber, body } = ctx;

// Ensure the disable label exists so it is available for future use
await ensureLabelExists(owner, repo, DISABLE_LABEL, DISABLE_LABEL_COLOR, DISABLE_LABEL_DESCRIPTION);

core.info(`Processing issue #${issueNumber} labeled with '${DISABLE_LABEL}'`);

// Extract workflow ID from body XML comment markers
const workflowId = extractWorkflowId(body);

if (!workflowId) {
core.warning(`Could not find workflow ID in issue #${issueNumber} body. Expected a <!-- gh-aw-workflow-id: ... --> marker.`);
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body:
`> [!WARNING]\n` +
`> **Could not disable agentic workflow**\n>\n` +
`> No workflow ID marker was found in this issue's body. ` +
`The \`${DISABLE_LABEL}\` label can only be used on issues that were created by an agentic workflow ` +
`(they contain a \`<!-- gh-aw-workflow-id: ... -->\` marker).\n>\n` +
`> To disable a workflow manually, trigger the maintenance workflow with the \`disable\` operation.`,
});
core.setFailed(`${ERR_NOT_FOUND}: No workflow ID marker found in issue #${issueNumber}`);
return;
}

core.info(`Found workflow ID: ${workflowId}`);
core.info(`Disabling agentic workflow '${workflowId}'...`);

// Disable the workflow via the GitHub REST API using its compiled lock file name
const lockFileName = `${workflowId}.lock.yml`;
try {
await github.rest.actions.disableWorkflow({ owner, repo, workflow_id: lockFileName });
Comment on lines +57 to +60
} catch (err) {
const msg = getErrorMessage(err);
core.error(`Failed to disable workflow '${workflowId}': ${msg}`);
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body:
`> [!WARNING]\n` +
`> **Failed to disable agentic workflow \`${workflowId}\`**\n>\n` +
`> ${msg}\n>\n` +
`> Please check the [workflow run logs](${process.env.GITHUB_SERVER_URL || "https://github.com"}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID || ""}) for details.`,
});
core.setFailed(`Failed to disable workflow '${workflowId}': ${msg}`);
return;
}

core.info(`Successfully disabled workflow '${workflowId}'`);

// Post a success comment on the issue
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `The agentic workflow \`${workflowId}\` has been disabled.\n\n` + `To re-enable it, trigger the maintenance workflow with the \`enable\` operation.\n\n` + `<!-- gh-aw-comment-type: workflow-disabled -->`,
});

core.info(`Posted disable confirmation comment on issue #${issueNumber}`);

// Remove the disable label now that the action is complete
await removeLabelSafely(owner, repo, issueNumber, DISABLE_LABEL);
}

module.exports = { main };
Loading