-
Notifications
You must be signed in to change notification settings - Fork 445
Add maintenance.disabled_jobs to selectively omit agentics-maintenance jobs and permissions
#43300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b953e20
f576d5c
5657aee
53a985a
7c84d1e
a581bb2
bc449a4
435f29b
c90ca26
904e8d7
d87438c
0c1296a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # ADR-43300: Scoped Permissions for Maintenance Jobs via Job Splitting and Selective Disablement | ||
|
|
||
| **Date**: 2026-07-04 | ||
| **Status**: Draft | ||
| **Deciders**: Unknown (Copilot SWE agent, pelikhan) | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| The generated `agentics-maintenance.yml` contained a single `close-expired-entities` job that always requested `discussions: write`, `issues: write`, and `pull-requests: write` together — regardless of which entity types a repo actually used. Similarly, jobs such as `apply_safe_outputs` and the label-triggered jobs were always emitted, forcing write scopes on repos that had no need for them. This violated least-privilege principles and increased the blast radius of any token compromise. Repos that handled only issues (no discussions, no PRs) still received all three write permissions. There was no mechanism for repo owners to opt out of individual maintenance jobs without forking the generated workflow or disabling maintenance entirely. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will split `close-expired-entities` into three separate jobs (`close-expired-discussions`, `close-expired-issues`, `close-expired-pull-requests`), each requesting only the single write permission it needs. We will also add a `maintenance.disabled_jobs` array to `aw.json` that allows repo owners to omit specific maintenance jobs from the generated workflow. Job IDs in the list are normalized (case-insensitive, `_` and `-` treated equivalently). When `apply_safe_outputs` is disabled, the `workflow_call.outputs.applied_run_url` output falls back to `inputs.run_url` to avoid dangling job-output references. When all label-triggered jobs are individually disabled, the `issues: labeled` trigger is suppressed. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Conditional steps within the existing monolithic job | ||
|
|
||
| Keep the single `close-expired-entities` job but add `if:` conditions on each step (discussions, issues, PRs) so only the relevant steps run. This avoids splitting the job definition. | ||
|
|
||
| Why not chosen: The job-level `permissions:` block is evaluated regardless of which steps actually run; GitHub Actions does not support per-step permission narrowing. A repo that disabled the discussions step would still receive `discussions: write` on the runner token — defeating the least-privilege goal. | ||
|
|
||
| #### Alternative 2: Extend the existing `label_triggers: false` pattern with per-job boolean flags | ||
|
|
||
| Instead of a free-form `disabled_jobs` array, add individual boolean fields (e.g., `close_expired_discussions: false`, `apply_safe_outputs: false`) to the maintenance config schema. | ||
|
|
||
| Why not chosen: Each new maintenance job would require a corresponding schema field, making the config schema grow proportionally with the workflow. A string list of job IDs scales to any number of jobs without schema changes, and the normalization strategy (case-insensitive, `_`/`-` equivalence) makes the values human-friendly. The downside is that typos in job IDs silently have no effect — a validation warning or enumeration constraint could be added later. | ||
|
|
||
| #### Alternative 3: Generate separate maintenance workflow variants per repo type | ||
|
|
||
| Detect the repo's entity mix (issues-only, discussions-only, etc.) automatically from repository settings and emit a pre-tailored workflow with only the relevant jobs and permissions. | ||
|
|
||
| Why not chosen: Auto-detection would require additional API calls at compile time and could misclassify repos mid-lifecycle (e.g., a repo that enables discussions after initial setup). Explicit opt-out via `disabled_jobs` gives repo owners deterministic, auditable control without silent automation surprises. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - Each `close-expired-*` job now requests only the one write permission it needs, eliminating unnecessary token scopes. | ||
| - Repos can omit entire maintenance jobs (including `apply_safe_outputs` and label-triggered jobs) without disabling maintenance globally. | ||
| - The `issues: labeled` trigger is automatically suppressed when all label-triggered jobs are disabled, reducing event processing overhead. | ||
| - The refactored `writeCloseExpiredJob` helper eliminates code duplication across the three close-expired jobs. | ||
|
|
||
| #### Negative | ||
| - The generated `agentics-maintenance.yml` now contains up to three separate jobs where there was previously one, increasing YAML verbosity. | ||
| - `disabled_jobs` entries with typos silently have no effect; there is no validation error for unknown job names. [TODO: verify whether a schema `enum` constraint should be added] | ||
| - The normalization logic (`_`/`-` equivalence, case-insensitivity) adds a subtle invariant that must be maintained consistently across the config parser, YAML builder, and tests. | ||
|
|
||
| #### Neutral | ||
| - Existing repos that do not set `disabled_jobs` see no behavioral change; all jobs continue to be generated as before (backward-compatible default). | ||
| - The `workflow_call` output `applied_run_url` changes its source expression when `apply_safe_outputs` is disabled — callers of the maintenance workflow via `workflow_call` should be aware this value may now reflect `inputs.run_url` rather than the job's own output. | ||
| - Tests were updated to assert per-job scoped permissions and to cover the new `disabled_jobs` config path. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -129,7 +129,12 @@ You can customize the maintenance workflow runner or disable maintenance entirel | |
| { | ||
| "maintenance": { | ||
| "runs_on": "ubuntu-latest", | ||
| "action_failure_issue_expires": 72 | ||
| "action_failure_issue_expires": 72, | ||
| "disabled_jobs": [ | ||
| "close-expired-entities", | ||
| "apply_safe_outputs", | ||
| "label_apply_safe_outputs" | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
@@ -138,6 +143,19 @@ The `runs_on` field accepts a single string or an array of strings for multi-lab | |
|
|
||
| The `action_failure_issue_expires` field controls expiration (in hours) for failure issues opened by the conclusion job (including grouped parent issues when `group-reports: true`). The default is `168` (7 days). | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs omit the full list of valid job IDs — The description says the field accepts job IDs and explains normalization, but never lists which IDs are actually recognised. Users need to know the valid values. The PR description lists them ( Please add a table or inline list of all supported job IDs with a brief description of what each one does so users can make an informed choice. @copilot please address this. |
||
| The `disabled_jobs` field lets you omit specific maintenance jobs from the generated workflow. Job IDs are case-insensitive, and `_` / `-` are treated equivalently. | ||
|
|
||
| Supported job IDs: | ||
|
|
||
| | Job ID | Effect when disabled | | ||
| | --- | --- | | ||
| | `close-expired-entities` | Omits all three close-expired cleanup jobs (`close-expired-discussions`, `close-expired-issues`, and `close-expired-pull-requests`). | | ||
| | `apply_safe_outputs` | Omits the `safe_outputs` replay job. When this is disabled, `workflow_call.outputs.applied_run_url` falls back to `inputs.run_url`; scheduled and manual runs leave that output empty. | | ||
| | `label_disable_agentic_workflow` | Omits the label-triggered disable workflow job. | | ||
| | `label_apply_safe_outputs` | Omits the label-triggered safe-outputs replay job. | | ||
|
|
||
| Unrecognized job IDs are rejected during config validation. | ||
|
|
||
| See [Self-Hosted Runners](/gh-aw/reference/self-hosted-runners/#configuring-the-maintenance-workflow-runner) for more details. | ||
|
|
||
| **Disable maintenance entirely:** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,6 +87,16 @@ | |
| "description": "Set to false to disable all label-triggered jobs (disable_agentic_workflow, label_apply_safe_outputs, etc.). When absent or true (default), all label-triggered jobs are included in the maintenance workflow.", | ||
| "type": "boolean" | ||
| }, | ||
| "disabled_jobs": { | ||
| "description": "Maintenance workflow job IDs to skip when generating agentics-maintenance.yml. Supported IDs are close-expired-entities, apply_safe_outputs, label_disable_agentic_workflow, and label_apply_safe_outputs. Values are case-insensitive; underscores and hyphens are treated equivalently.", | ||
| "type": "array", | ||
| "items": { | ||
| "type": "string", | ||
| "minLength": 1, | ||
| "examples": ["close-expired-entities", "apply_safe_outputs", "label_disable_agentic_workflow", "label_apply_safe_outputs"] | ||
| }, | ||
| "uniqueItems": true | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The schema has 💡 Suggested testAdd a case to
This makes the contract explicit regardless of which validator is in use. @copilot please address this. |
||
| }, | ||
| "compile": { | ||
| "description": "Configuration for the compile-workflows maintenance job.", | ||
| "type": "object", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/grill-with-docs] The docs say job IDs are case-insensitive and
_/-equivalent, but they don't list the valid job names a user can put indisabled_jobs. Without an exhaustive list (even a short one), users must read source code to know what strings are accepted — or silently misconfigure without any error feedback.💡 Suggested addition
Add a table or list of recognized job IDs to the docs block, e.g.:
close-expired-entitiesapply_safe_outputsapplied_run_urlfalls back toinputs.run_urllabel_disable_agentic_workflowlabel_apply_safe_outputsAlso note what happens when an unrecognised job ID is provided (currently: silently ignored).
@copilot please address this.