-
Notifications
You must be signed in to change notification settings - Fork 350
feat: add apply_safe_outputs workflow-dispatch job to agentic-maintenance #22973
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
5dedda9
80ac80b
266cce4
f1cdba8
131db41
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,184 @@ | ||||||||||||||||||||||||||||||||||||||||
| // @ts-check | ||||||||||||||||||||||||||||||||||||||||
| /// <reference types="@actions/github-script" /> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Apply Safe Outputs Replay Driver | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * Downloads the agent output artifact from a previous workflow run and replays | ||||||||||||||||||||||||||||||||||||||||
| * the safe outputs, applying them to the repository. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * Called from the `apply_safe_outputs` job in the agentic-maintenance workflow. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * Required environment variables: | ||||||||||||||||||||||||||||||||||||||||
| * GH_AW_RUN_URL - Run URL or run ID to replay safe outputs from. | ||||||||||||||||||||||||||||||||||||||||
| * Accepts a full URL (https://github.com/{owner}/{repo}/actions/runs/{runId}) | ||||||||||||||||||||||||||||||||||||||||
| * or a plain run ID (digits only). | ||||||||||||||||||||||||||||||||||||||||
| * GH_TOKEN - GitHub token for artifact download via `gh run download`. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * Optional environment variables: | ||||||||||||||||||||||||||||||||||||||||
| * GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG - If set, overrides the auto-generated handler config. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const fs = require("fs"); | ||||||||||||||||||||||||||||||||||||||||
| const path = require("path"); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const { getErrorMessage } = require("./error_helpers.cjs"); | ||||||||||||||||||||||||||||||||||||||||
| const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); | ||||||||||||||||||||||||||||||||||||||||
| const { AGENT_OUTPUT_FILENAME, TMP_GH_AW_PATH } = require("./constants.cjs"); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Parse a run ID from a run URL or plain run ID string. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * Accepts: | ||||||||||||||||||||||||||||||||||||||||
| * - A plain run ID: "23560193313" | ||||||||||||||||||||||||||||||||||||||||
| * - A full run URL: "https://github.com/{owner}/{repo}/actions/runs/{runId}" | ||||||||||||||||||||||||||||||||||||||||
| * - A run URL with job: "https://github.com/{owner}/{repo}/actions/runs/{runId}/job/{jobId}" | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * @param {string} runUrl - The run URL or run ID to parse | ||||||||||||||||||||||||||||||||||||||||
| * @returns {{ runId: string, owner: string|null, repo: string|null }} Parsed components | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| function parseRunUrl(runUrl) { | ||||||||||||||||||||||||||||||||||||||||
| if (!runUrl || typeof runUrl !== "string") { | ||||||||||||||||||||||||||||||||||||||||
| throw new Error(`${ERR_VALIDATION}: run_url is required`); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const trimmed = runUrl.trim(); | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| const trimmed = runUrl.trim(); | |
| const trimmed = runUrl.trim(); | |
| if (trimmed.length === 0) { | |
| throw new Error(`${ERR_VALIDATION}: run_url is required`); | |
| } |
Copilot
AI
Mar 25, 2026
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.
The run URL parsing regex is not anchored and will match any string containing github.com/<owner>/<repo>/actions/runs/<id> (e.g. https://example.com/github.com/...). To avoid accepting malformed inputs, parse with new URL(...) (or anchor the regex) and require hostname === 'github.com' (and the expected path structure).
| const match = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)/); | |
| if (match) { | |
| return { runId: match[3], owner: match[1], repo: match[2] }; | |
| try { | |
| const url = new URL(trimmed); | |
| // Ensure we only accept real GitHub URLs | |
| if (url.hostname !== "github.com") { | |
| throw new Error("Not a github.com URL"); | |
| } | |
| // Extract owner, repo, and run ID from the pathname | |
| // Anchored pattern: /{owner}/{repo}/actions/runs/{runId}[/job/{jobId}][/]? | |
| const pathMatch = url.pathname.match(/^\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)(?:\/job\/[^/]+)?\/?$/); | |
| if (pathMatch) { | |
| return { runId: pathMatch[3], owner: pathMatch[1], repo: pathMatch[2] }; | |
| } | |
| } catch { | |
| // Fall through to unified error below |
Copilot
AI
Mar 25, 2026
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.
downloadAgentArtifact downloads into a shared directory (/tmp/gh-aw). If a previous file is already present and a run's artifact is missing agent_output.json, the stale file could be mistakenly reused because only existsSync is checked. Consider downloading into a unique temp directory per replay (e.g. fs.mkdtempSync) or deleting any preexisting agent_output.json in destDir before running gh run download.
Copilot
AI
Mar 25, 2026
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.
Auto-generating GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG as { type: {} } enables handlers but loses the original per-handler policy config (e.g. allowed_files, protected_files_policy, label/title constraints). For code-push handlers, an empty config defaults to allowing all files (see actions/setup/js/manifest_file_helpers.cjs:174-200), which can make a replay more permissive than the original run. Consider storing the original handler config in the artifact and replaying it, or failing closed for policy-sensitive handlers unless an explicit config override is provided.
Uh oh!
There was an error while loading. Please reload this page.