diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml
index d308a754633..d2e1f05d0e1 100644
--- a/.github/workflows/agentics-maintenance.yml
+++ b/.github/workflows/agentics-maintenance.yml
@@ -48,6 +48,12 @@ on:
- 'enable'
- 'update'
- 'upgrade'
+ - 'safe_outputs'
+ run_url:
+ description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
+ required: false
+ type: string
+ default: ''
permissions: {}
@@ -100,7 +106,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 != '' && github.event.inputs.operation != 'safe_outputs' && !github.event.repository.fork }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -150,6 +156,51 @@ jobs:
const { main } = require('${{ runner.temp }}/gh-aw/actions/run_operation_update_upgrade.cjs');
await main();
+ apply_safe_outputs:
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'safe_outputs' && !github.event.repository.fork }}
+ 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
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+ - name: Apply Safe Outputs
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_AW_RUN_URL: ${{ github.event.inputs.run_url }}
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/apply_safe_outputs_replay.cjs');
+ await main();
+
compile-workflows:
if: ${{ !github.event.repository.fork && (github.event_name != 'workflow_dispatch' || github.event.inputs.operation == '') }}
runs-on: ubuntu-slim
diff --git a/actions/setup/js/apply_safe_outputs_replay.cjs b/actions/setup/js/apply_safe_outputs_replay.cjs
new file mode 100644
index 00000000000..b00b0ac871c
--- /dev/null
+++ b/actions/setup/js/apply_safe_outputs_replay.cjs
@@ -0,0 +1,184 @@
+// @ts-check
+///
+
+/**
+ * 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();
+
+ // Check if it's a plain run ID (digits only)
+ if (/^\d+$/.test(trimmed)) {
+ return { runId: trimmed, owner: null, repo: null };
+ }
+
+ // Parse a full GitHub Actions URL
+ // Pattern: https://github.com/{owner}/{repo}/actions/runs/{runId}[/job/{jobId}]
+ const match = trimmed.match(/github\.com\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)/);
+ if (match) {
+ return { runId: match[3], owner: match[1], repo: match[2] };
+ }
+
+ throw new Error(`${ERR_VALIDATION}: Cannot parse run ID from: ${trimmed}. Expected a plain run ID (digits only) or a GitHub Actions run URL (https://github.com/{owner}/{repo}/actions/runs/{runId}).`);
+}
+
+/**
+ * Download the agent artifact from a workflow run using `gh run download`.
+ *
+ * @param {string} runId - The workflow run ID
+ * @param {string} destDir - Destination directory for the downloaded artifact
+ * @param {string|null} repoSlug - Optional repository slug (owner/repo)
+ * @returns {Promise} Path to the downloaded agent_output.json file
+ */
+async function downloadAgentArtifact(runId, destDir, repoSlug) {
+ core.info(`Downloading agent artifact from run ${runId}...`);
+
+ fs.mkdirSync(destDir, { recursive: true });
+
+ const args = ["run", "download", runId, "--name", "agent", "--dir", destDir];
+ if (repoSlug) {
+ args.push("--repo", repoSlug);
+ }
+
+ const exitCode = await exec.exec("gh", args);
+ if (exitCode !== 0) {
+ throw new Error(`${ERR_SYSTEM}: Failed to download agent artifact from run ${runId}`);
+ }
+
+ const outputFile = path.join(destDir, AGENT_OUTPUT_FILENAME);
+ if (!fs.existsSync(outputFile)) {
+ throw new Error(`${ERR_SYSTEM}: Agent output file not found at ${outputFile} after download. Ensure run ${runId} has an "agent" artifact containing ${AGENT_OUTPUT_FILENAME}.`);
+ }
+
+ core.info(`✓ Agent artifact downloaded to ${outputFile}`);
+ return outputFile;
+}
+
+/**
+ * Build a handler config from the items present in the agent output file.
+ * Each item type found in the output is enabled (with an empty config object).
+ *
+ * @param {string} agentOutputFile - Path to the agent_output.json file
+ * @returns {Object} Handler config keyed by normalized type name
+ */
+function buildHandlerConfigFromOutput(agentOutputFile) {
+ const content = fs.readFileSync(agentOutputFile, "utf8");
+ const validatedOutput = JSON.parse(content);
+
+ if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) {
+ core.info("No items found in agent output; handler config will be empty");
+ return {};
+ }
+
+ const config = {};
+ for (const item of validatedOutput.items) {
+ if (item.type && typeof item.type === "string") {
+ // Normalize type: convert dashes to underscores (mirrors safe_outputs_append.cjs)
+ const normalizedType = item.type.replace(/-/g, "_");
+ config[normalizedType] = {};
+ }
+ }
+
+ core.info(`Handler config built from ${validatedOutput.items.length} item(s): ${Object.keys(config).join(", ")}`);
+ return config;
+}
+
+/**
+ * Download the agent artifact from a previous run and apply the safe outputs.
+ *
+ * @returns {Promise}
+ */
+async function main() {
+ const runUrl = process.env.GH_AW_RUN_URL;
+ if (!runUrl) {
+ core.setFailed(`${ERR_CONFIG}: GH_AW_RUN_URL environment variable is required but not set`);
+ return;
+ }
+
+ core.info(`Applying safe outputs from run: ${runUrl}`);
+
+ // Parse run ID and optional owner/repo from the URL
+ let runId, owner, repo;
+ try {
+ ({ runId, owner, repo } = parseRunUrl(runUrl));
+ } catch (error) {
+ core.setFailed(getErrorMessage(error));
+ return;
+ }
+
+ core.info(`Parsed run ID: ${runId}`);
+
+ // Determine repo slug: prefer URL-parsed value, fall back to current repo context
+ const repoSlug = owner && repo ? `${owner}/${repo}` : `${context.repo.owner}/${context.repo.repo}`;
+ core.info(`Target repository: ${repoSlug}`);
+
+ // Download the agent artifact into /tmp/gh-aw/
+ const destDir = TMP_GH_AW_PATH;
+ let agentOutputFile;
+ try {
+ agentOutputFile = await downloadAgentArtifact(runId, destDir, owner && repo ? repoSlug : null);
+ } catch (error) {
+ core.setFailed(getErrorMessage(error));
+ return;
+ }
+
+ // Set GH_AW_AGENT_OUTPUT so the handler manager can find the output file
+ process.env.GH_AW_AGENT_OUTPUT = agentOutputFile;
+ core.info(`Set GH_AW_AGENT_OUTPUT=${agentOutputFile}`);
+
+ // Auto-build GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG from the output if not already set
+ if (!process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG) {
+ try {
+ const handlerConfig = buildHandlerConfigFromOutput(agentOutputFile);
+ process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG = JSON.stringify(handlerConfig);
+ core.info("Auto-configured GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG from agent output");
+ } catch (error) {
+ core.setFailed(`Failed to build handler config: ${getErrorMessage(error)}`);
+ return;
+ }
+ }
+
+ // Apply safe outputs via the handler manager
+ core.info("Applying safe outputs...");
+ const { main: runHandlerManager } = require("./safe_output_handler_manager.cjs");
+ await runHandlerManager();
+}
+
+module.exports = { main, parseRunUrl, buildHandlerConfigFromOutput };
diff --git a/actions/setup/js/apply_safe_outputs_replay.test.cjs b/actions/setup/js/apply_safe_outputs_replay.test.cjs
new file mode 100644
index 00000000000..6202f8eba6e
--- /dev/null
+++ b/actions/setup/js/apply_safe_outputs_replay.test.cjs
@@ -0,0 +1,195 @@
+// @ts-check
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+
+/** Environment variables managed by tests */
+const TEST_ENV_VARS = ["GH_AW_RUN_URL", "GH_TOKEN", "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG", "GH_AW_AGENT_OUTPUT"];
+
+describe("apply_safe_outputs_replay", () => {
+ let originalEnv;
+ let originalGlobals;
+
+ beforeEach(() => {
+ originalEnv = { ...process.env };
+
+ originalGlobals = {
+ core: global.core,
+ github: global.github,
+ context: global.context,
+ exec: global.exec,
+ };
+
+ global.core = {
+ info: vi.fn(),
+ warning: vi.fn(),
+ error: vi.fn(),
+ setFailed: vi.fn(),
+ setOutput: vi.fn(),
+ };
+
+ global.github = {};
+
+ global.context = {
+ repo: {
+ owner: "testowner",
+ repo: "testrepo",
+ },
+ };
+
+ global.exec = {
+ exec: vi.fn().mockResolvedValue(0),
+ getExecOutput: vi.fn(),
+ };
+
+ // Clear managed env vars
+ for (const key of TEST_ENV_VARS) {
+ delete process.env[key];
+ }
+ });
+
+ afterEach(() => {
+ for (const key of TEST_ENV_VARS) {
+ if (originalEnv[key] !== undefined) {
+ process.env[key] = originalEnv[key];
+ } else {
+ delete process.env[key];
+ }
+ }
+
+ global.core = originalGlobals.core;
+ global.github = originalGlobals.github;
+ global.context = originalGlobals.context;
+ global.exec = originalGlobals.exec;
+
+ vi.clearAllMocks();
+ });
+
+ describe("parseRunUrl", () => {
+ it("parses a plain run ID", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ const result = parseRunUrl("23560193313");
+ expect(result.runId, "should parse run ID").toBe("23560193313");
+ expect(result.owner, "owner should be null for plain ID").toBeNull();
+ expect(result.repo, "repo should be null for plain ID").toBeNull();
+ });
+
+ it("parses a full GitHub Actions run URL", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ const result = parseRunUrl("https://github.com/github/gh-aw/actions/runs/23560193313");
+ expect(result.runId, "should parse run ID from URL").toBe("23560193313");
+ expect(result.owner, "should parse owner").toBe("github");
+ expect(result.repo, "should parse repo").toBe("gh-aw");
+ });
+
+ it("parses a run URL that includes a job ID", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ const result = parseRunUrl("https://github.com/github/gh-aw/actions/runs/23560193313/job/68600993738");
+ expect(result.runId, "should parse run ID ignoring job ID").toBe("23560193313");
+ expect(result.owner, "should parse owner").toBe("github");
+ expect(result.repo, "should parse repo").toBe("gh-aw");
+ });
+
+ it("trims whitespace from the input", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ const result = parseRunUrl(" 23560193313 ");
+ expect(result.runId, "should trim and parse run ID").toBe("23560193313");
+ });
+
+ it("throws for an empty string", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ expect(() => parseRunUrl(""), "should throw for empty string").toThrow(/run_url is required/);
+ });
+
+ it("throws for an invalid URL format", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ expect(() => parseRunUrl("not-a-valid-url"), "should throw for invalid format").toThrow(/Cannot parse run ID/);
+ });
+
+ it("throws for a URL without a run ID", async () => {
+ const { parseRunUrl } = await import("./apply_safe_outputs_replay.cjs");
+ expect(() => parseRunUrl("https://github.com/owner/repo/actions"), "should throw for URL without run ID").toThrow(/Cannot parse run ID/);
+ });
+ });
+
+ describe("buildHandlerConfigFromOutput", () => {
+ it("builds config from agent output items", async () => {
+ const fs = require("fs");
+ const os = require("os");
+ const path = require("path");
+ const { buildHandlerConfigFromOutput } = await import("./apply_safe_outputs_replay.cjs");
+
+ const tmpFile = path.join(os.tmpdir(), `test-agent-output-${Date.now()}.json`);
+ const agentOutput = {
+ items: [
+ { type: "create_issue", title: "Test issue" },
+ { type: "add_comment", body: "Hello" },
+ { type: "create_issue", title: "Duplicate type" },
+ ],
+ };
+ fs.writeFileSync(tmpFile, JSON.stringify(agentOutput));
+
+ try {
+ const config = buildHandlerConfigFromOutput(tmpFile);
+ expect(Object.keys(config), "should include create_issue").toContain("create_issue");
+ expect(Object.keys(config), "should include add_comment").toContain("add_comment");
+ expect(Object.keys(config).length, "should deduplicate types").toBe(2);
+ expect(config.create_issue, "config value should be empty object").toEqual({});
+ } finally {
+ fs.unlinkSync(tmpFile);
+ }
+ });
+
+ it("normalizes dashes to underscores in type names", async () => {
+ const fs = require("fs");
+ const os = require("os");
+ const path = require("path");
+ const { buildHandlerConfigFromOutput } = await import("./apply_safe_outputs_replay.cjs");
+
+ const tmpFile = path.join(os.tmpdir(), `test-agent-output-${Date.now()}.json`);
+ const agentOutput = {
+ items: [{ type: "push-to-pull-request-branch", branch: "main" }],
+ };
+ fs.writeFileSync(tmpFile, JSON.stringify(agentOutput));
+
+ try {
+ const config = buildHandlerConfigFromOutput(tmpFile);
+ expect(Object.keys(config), "should normalize dashes to underscores").toContain("push_to_pull_request_branch");
+ } finally {
+ fs.unlinkSync(tmpFile);
+ }
+ });
+
+ it("returns empty config for output with no items", async () => {
+ const fs = require("fs");
+ const os = require("os");
+ const path = require("path");
+ const { buildHandlerConfigFromOutput } = await import("./apply_safe_outputs_replay.cjs");
+
+ const tmpFile = path.join(os.tmpdir(), `test-agent-output-${Date.now()}.json`);
+ fs.writeFileSync(tmpFile, JSON.stringify({ items: [] }));
+
+ try {
+ const config = buildHandlerConfigFromOutput(tmpFile);
+ expect(Object.keys(config).length, "config should be empty").toBe(0);
+ } finally {
+ fs.unlinkSync(tmpFile);
+ }
+ });
+
+ it("returns empty config when items array is missing", async () => {
+ const fs = require("fs");
+ const os = require("os");
+ const path = require("path");
+ const { buildHandlerConfigFromOutput } = await import("./apply_safe_outputs_replay.cjs");
+
+ const tmpFile = path.join(os.tmpdir(), `test-agent-output-${Date.now()}.json`);
+ fs.writeFileSync(tmpFile, JSON.stringify({}));
+
+ try {
+ const config = buildHandlerConfigFromOutput(tmpFile);
+ expect(Object.keys(config).length, "config should be empty for missing items").toBe(0);
+ } finally {
+ fs.unlinkSync(tmpFile);
+ }
+ });
+ });
+});
diff --git a/pkg/workflow/maintenance_workflow.go b/pkg/workflow/maintenance_workflow.go
index 8591e758424..48a1d81f89c 100644
--- a/pkg/workflow/maintenance_workflow.go
+++ b/pkg/workflow/maintenance_workflow.go
@@ -199,6 +199,12 @@ on:
- 'enable'
- 'update'
- 'upgrade'
+ - 'safe_outputs'
+ run_url:
+ description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.'
+ required: false
+ type: string
+ default: ''
permissions: {}
@@ -274,10 +280,10 @@ jobs:
await main();
`)
- // Add unified run_operation job for all dispatch operations
+ // Add unified run_operation job for all dispatch operations except safe_outputs
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 != '' && github.event.inputs.operation != 'safe_outputs' && !github.event.repository.fork }}
runs-on: ubuntu-slim
permissions:
actions: write
@@ -322,6 +328,54 @@ jobs:
await main();
`)
+ // Add apply_safe_outputs job for workflow_dispatch with operation == 'safe_outputs'
+ yaml.WriteString(`
+ apply_safe_outputs:
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'safe_outputs' && !github.event.repository.fork }}
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: write
+ discussions: write
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Checkout actions folder
+ uses: ` + GetActionPin("actions/checkout") + `
+ with:
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+
+ - name: Setup Scripts
+ uses: ` + setupActionRef + `
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+
+ - name: Check admin/maintainer permissions
+ uses: ` + GetActionPin("actions/github-script") + `
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs');
+ await main();
+
+ - name: Apply Safe Outputs
+ uses: ` + GetActionPin("actions/github-script") + `
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_AW_RUN_URL: ${{ github.event.inputs.run_url }}
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/apply_safe_outputs_replay.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
diff --git a/pkg/workflow/maintenance_workflow_test.go b/pkg/workflow/maintenance_workflow_test.go
index 2d97f9182a6..f7ddc36398d 100644
--- a/pkg/workflow/maintenance_workflow_test.go
+++ b/pkg/workflow/maintenance_workflow_test.go
@@ -281,7 +281,8 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
yaml := string(content)
operationSkipCondition := `github.event_name != 'workflow_dispatch' || github.event.inputs.operation == ''`
- operationRunCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation != ''`
+ operationRunCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation != '' && github.event.inputs.operation != 'safe_outputs'`
+ applySafeOutputsCondition := `github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'safe_outputs'`
const jobSectionSearchRange = 300
const runOpSectionSearchRange = 200
@@ -303,6 +304,7 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
}
// run_operation job should NOT have the skip condition but should have its own activation condition
+ // and should exclude safe_outputs
runOpIdx := strings.Index(yaml, "\n run_operation:")
if runOpIdx == -1 {
t.Errorf("Job run_operation not found in generated workflow")
@@ -315,6 +317,27 @@ func TestGenerateMaintenanceWorkflow_OperationJobConditions(t *testing.T) {
t.Errorf("Job run_operation should have the activation condition %q", operationRunCondition)
}
}
+
+ // apply_safe_outputs job should be triggered when operation == 'safe_outputs'
+ applyIdx := strings.Index(yaml, "\n apply_safe_outputs:")
+ if applyIdx == -1 {
+ t.Errorf("Job apply_safe_outputs not found in generated workflow")
+ } else {
+ applySection := yaml[applyIdx : applyIdx+runOpSectionSearchRange]
+ if !strings.Contains(applySection, applySafeOutputsCondition) {
+ t.Errorf("Job apply_safe_outputs should have the activation condition %q in:\n%s", applySafeOutputsCondition, applySection)
+ }
+ }
+
+ // Verify safe_outputs is an option in the operation choices
+ if !strings.Contains(yaml, "- 'safe_outputs'") {
+ t.Error("workflow_dispatch operation choices should include 'safe_outputs'")
+ }
+
+ // Verify run_url input exists in workflow_dispatch
+ if !strings.Contains(yaml, "run_url:") {
+ t.Error("workflow_dispatch should include run_url input")
+ }
}
func TestGenerateMaintenanceWorkflow_ActionTag(t *testing.T) {