From 86ca33f2d91d93afe5bf611e1cb8d5c06f95ba82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:28:32 +0000 Subject: [PATCH 01/16] Add replace-label safe-outputs type for atomic label state transitions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .changeset/replace-label.md | 14 + actions/setup/js/replace_label.cjs | 347 ++++++++++++++++++ actions/setup/js/replace_label.test.cjs | 256 +++++++++++++ pkg/parser/schemas/main_workflow_schema.json | 114 +++++- pkg/workflow/compiler_types.go | 1 + pkg/workflow/replace_label.go | 34 ++ pkg/workflow/safe_output_handlers.go | 12 + .../safe_output_validation_config_test.go | 1 + pkg/workflow/safe_outputs_config.go | 6 + pkg/workflow/safe_outputs_handler_registry.go | 26 ++ pkg/workflow/safe_outputs_max_validation.go | 5 + pkg/workflow/safe_outputs_state.go | 4 +- .../safe_outputs_tools_computation.go | 4 + .../safe_outputs_tools_repo_params.go | 7 +- pkg/workflow/safe_outputs_validation.go | 3 + .../safe_outputs_validation_config.go | 9 + pkg/workflow/tool_description_enhancer.go | 16 + pkg/workflow/unified_prompt_step.go | 3 + 18 files changed, 858 insertions(+), 4 deletions(-) create mode 100644 .changeset/replace-label.md create mode 100644 actions/setup/js/replace_label.cjs create mode 100644 actions/setup/js/replace_label.test.cjs create mode 100644 pkg/workflow/replace_label.go diff --git a/.changeset/replace-label.md b/.changeset/replace-label.md new file mode 100644 index 00000000000..baa003abc54 --- /dev/null +++ b/.changeset/replace-label.md @@ -0,0 +1,14 @@ +"gh-aw": minor + +Add replace-label safe-outputs type for atomic label state transitions + +The new `replace-label` safe output type removes one label and adds another in a single atomic GraphQL operation. It is designed for label-based state machines where removing a label transitions an issue or PR to the next state (e.g. `in-progress` → `done`). + +Key capabilities: +- `allowed-add`: optional list of label patterns permitted as the target label +- `allowed-remove`: optional list of label patterns permitted as the source label +- `blocked`: label patterns rejected for both add and remove operations +- Inherits `target`, `target-repo`, `allowed-repos`, `required-labels`, `required-title-prefix`, `staged`, `max`, and `github-token` from the shared config infrastructure +- Uses a combined GraphQL mutation (`removeLabelsFromLabelable` + `addLabelsToLabelable`) for a single-request state transition +- Automatically creates the target label in the repository if it does not already exist +- Gracefully handles the case where the label to remove is not present on the item (still adds the new label) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs new file mode 100644 index 00000000000..a01ee91b41b --- /dev/null +++ b/actions/setup/js/replace_label.cjs @@ -0,0 +1,347 @@ +// @ts-check +/// + +/** + * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction + * @typedef {import('./types/handler-factory').ResolvedTemporaryIds} ResolvedTemporaryIds + * @typedef {import('./types/handler-factory').HandlerResult} HandlerResult + */ + +/** + * @typedef {{ + * item_number?: number|string, + * issue_number?: number|string, + * pr_number?: number|string, + * pull_number?: number|string, + * label_to_remove: string, + * label_to_add: string, + * repo?: string + * }} ReplaceLabelMessage + */ + +/** @type {string} Safe output type handled by this module */ +const HANDLER_TYPE = "replace_label"; + +/** + * GraphQL mutation that removes one label and adds another in a single request. + * Root mutations in a single request are executed sequentially (remove first, then add), + * providing an atomic state transition for label-based state machines. + * + * @type {string} + */ +const REPLACE_LABEL_MUTATION = /* GraphQL */ ` + mutation ReplaceLabelMutation($labelableId: ID!, $addLabelIds: [ID!]!, $removeLabelIds: [ID!]!) { + removeLabels: removeLabelsFromLabelable(input: { labelableId: $labelableId, labelIds: $removeLabelIds }) { + clientMutationId + } + addLabels: addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $addLabelIds }) { + labelable { + ... on Issue { + labels(first: 25) { + nodes { + name + } + } + } + ... on PullRequest { + labels(first: 25) { + nodes { + name + } + } + } + } + } + } +`; + +/** + * GraphQL query to resolve label node IDs from a repository by name. + * Searches for labels matching the given names so we can get their node IDs + * for the mutation. + * + * @type {string} + */ +const RESOLVE_LABEL_QUERY = /* GraphQL */ ` + query ResolveLabelNodeIds($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + labels(first: 100) { + nodes { + id + name + } + } + } + } +`; + +const { validateLabels } = require("./safe_output_validator.cjs"); +const { getErrorMessage } = require("./error_helpers.cjs"); +const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); +const { logStagedPreviewInfo } = require("./staged_preview.cjs"); +const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); +const { resolveSafeOutputIssueTarget } = require("./temporary_id.cjs"); +const { attachExecutionState, fetchIssueState, normalizeLabelNames } = require("./safe_output_execution_metadata.cjs"); +const { createCountGatedHandler } = require("./handler_scaffold.cjs"); +const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); +const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); + +/** + * Resolve or create a label in the repository, returning its GraphQL node ID. + * If the label does not exist, it is created with a deterministic pastel color. + * + * @param {any} githubClient - Authenticated GitHub client with REST and graphql + * @param {string} owner - Repository owner + * @param {string} repo - Repository name + * @param {string} labelName - Label name to resolve or create + * @param {Map} labelNodeIdCache - Cache of label name → node_id + * @returns {Promise} The GraphQL node ID of the label + */ +async function resolveOrCreateLabel(githubClient, owner, repo, labelName, labelNodeIdCache) { + if (labelNodeIdCache.has(labelName)) { + return /** @type {string} */ labelNodeIdCache.get(labelName); + } + + // Try to get the label from the repo + try { + const { data: label } = await githubClient.rest.issues.getLabel({ + owner, + repo, + name: labelName, + }); + labelNodeIdCache.set(labelName, label.node_id); + return label.node_id; + } catch (err) { + const msg = getErrorMessage(err); + if (!msg.includes("404") && !msg.toLowerCase().includes("not found")) { + throw err; + } + } + + // Label does not exist — create it with a deterministic color + core.info(`Label "${labelName}" not found in ${owner}/${repo}, creating it`); + const color = deterministicLabelColor(labelName); + const { data: created } = await githubClient.rest.issues.createLabel({ + owner, + repo, + name: labelName, + color, + }); + core.info(`Created label "${labelName}" with color #${color}`); + labelNodeIdCache.set(labelName, created.node_id); + return created.node_id; +} + +/** + * Generate a deterministic pastel hex color from a label name. + * Produces colors in the pastel range (128–191 per channel) for readability. + * + * @param {string} name + * @returns {string} Six-character hex color (no leading #) + */ +function deterministicLabelColor(name) { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = (hash * 31 + name.charCodeAt(i)) >>> 0; + } + const r = 128 + (hash & 0x3f); + const g = 128 + ((hash >> 6) & 0x3f); + const b = 128 + ((hash >> 12) & 0x3f); + return ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0"); +} + +/** + * Main handler factory for replace_label. + * Uses the GraphQL API to remove one label and add another in a single atomic request. + * @type {HandlerFactoryFunction} + */ +const main = createCountGatedHandler({ + handlerType: HANDLER_TYPE, + setup: async (config, maxCount, isStaged) => { + const blockedPatterns = config.blocked || []; + const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : []; + const requiredTitlePrefix = config.required_title_prefix || ""; + const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); + const githubClient = await createAuthenticatedGitHubClient(config); + + // Config keys use snake_case (set by the Go handler config builder) + const configAllowedAdd = Array.isArray(config.allowed_add) ? config.allowed_add : []; + const configAllowedRemove = Array.isArray(config.allowed_remove) ? config.allowed_remove : []; + + core.info(`Replace label configuration: max=${maxCount}`); + if (configAllowedAdd.length > 0) core.info(`Allowed labels to add: ${configAllowedAdd.join(", ")}`); + if (configAllowedRemove.length > 0) core.info(`Allowed labels to remove: ${configAllowedRemove.join(", ")}`); + if (blockedPatterns.length > 0) core.info(`Blocked patterns: ${blockedPatterns.join(", ")}`); + if (requiredLabels.length > 0) core.info(`Required labels (all): ${requiredLabels.join(", ")}`); + if (requiredTitlePrefix) core.info(`Required title prefix: ${requiredTitlePrefix}`); + core.info(`Default target repo: ${defaultTargetRepo}`); + if (allowedRepos.size > 0) core.info(`Allowed repos: ${[...allowedRepos].join(", ")}`); + + /** Cache of repo label name → node_id, keyed per repo to avoid cross-repo conflicts */ + /** @type {Map>} */ + const repoCaches = new Map(); + + /** + * Message handler function that processes a single replace_label message. + * @param {ReplaceLabelMessage} message - The replace_label message to process + * @param {ResolvedTemporaryIds} resolvedTemporaryIds - Map of temporary IDs to {repo, number} + * @returns {Promise} Result with success/error status + */ + return async function handleReplaceLabel(message, resolvedTemporaryIds) { + // Resolve and validate target repository + const repoResult = resolveAndValidateRepo(message, defaultTargetRepo, allowedRepos, "label"); + if (!repoResult.success) { + core.warning(`Skipping replace_label: ${repoResult.error}`); + return { success: false, error: repoResult.error }; + } + const { repo: itemRepo, repoParts } = repoResult; + core.info(`Target repository: ${itemRepo}`); + + // Determine target issue/PR number + const targetResult = resolveSafeOutputIssueTarget({ message, resolvedTemporaryIds, repoParts, handlerType: HANDLER_TYPE }); + if (!targetResult.success) return targetResult; + const effectiveContext = resolveInvocationContext(context); + const itemNumber = targetResult.number ?? effectiveContext.eventPayload?.issue?.number ?? effectiveContext.eventPayload?.pull_request?.number; + + if (!itemNumber || Number.isNaN(Number(itemNumber))) { + const error = "No issue/PR number available"; + core.warning(error); + return { success: false, error }; + } + + const contextType = effectiveContext.eventPayload?.pull_request ? "pull request" : "issue"; + const labelToRemove = String(message.label_to_remove ?? "").trim(); + const labelToAdd = String(message.label_to_add ?? "").trim(); + + core.info(`Requested label replacement for ${contextType} #${itemNumber}: "${labelToRemove}" → "${labelToAdd}"`); + + if (!labelToRemove || !labelToAdd) { + const error = "Both label_to_remove and label_to_add must be provided and non-empty"; + core.warning(error); + return { success: false, error }; + } + + // Validate label_to_remove against allowed-remove and blocked patterns + const removeValidation = validateLabels([labelToRemove], configAllowedRemove, 1, blockedPatterns); + if (!removeValidation.valid) { + core.warning(`label_to_remove validation failed: ${removeValidation.error}`); + return { success: false, error: removeValidation.error ?? "Invalid label_to_remove" }; + } + + // Validate label_to_add against allowed-add and blocked patterns + const addValidation = validateLabels([labelToAdd], configAllowedAdd, 1, blockedPatterns); + if (!addValidation.valid) { + core.warning(`label_to_add validation failed: ${addValidation.error}`); + return { success: false, error: addValidation.error ?? "Invalid label_to_add" }; + } + + // Apply required-labels and required-title-prefix filters + const { data: item } = await githubClient.rest.issues.get({ + owner: repoParts.owner, + repo: repoParts.repo, + issue_number: itemNumber, + }); + + if (requiredLabels.length > 0) { + const itemLabels = (item.labels || []).map(/** @param {any} l */ l => (typeof l === "string" ? l : l.name || "")); + if (!requiredLabels.every(r => itemLabels.includes(r))) { + core.info(`Skipping replace_label for ${contextType} #${itemNumber}: does not match required-labels filter (${requiredLabels.join(", ")})`); + return { success: false, skipped: true, error: "Item does not match required-labels filter" }; + } + } + if (requiredTitlePrefix && !item.title?.startsWith(requiredTitlePrefix)) { + core.info(`Skipping replace_label for ${contextType} #${itemNumber}: title does not start with required prefix "${requiredTitlePrefix}"`); + return { success: false, skipped: true, error: "Item title does not start with required prefix" }; + } + + // If in staged mode, preview the replacement without applying it + if (isStaged) { + logStagedPreviewInfo(`Would replace label "${labelToRemove}" → "${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); + return { + success: true, + staged: true, + previewInfo: { + number: itemNumber, + repo: itemRepo, + labelToRemove, + labelToAdd, + contextType, + }, + }; + } + + // Get or initialize the per-repo label cache + if (!repoCaches.has(itemRepo)) { + repoCaches.set(itemRepo, new Map()); + } + const labelNodeIdCache = /** @type {Map} */ repoCaches.get(itemRepo); + + // Resolve the node ID of label_to_add (create if it doesn't exist in the repo) + let addLabelNodeId; + try { + addLabelNodeId = await withRetry(() => resolveOrCreateLabel(githubClient, repoParts.owner, repoParts.repo, labelToAdd, labelNodeIdCache), RATE_LIMIT_RETRY_CONFIG, `resolve/create label "${labelToAdd}" in ${itemRepo}`); + } catch (err) { + const errorMessage = getErrorMessage(err); + core.error(`Failed to resolve/create label "${labelToAdd}": ${errorMessage}`); + return { success: false, error: `Failed to resolve/create label "${labelToAdd}": ${errorMessage}` }; + } + + // Find the node ID of label_to_remove from the issue's current labels. + // If the label is not on the issue we can still proceed (just won't remove anything). + const currentLabelMap = new Map((item.labels || []).map(/** @param {any} l */ l => [l.name || "", l.node_id || ""])); + const removeLabelNodeId = currentLabelMap.get(labelToRemove); + + if (!removeLabelNodeId) { + core.info(`Label "${labelToRemove}" is not present on ${contextType} #${itemNumber} in ${itemRepo} — will only add "${labelToAdd}"`); + } + + // Issue node_id for the GraphQL mutation + const labelableId = item.node_id; + + core.info(`Executing combined GraphQL mutation: remove="${labelToRemove}", add="${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); + + const beforeState = await fetchIssueState(githubClient, repoParts, itemNumber); + + try { + const mutationResult = await withRetry( + () => + githubClient.graphql(REPLACE_LABEL_MUTATION, { + labelableId, + addLabelIds: [addLabelNodeId], + removeLabelIds: removeLabelNodeId ? [removeLabelNodeId] : [], + }), + RATE_LIMIT_RETRY_CONFIG, + `replace_label on ${contextType} #${itemNumber} in ${itemRepo}` + ); + + const updatedLabels = mutationResult?.addLabels?.labelable?.labels?.nodes || []; + const updatedLabelNames = updatedLabels.map((/** @param {any} l */ l) => l.name || "").filter(Boolean); + + core.info(`Successfully replaced label "${labelToRemove}" → "${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); + core.info(`Updated labels: ${JSON.stringify(updatedLabelNames)}`); + + return attachExecutionState( + { + success: true, + number: itemNumber, + repo: itemRepo, + labelRemoved: removeLabelNodeId ? labelToRemove : null, + labelAdded: labelToAdd, + contextType, + }, + beforeState, + { + ...beforeState, + labels: updatedLabelNames.length > 0 ? updatedLabelNames : normalizeLabelNames(item.labels), + } + ); + } catch (err) { + const errorMessage = getErrorMessage(err); + core.error(`Failed to replace label: ${errorMessage}`); + return { success: false, error: errorMessage }; + } + }; + }, +}); + +module.exports = { main, deterministicLabelColor, REPLACE_LABEL_MUTATION, RESOLVE_LABEL_QUERY }; diff --git a/actions/setup/js/replace_label.test.cjs b/actions/setup/js/replace_label.test.cjs new file mode 100644 index 00000000000..fe42cd047ba --- /dev/null +++ b/actions/setup/js/replace_label.test.cjs @@ -0,0 +1,256 @@ +// @ts-check +import { describe, it, expect, beforeEach, vi } from "vitest"; +const { main, deterministicLabelColor } = require("./replace_label.cjs"); + +describe("replace_label", () => { + let mockCore; + let mockGithub; + let mockContext; + + beforeEach(() => { + mockCore = { + info: () => {}, + warning: () => {}, + error: () => {}, + debug: () => {}, + messages: [], + infos: [], + warnings: [], + errors: [], + }; + + mockCore.info = msg => { + mockCore.infos.push(msg); + mockCore.messages.push({ level: "info", message: msg }); + }; + mockCore.warning = msg => { + mockCore.warnings.push(msg); + mockCore.messages.push({ level: "warning", message: msg }); + }; + mockCore.error = msg => { + mockCore.errors.push(msg); + mockCore.messages.push({ level: "error", message: msg }); + }; + + mockGithub = { + rest: { + issues: { + get: async () => ({ + data: { + title: "Test issue title", + labels: [ + { name: "in-progress", node_id: "LA_in_progress_123" }, + { name: "bug", node_id: "LA_bug_456" }, + ], + node_id: "I_issue_789", + }, + }), + getLabel: async ({ name }) => { + const labels = { + done: { name: "done", node_id: "LA_done_789", color: "0075ca" }, + "in-progress": { name: "in-progress", node_id: "LA_in_progress_123", color: "e4e669" }, + }; + if (labels[name]) { + return { data: labels[name] }; + } + const err = new Error("Not Found"); + err.status = 404; + throw err; + }, + createLabel: async ({ name, color }) => ({ + data: { name, node_id: `LA_${name}_new`, color }, + }), + }, + }, + graphql: async (mutation, variables) => { + if (mutation.includes("ReplaceLabelMutation")) { + return { + removeLabels: { clientMutationId: null }, + addLabels: { + labelable: { + labels: { + nodes: [{ name: "done" }], + }, + }, + }, + }; + } + throw new Error("Unknown query"); + }, + }; + + mockContext = { + repo: { + owner: "test-owner", + repo: "test-repo", + }, + payload: { + issue: { + number: 42, + }, + }, + }; + + global.core = mockCore; + global.github = mockGithub; + global.context = mockContext; + }); + + it("should replace label when both labels are valid", async () => { + const handler = await main({ allowed_add: [], allowed_remove: [], blocked: [] }); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(true); + expect(result.labelRemoved).toBe("in-progress"); + expect(result.labelAdded).toBe("done"); + }); + + it("should create label_to_add if it does not exist in the repo", async () => { + let createLabelCalled = false; + mockGithub.rest.issues.getLabel = async ({ name }) => { + const err = new Error("Not Found"); + err.status = 404; + throw err; // All labels "not found" + }; + mockGithub.rest.issues.createLabel = async ({ name, color }) => { + createLabelCalled = true; + return { data: { name, node_id: `LA_${name}_created`, color } }; + }; + + const handler = await main({}); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "needs-review" }, {}); + + expect(result.success).toBe(true); + expect(createLabelCalled).toBe(true); + }); + + it("should succeed even when label_to_remove is not present on the issue", async () => { + mockGithub.rest.issues.get = async () => ({ + data: { + title: "Test issue", + labels: [{ name: "bug", node_id: "LA_bug_456" }], // "in-progress" not present + node_id: "I_issue_789", + }, + }); + + const handler = await main({}); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(true); + expect(result.labelRemoved).toBeNull(); + expect(result.labelAdded).toBe("done"); + }); + + it("should return error when label_to_remove is missing", async () => { + const handler = await main({}); + // @ts-ignore - testing missing field + const result = await handler({ label_to_add: "done" }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain("label_to_remove"); + }); + + it("should return error when label_to_add is missing", async () => { + const handler = await main({}); + // @ts-ignore - testing missing field + const result = await handler({ label_to_remove: "in-progress" }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain("label_to_add"); + }); + + it("should reject label_to_add that is not in allowed-add list", async () => { + const handler = await main({ allowed_add: ["approved", "done"] }, 1, false, github, context); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "wontfix" }, {}); + + expect(result.success).toBe(false); + }); + + it("should reject label_to_remove that is not in allowed-remove list", async () => { + const handler = await main({ allowed_remove: ["in-progress", "review-needed"] }, 1, false, github, context); + const result = await handler({ label_to_remove: "bug", label_to_add: "done" }, {}); + + expect(result.success).toBe(false); + }); + + it("should reject labels matching blocked patterns", async () => { + const handler = await main({ blocked: ["~*"] }, 1, false, github, context); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "~internal" }, {}); + + expect(result.success).toBe(false); + }); + + it("should skip when required-labels filter does not match", async () => { + const handler = await main({ required_labels: ["approved"] }, 1, false, github, context); + // Issue has "in-progress" and "bug" but not "approved" + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(false); + expect(result.skipped).toBe(true); + }); + + it("should skip when required-title-prefix does not match", async () => { + const handler = await main({ required_title_prefix: "[BUG]" }, 1, false, github, context); + // Issue title is "Test issue title", does not start with "[BUG]" + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(false); + expect(result.skipped).toBe(true); + }); + + it("should return staged result when in staged mode", async () => { + const handler = await main({ staged: true }); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(true); + expect(result.staged).toBe(true); + expect(result.previewInfo?.labelToRemove).toBe("in-progress"); + expect(result.previewInfo?.labelToAdd).toBe("done"); + }); + + it("should return error when no item number is available", async () => { + global.context = { + repo: { owner: "test-owner", repo: "test-repo" }, + payload: {}, // no issue or pull_request in payload + }; + + const handler = await main({}); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(false); + }); +}); + +describe("deterministicLabelColor", () => { + it("should return a 6-char hex string", () => { + const color = deterministicLabelColor("done"); + expect(color).toMatch(/^[0-9a-f]{6}$/); + }); + + it("should return different colors for different labels", () => { + const c1 = deterministicLabelColor("done"); + const c2 = deterministicLabelColor("in-progress"); + expect(c1).not.toBe(c2); + }); + + it("should return the same color for the same label", () => { + const c1 = deterministicLabelColor("needs-review"); + const c2 = deterministicLabelColor("needs-review"); + expect(c1).toBe(c2); + }); + + it("should return pastel colors (128-191 per channel)", () => { + for (const name of ["done", "in-progress", "approved", "needs-review", "blocked"]) { + const hex = deterministicLabelColor(name); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + expect(r).toBeGreaterThanOrEqual(128); + expect(r).toBeLessThanOrEqual(191); + expect(g).toBeGreaterThanOrEqual(128); + expect(g).toBeLessThanOrEqual(191); + expect(b).toBeGreaterThanOrEqual(128); + expect(b).toBeLessThanOrEqual(191); + } + }); +}); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index d8a83d0f8f1..a76ba8c5968 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -3716,7 +3716,7 @@ "type": "integer", "minimum": 1, "default": 5, - "description": "Maximum number of consecutive AWF cache misses allowed before the API proxy blocks further requests. Maps to `apiProxy.maxCacheMisses`. Precedence: frontmatter value → `GH_AW_DEFAULT_MAX_TURN_CACHE_MISSES` env override → built-in default `5`." + "description": "Maximum number of consecutive AWF cache misses allowed before the API proxy blocks further requests. Maps to `apiProxy.maxCacheMisses`. Precedence: frontmatter value \u2192 `GH_AW_DEFAULT_MAX_TURN_CACHE_MISSES` env override \u2192 built-in default `5`." }, "max-daily-ai-credits": { "$ref": "#/$defs/max_daily_ai_credits_limit", @@ -10235,7 +10235,7 @@ }, "allowed-teams": { "type": "array", - "description": "List of team slugs whose members are always allowed to be mentioned. Accepts 'team-slug' (resolved against the current org) or 'org/team-slug' format. Team members are fetched from the GitHub API at runtime; bots are excluded. IMPORTANT: requires read:org scope — not available with the default GITHUB_TOKEN. Use a classic PAT with read:org, a fine-grained PAT with Members:Read, or a GitHub App with the Members:Read permission. Without the required scope, team lookups fail with a warning and those members are skipped.", + "description": "List of team slugs whose members are always allowed to be mentioned. Accepts 'team-slug' (resolved against the current org) or 'org/team-slug' format. Team members are fetched from the GitHub API at runtime; bots are excluded. IMPORTANT: requires read:org scope \u2014 not available with the default GITHUB_TOKEN. Use a classic PAT with read:org, a fine-grained PAT with Members:Read, or a GitHub App with the Members:Read permission. Without the required scope, team lookups fail with a warning and those members are skipped.", "items": { "type": "string", "minLength": 1 @@ -10540,6 +10540,116 @@ } ], "description": "Enable AI agents to signal that a task could not be completed due to infrastructure or tool failures (e.g., MCP crash, missing auth, inaccessible repository). Activates failure handling even when the agent exits 0." + }, + "replace-label": { + "oneOf": [ + { + "type": "null", + "description": "Null configuration allows any labels to be replaced. Labels will be created if they don't already exist in the repository." + }, + { + "type": "object", + "description": "Configuration for replacing one label with another on issues/PRs in a single atomic operation. Enables clear state transitions (e.g. 'in-progress' \u2192 'done').", + "properties": { + "allowed-add": { + "type": "array", + "description": "Optional list of allowed label patterns that can be added (supports glob patterns like 'state-*'). Labels will be created if they don't already exist in the repository. If omitted, any labels are allowed.", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 50 + }, + "allowed-remove": { + "type": "array", + "description": "Optional list of allowed label patterns that can be removed (supports glob patterns like 'state-*'). If omitted, any labels can be removed.", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 50 + }, + "blocked": { + "type": "array", + "description": "Optional list of blocked label patterns (supports glob patterns like '~*', '*[bot]'). Applied to both label_to_add and label_to_remove. Rejected before allowed list filtering.", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 50 + }, + "max": { + "description": "Optional maximum number of label replacements (default: 5). Supports integer or GitHub Actions expression (e.g. '${{ inputs.max }}').", + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^\\$\\{\\{.*\\}\\}$", + "description": "GitHub Actions expression that resolves to an integer at runtime" + } + ] + }, + "target": { + "type": "string", + "description": "Target for the operation: 'triggering' (default), '*' (any issue/PR), or explicit issue/PR number" + }, + "target-repo": { + "type": "string", + "description": "Target repository in format 'owner/repo' for cross-repository label replacement. Takes precedence over trial target repo settings." + }, + "github-token": { + "$ref": "#/$defs/github_token", + "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." + }, + "allowed-repos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of additional repositories in format 'owner/repo' that label replacements can target. When specified, the agent can use a 'repo' field in the output to specify which repository to update labels on." + }, + "required-labels": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 50, + "description": "Conjunctive label constraint: ALL of these labels must be present on the issue/PR for the operation to proceed." + }, + "required-title-prefix": { + "type": "string", + "description": "Title prefix constraint: the issue/PR title must start with this prefix for the operation to proceed." + }, + "staged": { + "type": "boolean", + "description": "If true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", + "examples": [true, false] + }, + "samples": { + "description": "Internal hidden feature. Optional list of declarative sample payloads that exercise this safe-output handler.", + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + { + "type": "object", + "additionalProperties": true + } + ] + } + }, + "additionalProperties": false + } + ], + "description": "Enable AI agents to replace one label with another on GitHub issues or pull requests in a single atomic operation. Ideal for maintaining label-based state machines (e.g. transitioning issues through workflow states)." } }, "additionalProperties": false diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 64e0208e0b1..e08fc457af3 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -688,6 +688,7 @@ type SafeOutputsConfig struct { CreateCheckRun *CreateCheckRunConfig `yaml:"create-check-run,omitempty"` // Create GitHub Check Runs to report agent analysis results AddLabels *AddLabelsConfig `yaml:"add-labels,omitempty"` RemoveLabels *RemoveLabelsConfig `yaml:"remove-labels,omitempty"` + ReplaceLabel *ReplaceLabelConfig `yaml:"replace-label,omitempty"` // Replace one label with another in a single atomic operation AddReviewer *AddReviewerConfig `yaml:"add-reviewer,omitempty"` AssignMilestone *AssignMilestoneConfig `yaml:"assign-milestone,omitempty"` AssignToAgent *AssignToAgentConfig `yaml:"assign-to-agent,omitempty"` diff --git a/pkg/workflow/replace_label.go b/pkg/workflow/replace_label.go new file mode 100644 index 00000000000..9f9ec8b7762 --- /dev/null +++ b/pkg/workflow/replace_label.go @@ -0,0 +1,34 @@ +package workflow + +import ( + "github.com/github/gh-aw/pkg/logger" +) + +var replaceLabelLog = logger.New("workflow:replace_label") + +// ReplaceLabelConfig holds configuration for replacing one label with another on issues/PRs from agent output. +// It combines the capabilities of add-labels and remove-labels into a single atomic GraphQL operation, +// enabling clear state transitions (e.g. "in-progress" → "done"). +type ReplaceLabelConfig struct { + BaseSafeOutputConfig `yaml:",inline"` + SafeOutputTargetConfig `yaml:",inline"` + SafeOutputFilterConfig `yaml:",inline"` + AllowedAdd []string `yaml:"allowed-add,omitempty"` // Optional list of allowed label patterns that can be added (supports glob patterns like "state-*"). If omitted, any labels are allowed. + AllowedRemove []string `yaml:"allowed-remove,omitempty"` // Optional list of allowed label patterns that can be removed (supports glob patterns like "state-*"). If omitted, any labels can be removed. + Blocked []string `yaml:"blocked,omitempty"` // Optional list of blocked label patterns (supports glob patterns like "~*", "*[bot]"). Applied to both add and remove labels. +} + +// parseReplaceLabelConfig handles replace-label configuration +func (c *Compiler) parseReplaceLabelConfig(outputMap map[string]any) *ReplaceLabelConfig { + config := parseConfigScaffold(outputMap, "replace-label", replaceLabelLog, func(err error) *ReplaceLabelConfig { + replaceLabelLog.Printf("Failed to unmarshal config: %v", err) + // Handle null case: create empty config (allows any labels) + replaceLabelLog.Print("Using empty configuration (allows any labels)") + return &ReplaceLabelConfig{} + }) + if config != nil { + replaceLabelLog.Printf("Parsed configuration: allowed_add_count=%d, allowed_remove_count=%d, blocked_count=%d, target=%s", + len(config.AllowedAdd), len(config.AllowedRemove), len(config.Blocked), config.Target) + } + return config +} diff --git a/pkg/workflow/safe_output_handlers.go b/pkg/workflow/safe_output_handlers.go index 05a68ab8ddf..f4b81f66dc3 100644 --- a/pkg/workflow/safe_output_handlers.go +++ b/pkg/workflow/safe_output_handlers.go @@ -277,6 +277,18 @@ var safeOutputHandlers = []safeOutputHandlerDescriptor{ return NewPermissionsContentsReadIssuesWritePRWrite() }, }, + { + Key: "replace-label", + StructField: "ReplaceLabel", + ToolName: "replace_label", + NewConfig: func() any { return &ReplaceLabelConfig{} }, + PermissionBuilder: func(safeOutputs *SafeOutputsConfig) *Permissions { + if !isSafeOutputHandlerEnabledAndUnstaged(safeOutputs, "ReplaceLabel") { + return nil + } + return NewPermissionsContentsReadIssuesWritePRWrite() + }, + }, { Key: "add-reviewer", StructField: "AddReviewer", diff --git a/pkg/workflow/safe_output_validation_config_test.go b/pkg/workflow/safe_output_validation_config_test.go index 44f57586982..5dd9d65f310 100644 --- a/pkg/workflow/safe_output_validation_config_test.go +++ b/pkg/workflow/safe_output_validation_config_test.go @@ -49,6 +49,7 @@ func TestGetValidationConfigJSON(t *testing.T) { "link_sub_issue", "update_discussion", "remove_labels", + "replace_label", "unassign_from_user", "hide_comment", "missing_data", diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 25e7e1c50d0..475df12b9fe 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -227,6 +227,12 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut config.RemoveLabels = removeLabelsConfig } + // Parse replace-label configuration + replaceLabelConfig := c.parseReplaceLabelConfig(outputMap) + if replaceLabelConfig != nil { + config.ReplaceLabel = replaceLabelConfig + } + // Parse add-reviewer configuration addReviewerConfig := c.parseAddReviewerConfig(outputMap) if addReviewerConfig != nil { diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index 42fe9c824c2..0d8bb2774b5 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -169,6 +169,32 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfTrue("staged", c.Staged). Build() }, + "replace_label": func(cfg *SafeOutputsConfig) map[string]any { + if cfg.ReplaceLabel == nil { + return nil + } + c := cfg.ReplaceLabel + config := newHandlerConfigBuilder(). + AddTemplatableInt("max", c.Max). + AddStringSlice("allowed_add", c.AllowedAdd). + AddStringSlice("allowed_remove", c.AllowedRemove). + AddStringSlice("blocked", c.Blocked). + AddIfNotEmpty("target", c.Target). + AddIfNotEmpty("target-repo", c.TargetRepoSlug). + AddStringSlice("allowed_repos", c.AllowedRepos). + AddStringSlice("required_labels", c.RequiredLabels). + AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). + AddIfNotEmpty("github-token", c.GitHubToken). + AddIfTrue("staged", c.Staged). + Build() + // If config is empty, it means replace_label was explicitly configured with no options + // (null config), which means "allow any labels". Return non-nil empty map to + // indicate the handler is enabled. + if len(config) == 0 { + return make(map[string]any) + } + return config + }, "add_reviewer": func(cfg *SafeOutputsConfig) map[string]any { if cfg.AddReviewer == nil { return nil diff --git a/pkg/workflow/safe_outputs_max_validation.go b/pkg/workflow/safe_outputs_max_validation.go index ec86ac9bc19..bb0db31d406 100644 --- a/pkg/workflow/safe_outputs_max_validation.go +++ b/pkg/workflow/safe_outputs_max_validation.go @@ -214,6 +214,11 @@ func validateSafeOutputsMax(config *SafeOutputsConfig) error { return err } } + if config.ReplaceLabel != nil { + if err := checkMaxField("replace_label", config.ReplaceLabel.Max); err != nil { + return err + } + } if config.ReplyToPullRequestReviewComment != nil { if err := checkMaxField("reply_to_pull_request_review_comment", config.ReplyToPullRequestReviewComment.Max); err != nil { return err diff --git a/pkg/workflow/safe_outputs_state.go b/pkg/workflow/safe_outputs_state.go index ee5b7929e29..478adf9238e 100644 --- a/pkg/workflow/safe_outputs_state.go +++ b/pkg/workflow/safe_outputs_state.go @@ -61,6 +61,7 @@ func hasAnySafeOutputEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.CreateCheckRun != nil || safeOutputs.AddLabels != nil || safeOutputs.RemoveLabels != nil || + safeOutputs.ReplaceLabel != nil || safeOutputs.AddReviewer != nil || safeOutputs.AssignMilestone != nil || safeOutputs.AssignToAgent != nil || @@ -85,7 +86,7 @@ func hasAnySafeOutputEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.MissingData != nil || safeOutputs.SetIssueType != nil || safeOutputs.SetIssueField != nil || - safeOutputs.NoOp != nil // 44th field + safeOutputs.NoOp != nil // 45th field } // The builtin types (noop, missing-data, missing-tool) are excluded from this check @@ -126,6 +127,7 @@ func hasNonBuiltinSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.CreateCheckRun != nil || safeOutputs.AddLabels != nil || safeOutputs.RemoveLabels != nil || + safeOutputs.ReplaceLabel != nil || safeOutputs.AddReviewer != nil || safeOutputs.AssignMilestone != nil || safeOutputs.AssignToAgent != nil || diff --git a/pkg/workflow/safe_outputs_tools_computation.go b/pkg/workflow/safe_outputs_tools_computation.go index 9b5b0570c6c..4d640cec2a1 100644 --- a/pkg/workflow/safe_outputs_tools_computation.go +++ b/pkg/workflow/safe_outputs_tools_computation.go @@ -92,6 +92,10 @@ func computeEnabledToolNames(data *WorkflowData) map[string]struct { enabledTools["remove_labels"] = struct { }{} } + if data.SafeOutputs.ReplaceLabel != nil { + enabledTools["replace_label"] = struct { + }{} + } if data.SafeOutputs.AddReviewer != nil { enabledTools["add_reviewer"] = struct { }{} diff --git a/pkg/workflow/safe_outputs_tools_repo_params.go b/pkg/workflow/safe_outputs_tools_repo_params.go index 471d113f620..e41254da6ff 100644 --- a/pkg/workflow/safe_outputs_tools_repo_params.go +++ b/pkg/workflow/safe_outputs_tools_repo_params.go @@ -79,7 +79,7 @@ func addRepoParameterIfNeeded(tool map[string]any, toolName string, safeOutputs hasAllowedRepos = len(config.AllowedRepos) > 0 targetRepoSlug = config.TargetRepoSlug } - case "add_labels", "remove_labels", "hide_comment", "link_sub_issue", "mark_pull_request_as_ready_for_review", + case "add_labels", "remove_labels", "replace_label", "hide_comment", "link_sub_issue", "mark_pull_request_as_ready_for_review", "add_reviewer", "assign_milestone", "assign_to_agent", "assign_to_user", "unassign_from_user", "set_issue_type", "set_issue_field": // These use SafeOutputTargetConfig - check the appropriate config @@ -94,6 +94,11 @@ func addRepoParameterIfNeeded(tool map[string]any, toolName string, safeOutputs hasAllowedRepos = len(config.AllowedRepos) > 0 targetRepoSlug = config.TargetRepoSlug } + case "replace_label": + if config := safeOutputs.ReplaceLabel; config != nil { + hasAllowedRepos = len(config.AllowedRepos) > 0 + targetRepoSlug = config.TargetRepoSlug + } case "hide_comment": if config := safeOutputs.HideComment; config != nil { hasAllowedRepos = len(config.AllowedRepos) > 0 diff --git a/pkg/workflow/safe_outputs_validation.go b/pkg/workflow/safe_outputs_validation.go index f5504194d4c..aac2865c889 100644 --- a/pkg/workflow/safe_outputs_validation.go +++ b/pkg/workflow/safe_outputs_validation.go @@ -117,6 +117,9 @@ func validateSafeOutputsTarget(config *SafeOutputsConfig) error { if config.RemoveLabels != nil { configs = append(configs, targetConfig{"remove-labels", config.RemoveLabels.Target}) } + if config.ReplaceLabel != nil { + configs = append(configs, targetConfig{"replace-label", config.ReplaceLabel.Target}) + } if config.AddReviewer != nil { configs = append(configs, targetConfig{"add-reviewer", config.AddReviewer.Target}) } diff --git a/pkg/workflow/safe_outputs_validation_config.go b/pkg/workflow/safe_outputs_validation_config.go index b94f26d3a4f..c87a7099288 100644 --- a/pkg/workflow/safe_outputs_validation_config.go +++ b/pkg/workflow/safe_outputs_validation_config.go @@ -392,6 +392,15 @@ var ValidationConfig = map[string]TypeValidationConfig{ "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" }, }, + "replace_label": { + DefaultMax: 5, + Fields: map[string]FieldValidation{ + "label_to_remove": {Required: true, Type: "string", Sanitize: true, MaxLength: 128}, + "label_to_add": {Required: true, Type: "string", Sanitize: true, MaxLength: 128}, + "item_number": {IssueNumberOrTemporaryID: true}, + "repo": {Type: "string", MaxLength: 256}, // Optional: target repository in format "owner/repo" + }, + }, "unassign_from_user": { DefaultMax: 1, Fields: map[string]FieldValidation{ diff --git a/pkg/workflow/tool_description_enhancer.go b/pkg/workflow/tool_description_enhancer.go index fb9e2d011a2..2f336e69a6a 100644 --- a/pkg/workflow/tool_description_enhancer.go +++ b/pkg/workflow/tool_description_enhancer.go @@ -338,6 +338,22 @@ func enhanceToolDescription(toolName, baseDescription string, safeOutputs *SafeO } } + case "replace_label": + if config := safeOutputs.ReplaceLabel; config != nil { + if templatableIntValue(config.Max) > 0 { + constraints = append(constraints, fmt.Sprintf("Maximum %d label replacement(s) allowed.", templatableIntValue(config.Max))) + } + if len(config.AllowedAdd) > 0 { + constraints = append(constraints, fmt.Sprintf("Only these labels can be added: %s.", formatStringList(config.AllowedAdd))) + } + if len(config.AllowedRemove) > 0 { + constraints = append(constraints, fmt.Sprintf("Only these labels can be removed: %v.", config.AllowedRemove)) + } + if config.Target != "" { + constraints = append(constraints, fmt.Sprintf("Target: %s.", config.Target)) + } + } + case "add_reviewer": if config := safeOutputs.AddReviewer; config != nil { if templatableIntValue(config.Max) > 0 { diff --git a/pkg/workflow/unified_prompt_step.go b/pkg/workflow/unified_prompt_step.go index a47f9795c34..c657aed9c02 100644 --- a/pkg/workflow/unified_prompt_step.go +++ b/pkg/workflow/unified_prompt_step.go @@ -600,6 +600,9 @@ func buildSafeOutputsSections(safeOutputs *SafeOutputsConfig) []PromptSection { if safeOutputs.RemoveLabels != nil { tools = append(tools, toolWithMaxBudget("remove_labels", safeOutputs.RemoveLabels.Max)) } + if safeOutputs.ReplaceLabel != nil { + tools = append(tools, toolWithMaxBudget("replace_label", safeOutputs.ReplaceLabel.Max)) + } if safeOutputs.AddReviewer != nil { tools = append(tools, toolWithMaxBudget("add_reviewer", safeOutputs.AddReviewer.Max)) } From 07b92ca6ccd555303628f240df59aa354cc7fe11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:32:09 +0000 Subject: [PATCH 02/16] Fix code review issues: formatStringList for AllowedRemove, fix test call signatures Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.test.cjs | 10 +++++----- pkg/workflow/tool_description_enhancer.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/replace_label.test.cjs b/actions/setup/js/replace_label.test.cjs index fe42cd047ba..0ef339cb3a8 100644 --- a/actions/setup/js/replace_label.test.cjs +++ b/actions/setup/js/replace_label.test.cjs @@ -160,28 +160,28 @@ describe("replace_label", () => { }); it("should reject label_to_add that is not in allowed-add list", async () => { - const handler = await main({ allowed_add: ["approved", "done"] }, 1, false, github, context); + const handler = await main({ allowed_add: ["approved", "done"] }); const result = await handler({ label_to_remove: "in-progress", label_to_add: "wontfix" }, {}); expect(result.success).toBe(false); }); it("should reject label_to_remove that is not in allowed-remove list", async () => { - const handler = await main({ allowed_remove: ["in-progress", "review-needed"] }, 1, false, github, context); + const handler = await main({ allowed_remove: ["in-progress", "review-needed"] }); const result = await handler({ label_to_remove: "bug", label_to_add: "done" }, {}); expect(result.success).toBe(false); }); it("should reject labels matching blocked patterns", async () => { - const handler = await main({ blocked: ["~*"] }, 1, false, github, context); + const handler = await main({ blocked: ["~*"] }); const result = await handler({ label_to_remove: "in-progress", label_to_add: "~internal" }, {}); expect(result.success).toBe(false); }); it("should skip when required-labels filter does not match", async () => { - const handler = await main({ required_labels: ["approved"] }, 1, false, github, context); + const handler = await main({ required_labels: ["approved"] }); // Issue has "in-progress" and "bug" but not "approved" const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); @@ -190,7 +190,7 @@ describe("replace_label", () => { }); it("should skip when required-title-prefix does not match", async () => { - const handler = await main({ required_title_prefix: "[BUG]" }, 1, false, github, context); + const handler = await main({ required_title_prefix: "[BUG]" }); // Issue title is "Test issue title", does not start with "[BUG]" const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); diff --git a/pkg/workflow/tool_description_enhancer.go b/pkg/workflow/tool_description_enhancer.go index 2f336e69a6a..764ebcae396 100644 --- a/pkg/workflow/tool_description_enhancer.go +++ b/pkg/workflow/tool_description_enhancer.go @@ -347,7 +347,7 @@ func enhanceToolDescription(toolName, baseDescription string, safeOutputs *SafeO constraints = append(constraints, fmt.Sprintf("Only these labels can be added: %s.", formatStringList(config.AllowedAdd))) } if len(config.AllowedRemove) > 0 { - constraints = append(constraints, fmt.Sprintf("Only these labels can be removed: %v.", config.AllowedRemove)) + constraints = append(constraints, fmt.Sprintf("Only these labels can be removed: %s.", formatStringList(config.AllowedRemove))) } if config.Target != "" { constraints = append(constraints, fmt.Sprintf("Target: %s.", config.Target)) From 6c9162888455de2e515de5e8b70b67751ec88415 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:33:35 +0000 Subject: [PATCH 03/16] Remove stale field-count comment in safe_outputs_state.go Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/safe_outputs_state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/safe_outputs_state.go b/pkg/workflow/safe_outputs_state.go index 478adf9238e..7962bf13fb9 100644 --- a/pkg/workflow/safe_outputs_state.go +++ b/pkg/workflow/safe_outputs_state.go @@ -86,7 +86,7 @@ func hasAnySafeOutputEnabled(safeOutputs *SafeOutputsConfig) bool { safeOutputs.MissingData != nil || safeOutputs.SetIssueType != nil || safeOutputs.SetIssueField != nil || - safeOutputs.NoOp != nil // 45th field + safeOutputs.NoOp != nil } // The builtin types (noop, missing-data, missing-tool) are excluded from this check From 357e5a0f42646096c178362eabc977ce65efe2f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:13:09 +0000 Subject: [PATCH 04/16] docs: add replace-label W3C specification Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- specs/replace-label-spec.md | 849 ++++++++++++++++++++++++++++++++++++ 1 file changed, 849 insertions(+) create mode 100644 specs/replace-label-spec.md diff --git a/specs/replace-label-spec.md b/specs/replace-label-spec.md new file mode 100644 index 00000000000..1464f03023e --- /dev/null +++ b/specs/replace-label-spec.md @@ -0,0 +1,849 @@ +--- +title: replace-label Safe-Output Type Specification +description: Formal W3C-style specification for the replace-label safe-output type in GitHub Agentic Workflows +sidebar: + order: 1005 +--- + +# replace-label Safe-Output Type Specification + +**Version**: 1.0.0 +**Status**: Candidate Recommendation +**Latest Version**: https://github.com/github/gh-aw/blob/main/specs/replace-label-spec.md +**Editors**: GitHub gh-aw Team (GitHub, Inc.) +**Publication Date**: 2026-06-20 + +--- + +## Abstract + +This specification defines the `replace-label` safe-output type for GitHub Agentic Workflows (gh-aw), a mechanism that enables AI agents to atomically transition label state on GitHub issues and pull requests. The `replace-label` type removes one label and adds another in a single GraphQL request, eliminating the race window that would otherwise exist when using `remove-labels` and `add-labels` as separate sequential operations. + +The specification covers the configuration schema, the message schema produced by AI agents, the multi-stage validation pipeline, the label-resolution and auto-creation mechanism, the GraphQL mutation executed against the GitHub API, error-handling requirements, security controls, and conformance testing requirements. + +## Status of This Document + +This is a Candidate Recommendation specification representing the design and implementation of the `replace-label` safe-output type as shipped in gh-aw version 1.0.0. This specification is subject to updates based on implementation feedback, operational experience, and security research. Future versions may introduce additional configuration options or refine validation semantics. + +**Governance**: This specification is maintained by the GitHub gh-aw Team and governed by GitHub's research and engineering processes. Feedback and errata should be submitted as issues to the `github/gh-aw` repository. + +--- + +## Table of Contents + +1. [Introduction](#1-introduction) +2. [Conformance](#2-conformance) +3. [Concepts and Terminology](#3-concepts-and-terminology) +4. [Data Model](#4-data-model) +5. [Processing Model](#5-processing-model) +6. [GraphQL Interface](#6-graphql-interface) +7. [Error Handling](#7-error-handling) +8. [Security Considerations](#8-security-considerations) +9. [Compliance Testing](#9-compliance-testing) +10. [Examples](#10-examples) +11. [References](#references) +12. [Change Log](#change-log) + +--- + +## 1. Introduction + +### 1.1 Purpose + +Label-based state machines are a common pattern in GitHub issue and pull request workflows. A triage issue may progress from `pending` → `in-review` → `approved`, or a PR review cycle may move from `needs-revision` → `ready-to-merge`. When these transitions are driven by AI agents operating through gh-aw, the canonical implementation using separate `remove-labels` and `add-labels` safe-output messages introduces a race window: between the removal and the addition, the item carries no label — or may be picked up by a concurrent automation that considers the intermediate label-less state valid. + +The `replace-label` type solves this by combining the remove and add operations into a single GraphQL request. The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order, guaranteeing that the removal is applied before the addition and that no external observer can witness the intermediate state within the same request. + +### 1.2 Scope + +This specification covers: + +- The YAML configuration schema for the `replace-label` key within the `safe-outputs` frontmatter block +- The JSON message schema produced by AI agents when requesting a label replacement +- The multi-stage processing pipeline: schema validation, label allowlist/blocklist enforcement, required-label and title-prefix gate checks, staged-mode preview, label node-ID resolution and auto-creation, and GraphQL mutation execution +- The exact GraphQL mutation used against the GitHub API +- Rate-limit retry semantics and error propagation +- Security controls, including cross-repository access restrictions +- Conformance requirements and test procedures + +This specification does NOT cover: + +- The `add-labels` safe-output type (separate type with different semantics) +- The `remove-labels` safe-output type (separate type with different semantics) +- Label management outside the context of gh-aw safe outputs (e.g., repository label administration) +- The gh-aw compilation pipeline (defined separately) +- GitHub Actions platform security guarantees +- AI agent prompt engineering or model behavior + +### 1.3 Design Goals + +The `replace-label` type is designed to satisfy the following goals: + +1. **Atomicity**: Remove and add operations MUST execute in a single GitHub API round-trip to eliminate the observable intermediate state. +2. **Idempotency on missing source label**: If the label to be removed is not present on the target item, the operation MUST still add the new label and succeed, rather than failing. +3. **Least privilege**: Allowlist-based configuration MUST constrain which labels agents may add or remove, limiting the blast radius of a misbehaving agent. +4. **Label hygiene**: If the label to be added does not yet exist in the repository, the handler MUST auto-create it with a deterministic pastel color rather than failing, avoiding manual label-setup requirements. +5. **Safe preview**: Staged mode MUST allow operators to review what the agent would do without applying any changes to the GitHub API. +6. **Consistency with the safe-outputs framework**: Configuration fields (`max`, `target`, `target-repo`, `allowed-repos`, `github-token`, `staged`, `required-labels`, `required-title-prefix`) MUST follow the same semantics as all other safe-output types in gh-aw. + +--- + +## 2. Conformance + +### 2.1 Conformance Classes + +This specification defines a single conformance class: a **conforming replace-label implementation**. A conforming implementation MUST satisfy all normative requirements (MUST/SHALL) defined in this specification. Optional features (SHOULD/MAY) are not required for conformance but are RECOMMENDED for production use. + +### 2.2 Requirements Notation + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). + +Normative requirements are additionally identified by a short requirement code of the form **RL-NNN** (e.g., **RL-001**). These codes are the stable identifiers used in the compliance testing section. + +### 2.3 Compliance Levels + +| Level | Description | Requirements | +|-------|-------------|--------------| +| **Level 1 — Core** | Minimum viable replace-label support | All MUST/SHALL requirements | +| **Level 2 — Production** | Production-ready deployment | All Level 1 requirements + all SHOULD requirements | + +--- + +## 3. Concepts and Terminology + +**Label replacement**: The atomic operation of removing one named label from a GitHub labelable item and adding a different named label in the same GitHub GraphQL API request. + +**Labelable**: A GitHub resource that can carry labels. In this specification, labelable items are GitHub Issues and GitHub Pull Requests. The GitHub GraphQL type `Labelable` is the interface implemented by both. + +**Label node ID**: The globally unique GraphQL node ID assigned by GitHub to each label object within a repository (e.g., `LA_kwDOABCD123`). Node IDs are required by the GraphQL mutation API and differ from the integer REST API label ID. + +**Label allowlist** (`allowed-add`, `allowed-remove`): Optional glob pattern lists in the workflow configuration that constrain which labels an agent may add or remove. When absent, no label-name restriction applies. + +**Label blocklist** (`blocked`): Optional glob pattern list in the workflow configuration that unconditionally prohibits specific labels from being added or removed, applied after allowlist checks. + +**Staged mode**: A preview mode in which the handler logs what it would do but makes no GitHub API calls. Activated by setting `staged: true` in the configuration. + +**Triggering item**: The GitHub issue or pull request that caused the workflow to execute, identified from the GitHub Actions event context (`github.event.issue.number` or `github.event.pull_request.number`). + +**Temporary ID**: A placeholder identifier used in agent messages when the target item number is not yet known (e.g., a newly created issue whose number is resolved by a prior safe-output handler). Temporary ID resolution is governed by the common gh-aw temporary-ID framework. + +**Deterministic pastel color**: A six-character hex color code derived from a label name using a deterministic hash function, producing values in the pastel range (128–191 per RGB channel) for visual readability. + +**Count gate**: The mechanism that tracks how many `replace-label` operations have been executed during a single workflow run and enforces the configured `max` limit. + +--- + +## 4. Data Model + +### 4.1 Configuration Schema + +The `replace-label` type is configured under the `safe-outputs` key in a gh-aw workflow frontmatter block. All fields are optional unless otherwise noted. + +```yaml +safe-outputs: + replace-label: + allowed-add: ["approved", "done"] # Glob patterns for labels that may be added + allowed-remove: ["in-review", "pending"] # Glob patterns for labels that may be removed + blocked: ["~*", "*[bot]"] # Glob patterns blocked for both add and remove + max: 5 # Max replacements per run (default 5) + target: "triggering" # "triggering" | "*" | explicit number + target-repo: "owner/repo" # Cross-repo target repository + allowed-repos: ["owner/repo"] # Allowlist for multi-repo targeting + github-token: "${{ secrets.TOKEN }}" # Per-type token override + required-labels: ["triage"] # ALL must be present on item before operation + required-title-prefix: "[Bug]" # Item title must start with this prefix + staged: false # Preview-only mode; no API calls made +``` + +#### 4.1.1 Field Definitions + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `allowed-add` | `string[]` | `[]` (any) | Glob patterns for labels the agent is permitted to add. When empty or absent, no add restriction applies. | +| `allowed-remove` | `string[]` | `[]` (any) | Glob patterns for labels the agent is permitted to remove. When empty or absent, no remove restriction applies. | +| `blocked` | `string[]` | `[]` (none) | Glob patterns that are unconditionally prohibited for both add and remove operations. Applied after allowlist checks. | +| `max` | `integer` or GHA expression | `5` | Maximum number of `replace-label` operations permitted in a single workflow run. Supports GitHub Actions expressions (e.g., `${{ inputs.max_labels }}`). | +| `target` | `"triggering"` \| `"*"` \| integer | `"triggering"` | Determines which issue/PR may be targeted. `"triggering"` restricts to the event item; `"*"` permits any item (requires `item_number` in message); an integer pins to a specific item number. | +| `target-repo` | `string` | (current repo) | Default target repository in `owner/repo` format for cross-repository operations. | +| `allowed-repos` | `string[]` | `[]` | Additional repositories the agent may target, beyond `target-repo`. | +| `github-token` | `string` | (workflow default) | GitHub token or GitHub Actions expression for authentication. Overrides the workflow-level token for this type only. | +| `required-labels` | `string[]` | `[]` | ALL listed labels must be present on the target item for the operation to proceed. Items that do not satisfy this gate are skipped (not errored). | +| `required-title-prefix` | `string` | `""` | The target item's title must begin with this prefix for the operation to proceed. Items that do not satisfy this gate are skipped (not errored). | +| `staged` | `boolean` | `false` | When `true`, the handler logs a preview of each operation but makes no GitHub API calls. | + +#### 4.1.2 Glob Pattern Semantics + +**RL-001**: Glob pattern matching for `allowed-add`, `allowed-remove`, and `blocked` MUST follow the semantics of the `gobwas/glob` library (as used elsewhere in gh-aw), where `*` matches any sequence of characters within a label name and `[...]` denotes a character class. + +**RL-002**: A label name MUST match at least one pattern in an allowlist (`allowed-add` or `allowed-remove`) when the list is non-empty. An empty or absent allowlist permits any label name. + +**RL-003**: A label name MUST NOT match any pattern in the `blocked` list. Blocklist evaluation MUST occur after allowlist evaluation. A label that passes the allowlist but matches a blocked pattern MUST be rejected. + +### 4.2 Message Schema + +AI agents emit `replace_label` messages as part of the safe-outputs protocol. The following JSON schema defines the message structure: + +```json +{ + "label_to_remove": "", + "label_to_add": "", + "item_number": "", + "repo": "" +} +``` + +#### 4.2.1 Message Field Definitions + +| Field | Type | Required | Max Length | Description | +|-------|------|----------|------------|-------------| +| `label_to_remove` | `string` | Yes | 128 characters | Name of the label to remove from the target item. The label need not currently be present on the item (see §5.3.4). | +| `label_to_add` | `string` | Yes | 128 characters | Name of the label to add to the target item. The label need not pre-exist in the repository (see §5.4). | +| `item_number` | `integer` or temporary-ID `string` | No | — | Issue or pull request number to target. When absent, falls back to the triggering item derived from the GitHub Actions event context. May be a temporary-ID string resolved by the gh-aw temporary-ID framework. | +| `repo` | `string` | No | 256 characters | Target repository in `owner/repo` format. Overrides the configured `target-repo` for this message only. Must satisfy the `allowed-repos` configuration constraint. | + +**RL-004**: A conforming implementation MUST reject any `replace_label` message in which `label_to_remove` is absent, empty after trimming, or exceeds 128 characters. + +**RL-005**: A conforming implementation MUST reject any `replace_label` message in which `label_to_add` is absent, empty after trimming, or exceeds 128 characters. + +**RL-006**: A conforming implementation MUST reject any `replace_label` message in which `repo` is present and exceeds 256 characters. + +**RL-007**: A conforming implementation MUST sanitize the string values of `label_to_remove` and `label_to_add` against the standard gh-aw safe-output string sanitization rules before use. + +#### 4.2.2 Aliased Item Number Fields + +For compatibility with agents that follow other safe-output conventions, the handler MUST also accept the following field names as aliases for `item_number`: + +- `issue_number` +- `pr_number` +- `pull_number` + +When multiple aliased fields are present in the same message, `item_number` takes precedence, followed by `issue_number`, `pr_number`, and `pull_number` in that order. + +--- + +## 5. Processing Model + +A conforming implementation MUST execute the following pipeline for each `replace_label` message. The stages are ordered; a failure at any stage MUST prevent execution of subsequent stages. + +``` + Agent Message (JSON) + │ + ▼ + ┌─────────────────────┐ + │ Stage 1 │ + │ Schema Validation │ ← RL-004 – RL-007 + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 2 │ + │ Count Gate Check │ ← max enforcement + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 3 │ + │ Target Resolution │ ← item_number, repo, target config + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 4 │ + │ Label Validation │ ← allowed-add, allowed-remove, blocked + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 5 │ + │ Gate Checks │ ← required-labels, required-title-prefix + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 6 │ + │ Staged Mode Check │ ← log preview; exit if staged: true + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 7 │ + │ Label Resolution │ ← resolve or auto-create labels + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ Stage 8 │ + │ GraphQL Mutation │ ← single atomic request + └─────────────────────┘ +``` + +### 5.1 Stage 1: Schema Validation + +**RL-008**: The implementation MUST validate each incoming message against the schema defined in §4.2 before any other processing. + +**RL-009**: Schema validation MUST be performed by the common gh-aw safe-output validation pipeline (defined in `pkg/workflow/safe_outputs_validation_config.go`) using the `replace_label` entry with `DefaultMax: 5` and the field map: + +| Field | Required | Type | Max Length | Notes | +|-------|----------|------|------------|-------| +| `label_to_remove` | Yes | `string` | 128 | Sanitized | +| `label_to_add` | Yes | `string` | 128 | Sanitized | +| `item_number` | No | issue number or temporary ID | — | — | +| `repo` | No | `string` | 256 | — | + +Messages that fail schema validation MUST be rejected with a structured error logged to the GitHub Actions step summary. The workflow run MUST NOT be marked as failed solely due to a validation rejection; the message is silently skipped with a warning. + +### 5.2 Stage 2: Count Gate Check + +**RL-010**: The implementation MUST track the number of `replace-label` operations executed in the current workflow run. When the count would exceed the configured `max` value, the message MUST be rejected with a warning and processing MUST stop for that message. + +**RL-011**: The `max` field MUST support GitHub Actions expressions (e.g., `${{ inputs.max }}`) which are resolved at workflow execution time. + +**RL-012**: The default value of `max` MUST be `5` when the field is absent from the configuration. + +### 5.3 Stage 3: Target Resolution + +#### 5.3.1 Repository Resolution + +**RL-013**: The target repository is resolved as follows, in priority order: + +1. The `repo` field in the agent message, when present. +2. The `target-repo` configuration field, when present. +3. The repository of the triggering workflow run (the default). + +**RL-014**: The resolved repository MUST be in `owner/repo` format. The implementation MUST reject messages with a malformed repository identifier. + +**RL-015**: When `allowed-repos` is non-empty, the resolved repository MUST match one of the entries in `allowed-repos`. A message targeting a repository not in this list MUST be rejected with a warning. + +#### 5.3.2 Item Number Resolution + +**RL-016**: The target item number is resolved as follows, in priority order: + +1. The item number resolved from any temporary-ID field (`item_number`, `issue_number`, `pr_number`, `pull_number`) via the gh-aw temporary-ID framework. +2. A literal numeric value from the same aliased fields. +3. The triggering issue number from `github.event.issue.number`. +4. The triggering pull request number from `github.event.pull_request.number`. + +**RL-017**: When no item number can be resolved through any of the four mechanisms above, the message MUST be rejected with the error "No issue/PR number available". + +#### 5.3.3 Target Mode Enforcement + +**RL-018**: When `target` is set to `"triggering"`, the resolved item number MUST equal the triggering item's number. A message specifying a different `item_number` MUST be rejected. + +**RL-019**: When `target` is set to an explicit integer, the resolved item number MUST equal that integer. Messages specifying a different number MUST be rejected. + +**RL-020**: When `target` is set to `"*"`, any item number is permitted, subject to repository constraints. + +### 5.4 Stage 4: Label Validation + +**RL-021**: The implementation MUST evaluate `label_to_remove` against `allowed-remove` (if non-empty) and `blocked` (if non-empty). The evaluation order is: + +1. If `allowed-remove` is non-empty, `label_to_remove` MUST match at least one pattern. Failure → reject. +2. If `blocked` is non-empty, `label_to_remove` MUST NOT match any pattern. Match → reject. + +**RL-022**: The implementation MUST evaluate `label_to_add` against `allowed-add` (if non-empty) and `blocked` (if non-empty). The evaluation order is: + +1. If `allowed-add` is non-empty, `label_to_add` MUST match at least one pattern. Failure → reject. +2. If `blocked` is non-empty, `label_to_add` MUST NOT match any pattern. Match → reject. + +**RL-023**: Label validation failures MUST be reported as warnings and the message MUST be skipped. The workflow run MUST NOT be marked as failed solely due to a label validation rejection. + +### 5.5 Stage 5: Gate Checks + +Gate checks require fetching the current state of the target item via the GitHub REST API (`GET /repos/{owner}/{repo}/issues/{issue_number}`) before proceeding. + +#### 5.5.1 Required-Labels Gate + +**RL-024**: When `required-labels` is non-empty, the implementation MUST retrieve the current labels on the target item and verify that ALL labels in `required-labels` are present. If any required label is absent, the message MUST be skipped (not failed) with an informational log entry. + +#### 5.5.2 Required-Title-Prefix Gate + +**RL-025**: When `required-title-prefix` is non-empty, the implementation MUST retrieve the title of the target item and verify that it begins with the configured prefix. If the title does not match, the message MUST be skipped (not failed) with an informational log entry. + +**RL-026**: Gate check failures MUST result in a `{ success: false, skipped: true }` handler result, distinguishing them from hard errors. + +### 5.6 Stage 6: Staged Mode + +**RL-027**: When `staged: true` is set in the configuration, the implementation MUST NOT make any GitHub API write calls for this message. Instead, the implementation MUST log a structured preview entry describing what would have been executed, including: + +- The target item number +- The target repository +- The label that would be removed (`label_to_remove`) +- The label that would be added (`label_to_add`) +- The item type (issue or pull request) + +**RL-028**: Staged mode execution MUST return `{ success: true, staged: true }` from the handler, and MUST NOT decrement any rate-limit budget or increment the operation count. + +### 5.7 Stage 7: Label Resolution and Auto-Creation + +Label resolution converts human-readable label names into the GraphQL node IDs required by the mutation. The implementation maintains a per-repository in-memory cache of `labelName → nodeId` to avoid redundant API calls within a single workflow run. + +#### 5.7.1 Resolving `label_to_add` + +**RL-029**: The implementation MUST attempt to resolve the `label_to_add` name to a GraphQL node ID using the GitHub REST API (`GET /repos/{owner}/{repo}/labels/{name}`). + +**RL-030**: When the label does not exist in the target repository (HTTP 404), the implementation MUST automatically create it using the GitHub REST API (`POST /repos/{owner}/{repo}/labels`) with: +- `name`: the exact label name from the message +- `color`: the deterministic pastel color computed by the algorithm in §5.7.3 + +**RL-031**: Label resolution and auto-creation MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy defined in `actions/setup/js/error_recovery.cjs`. + +**RL-032**: If resolution or creation fails for any reason other than a rate-limit or 404, the implementation MUST return a hard error and the message MUST be rejected. + +#### 5.7.2 Resolving `label_to_remove` + +**RL-033**: The node ID of `label_to_remove` MUST be looked up from the current labels already attached to the target item (returned by the gate-check REST call in Stage 5), rather than from a separate API call. + +**RL-034**: When `label_to_remove` is not currently attached to the target item, the implementation MUST proceed with the `add` operation only, passing an empty `removeLabelIds` array to the mutation. The operation MUST NOT fail in this case (see §1.3, Design Goal 2). + +#### 5.7.3 Deterministic Pastel Color Algorithm + +The color assigned to auto-created labels is derived from the label name using the following algorithm: + +``` +hash = 0 +for each character c in labelName: + hash = (hash * 31 + charCode(c)) unsigned-right-shifted 0 +r = 128 + (hash & 0x3F) # 128–191 +g = 128 + ((hash >> 6) & 0x3F) # 128–191 +b = 128 + ((hash >> 12) & 0x3F) # 128–191 +color = hex(r << 16 | g << 8 | b) # zero-padded to 6 characters +``` + +**RL-035**: A conforming implementation MUST use this algorithm (or one producing identical outputs) for all auto-created label colors, ensuring cross-run reproducibility. + +### 5.8 Stage 8: GraphQL Mutation + +**RL-036**: The implementation MUST execute the label replacement using the single GraphQL mutation defined in §6 rather than two separate REST or GraphQL API calls. + +**RL-037**: The mutation MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy. + +**RL-038**: On successful mutation, the implementation MUST log: +- The item type (issue or pull request), item number, and repository +- The label that was removed (or a note that the source label was absent and no removal occurred) +- The label that was added +- The complete updated label set returned by the `addLabels` mutation result + +**RL-039**: The handler result for a successful operation MUST include the fields `success: true`, `number`, `repo`, `labelRemoved` (null when the source label was absent), `labelAdded`, and `contextType`. + +**RL-040**: The handler result MUST include before-state and after-state execution metadata (via `attachExecutionState`) for observability and audit purposes. + +--- + +## 6. GraphQL Interface + +### 6.1 Mutation + +The following GraphQL mutation is the normative interface used by the `replace-label` handler to execute label replacement operations. + +```graphql +mutation ReplaceLabelMutation( + $labelableId: ID! + $addLabelIds: [ID!]! + $removeLabelIds: [ID!]! +) { + removeLabels: removeLabelsFromLabelable( + input: { labelableId: $labelableId, labelIds: $removeLabelIds } + ) { + clientMutationId + } + addLabels: addLabelsToLabelable( + input: { labelableId: $labelableId, labelIds: $addLabelIds } + ) { + labelable { + ... on Issue { + labels(first: 25) { + nodes { name } + } + } + ... on PullRequest { + labels(first: 25) { + nodes { name } + } + } + } + } +} +``` + +#### 6.1.1 Variable Bindings + +| Variable | Type | Source | +|----------|------|--------| +| `$labelableId` | `ID!` | GraphQL node ID of the target issue or pull request (`item.node_id` from REST response) | +| `$addLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_add`, as resolved in Stage 7 | +| `$removeLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_remove` when present on the item; empty array when the label is absent | + +**RL-041**: `$addLabelIds` MUST always contain exactly one element. + +**RL-042**: `$removeLabelIds` MUST contain exactly one element when the label to remove is present on the target item, and MUST be an empty array (`[]`) otherwise. + +### 6.2 Atomicity Semantics + +**RL-043**: The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order. A conforming implementation MUST rely on this sequential guarantee: `removeLabelsFromLabelable` executes before `addLabelsToLabelable` within the same request, preventing any external observer from reading the item in a state where neither label is present. + +> **Informative note**: The term "atomic" in this specification refers to the single-request delivery guarantee, not to database transaction atomicity. GitHub does not provide rollback semantics: if `removeLabels` succeeds but `addLabels` fails, the remove is not reversed. Implementations SHOULD log this partial-success condition clearly when it occurs. + +### 6.3 Label Fetch Query + +The following read-only GraphQL query is used during label cache population (where needed) to bulk-fetch label node IDs from a repository. This query is supplementary; the primary resolution path uses the REST API (§5.7.1–5.7.2). + +```graphql +query ResolveLabelNodeIds($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + labels(first: 100) { + nodes { + id + name + } + } + } +} +``` + +--- + +## 7. Error Handling + +### 7.1 Error Categories + +| Category | Code | Behavior | +|----------|------|----------| +| Schema validation failure | `SCHEMA_INVALID` | Skip message with warning; do not fail run | +| Count gate exceeded | `MAX_EXCEEDED` | Skip message with warning; do not fail run | +| Target resolution failure | `TARGET_UNRESOLVABLE` | Skip message with warning; do not fail run | +| Repository not in allowlist | `REPO_NOT_ALLOWED` | Skip message with warning; do not fail run | +| Label validation failure | `LABEL_BLOCKED` or `LABEL_NOT_ALLOWED` | Skip message with warning; do not fail run | +| Gate check (required-labels) | `GATE_REQUIRED_LABELS` | Skip message (skipped=true); do not fail run | +| Gate check (title prefix) | `GATE_TITLE_PREFIX` | Skip message (skipped=true); do not fail run | +| Label resolution / creation failure | `LABEL_RESOLUTION_FAILED` | Return hard error; message fails | +| GraphQL mutation failure | `MUTATION_FAILED` | Return hard error; message fails | +| Rate-limit exhausted after retries | `RATE_LIMIT_EXHAUSTED` | Return hard error; message fails | + +**RL-044**: A conforming implementation MUST NOT mark the workflow run as failed for soft-skip errors (schema invalid, count gate, target unresolvable, repo not allowed, label validation failure, gate check failures). These errors MUST be surfaced as workflow warnings only. + +**RL-045**: A conforming implementation MUST surface hard errors (label resolution failure, mutation failure, rate-limit exhaustion) as `core.error()` entries in the GitHub Actions log. + +### 7.2 Partial Mutation Success + +As noted in §6.2, the GraphQL API provides no rollback guarantee. The following rules govern partial-success handling: + +**RL-046**: When `removeLabelsFromLabelable` succeeds but `addLabelsToLabelable` returns an error, the implementation MUST log a `core.error()` entry clearly indicating that the remove succeeded but the add failed, and MUST return `{ success: false, error: }`. + +**RL-047**: Implementors SHOULD provide the full GraphQL error detail in the error log entry to aid diagnosis. + +### 7.3 Rate-Limit Retry Policy + +**RL-048**: Both the label-resolution step (Stage 7) and the GraphQL mutation step (Stage 8) MUST apply the `RATE_LIMIT_RETRY_CONFIG` retry policy from `actions/setup/js/error_recovery.cjs`. This policy covers secondary rate-limit responses (HTTP 403 with Retry-After header) and primary rate-limit responses (HTTP 429). + +--- + +## 8. Security Considerations + +### 8.1 Label Allowlist Enforcement + +Label allowlists and blocklists are the primary mechanism preventing AI agents from performing unintended or malicious label transitions. For example, a `blocked: ["~*"]` pattern would prohibit any label whose name begins with `~`, while `blocked: ["*[bot]"]` would prohibit adding bot-created labels. + +**RL-049**: Allowlist and blocklist evaluation MUST be performed server-side (in the JavaScript handler executing within GitHub Actions), not by the AI agent. Agents MUST NOT be trusted to self-enforce label restrictions. + +### 8.2 Cross-Repository Restrictions + +By default, `replace-label` operates on the repository of the triggering workflow. Cross-repository operation is opt-in and must be explicitly declared. + +**RL-050**: A conforming implementation MUST NOT execute a label replacement on a repository not reachable via the resolved `target-repo` or `allowed-repos` configuration. Unrecognized repository identifiers in agent messages MUST be rejected. + +**RL-051**: The GitHub token used for cross-repository operations MUST have `issues: write` permission on the target repository. The implementation SHOULD validate this at startup or log a clear error when the token lacks the required scope. + +### 8.3 Label Auto-Creation Risk + +Auto-creating labels on behalf of an AI agent carries a risk of repository label pollution. Workflow authors SHOULD use `allowed-add` to restrict the set of labels that may be auto-created. + +**RL-052**: Label auto-creation MUST only be performed when the `label_to_add` value has passed the allowlist and blocklist checks in Stage 4. The implementation MUST NOT auto-create labels that are blocked or not in the allowlist. + +### 8.4 Required-Labels as an Execution Gate + +The `required-labels` configuration field provides an additional execution gate that can be used to constrain when label replacements are permitted. For example, a workflow can require that an issue carries a `triage` label before any agent-driven label transition is allowed. + +**RL-053**: The `required-labels` check MUST use the current server-side label state fetched from the GitHub REST API, not any label state provided in the agent message itself. + +### 8.5 Token Scope + +**RL-054**: The GitHub token used by `replace-label` MUST have the `issues: write` permission (which also covers pull request label operations). This is the minimum required scope. + +**RL-055**: The implementation SHOULD use a dedicated per-type token (via `github-token`) when the default workflow token carries broader permissions than required by `replace-label` alone. + +### 8.6 Staged Mode as a Security Control + +Staged mode provides a mechanism for operators to audit AI agent label-transition behavior before it takes effect. + +**RL-056**: When `staged: true`, the implementation MUST NOT call any write API endpoint. Read-only API calls performed during Stage 5 gate checks MAY proceed in staged mode. + +--- + +## 9. Compliance Testing + +### 9.1 Test Suite Structure + +The test suite for `replace-label` spans two layers: + +- **Unit tests** (`actions/setup/js/replace_label.test.cjs`): Test the JavaScript handler in isolation using mocked GitHub API clients. +- **Integration tests** (`pkg/workflow/`): Test Go configuration parsing and schema validation using the common safe-output test infrastructure. + +### 9.2 Test Requirements + +#### 9.2.1 Schema Validation Tests + +- **T-RL-001**: Verify that a message with a missing `label_to_remove` is rejected. +- **T-RL-002**: Verify that a message with a missing `label_to_add` is rejected. +- **T-RL-003**: Verify that a message with `label_to_remove` exceeding 128 characters is rejected. +- **T-RL-004**: Verify that a message with `label_to_add` exceeding 128 characters is rejected. +- **T-RL-005**: Verify that a message with `repo` exceeding 256 characters is rejected. +- **T-RL-006**: Verify that a valid message passes schema validation without error. + +#### 9.2.2 Count Gate Tests + +- **T-RL-010**: Verify that the operation succeeds when count < max. +- **T-RL-011**: Verify that the operation is rejected when count = max (gate is exclusive: count must be strictly less than max). +- **T-RL-012**: Verify that the default max of 5 is enforced when `max` is absent from configuration. +- **T-RL-013**: Verify that a GHA expression in `max` is resolved at runtime. + +#### 9.2.3 Label Validation Tests + +- **T-RL-020**: Verify that `label_to_add` is accepted when `allowed-add` is empty. +- **T-RL-021**: Verify that `label_to_add` matching a pattern in `allowed-add` is accepted. +- **T-RL-022**: Verify that `label_to_add` not matching any pattern in a non-empty `allowed-add` is rejected. +- **T-RL-023**: Verify that `label_to_add` matching a `blocked` pattern is rejected even when it also matches `allowed-add`. +- **T-RL-024**: Verify that `label_to_remove` matching a `blocked` pattern is rejected. +- **T-RL-025**: Verify that `label_to_remove` is accepted when `allowed-remove` is empty. + +#### 9.2.4 Gate Check Tests + +- **T-RL-030**: Verify that an item satisfying all `required-labels` proceeds to the mutation stage. +- **T-RL-031**: Verify that an item missing a required label is skipped (`skipped: true`) without failing. +- **T-RL-032**: Verify that an item with a title matching `required-title-prefix` proceeds. +- **T-RL-033**: Verify that an item whose title does not match `required-title-prefix` is skipped without failing. + +#### 9.2.5 Label Resolution Tests + +- **T-RL-040**: Verify that an existing label is resolved via REST and its node ID is used. +- **T-RL-041**: Verify that a missing label is auto-created with the correct deterministic pastel color. +- **T-RL-042**: Verify that the deterministic color algorithm produces stable, reproducible output for the same label name across invocations. +- **T-RL-043**: Verify that the label node-ID cache prevents redundant API calls for the same label name within a run. +- **T-RL-044**: Verify that when `label_to_remove` is not on the item, `removeLabelIds` is `[]` and the operation proceeds. + +#### 9.2.6 GraphQL Mutation Tests + +- **T-RL-050**: Verify the GraphQL mutation is called with the correct `labelableId`, `addLabelIds`, and `removeLabelIds` variables. +- **T-RL-051**: Verify that the mutation result's updated label list is logged. +- **T-RL-052**: Verify that a GraphQL mutation error produces a hard error result with `success: false`. +- **T-RL-053**: Verify that `$addLabelIds` always has exactly one element. +- **T-RL-054**: Verify that rate-limit responses trigger retry behavior. + +#### 9.2.7 Staged Mode Tests + +- **T-RL-060**: Verify that no write API call is made when `staged: true`. +- **T-RL-061**: Verify that the preview log entry includes the correct label names, item number, and repository. +- **T-RL-062**: Verify that staged mode returns `{ success: true, staged: true }`. + +#### 9.2.8 Cross-Repository Tests + +- **T-RL-070**: Verify that a message with a `repo` in `allowed-repos` is accepted. +- **T-RL-071**: Verify that a message with a `repo` not in `allowed-repos` is rejected. +- **T-RL-072**: Verify that `target-repo` is used as the default when `repo` is absent from the message. + +### 9.3 Compliance Checklist + +| Requirement | Test ID(s) | Level | Status | +|-------------|------------|-------|--------| +| RL-001 Glob pattern matching semantics | T-RL-020 – T-RL-025 | 1 | Required | +| RL-002 Allowlist enforcement | T-RL-021, T-RL-022, T-RL-025 | 1 | Required | +| RL-003 Blocklist enforcement | T-RL-023, T-RL-024 | 1 | Required | +| RL-004 label_to_remove required | T-RL-001 | 1 | Required | +| RL-005 label_to_add required | T-RL-002 | 1 | Required | +| RL-006 repo max length | T-RL-005 | 1 | Required | +| RL-007 String sanitization | T-RL-006 | 1 | Required | +| RL-010 Count gate enforcement | T-RL-010, T-RL-011 | 1 | Required | +| RL-012 Default max = 5 | T-RL-012 | 1 | Required | +| RL-017 No item number error | T-RL-006 | 1 | Required | +| RL-024 required-labels gate | T-RL-030, T-RL-031 | 1 | Required | +| RL-025 required-title-prefix gate | T-RL-032, T-RL-033 | 1 | Required | +| RL-027 Staged mode no writes | T-RL-060 | 1 | Required | +| RL-029 label_to_add resolution | T-RL-040 | 1 | Required | +| RL-030 label_to_add auto-creation | T-RL-041 | 1 | Required | +| RL-034 Missing label_to_remove proceeds | T-RL-044 | 1 | Required | +| RL-035 Deterministic color algorithm | T-RL-042 | 1 | Required | +| RL-036 Single GraphQL mutation | T-RL-050 | 1 | Required | +| RL-037 Rate-limit retry on mutation | T-RL-054 | 2 | Recommended | +| RL-041 addLabelIds single element | T-RL-053 | 1 | Required | +| RL-042 removeLabelIds empty when absent | T-RL-044 | 1 | Required | +| RL-043 Sequential mutation semantics | T-RL-050 | 1 | Required | +| RL-050 Cross-repo restrictions | T-RL-070 – T-RL-072 | 1 | Required | +| RL-052 No auto-create blocked labels | T-RL-023, T-RL-041 | 1 | Required | + +--- + +## 10. Examples + +### 10.1 Basic Configuration: Approved Review Workflow + +A workflow that allows an AI code-review agent to transition pull requests from `in-review` to `approved` or `changes-requested`: + +```yaml +safe-outputs: + replace-label: + allowed-remove: ["in-review", "changes-requested"] + allowed-add: ["approved", "changes-requested"] + blocked: ["~*", "do-not-merge"] + max: 3 + target: "triggering" + required-labels: ["in-review"] +``` + +When the agent emits: + +```json +{ + "label_to_remove": "in-review", + "label_to_add": "approved" +} +``` + +The handler will: +1. Validate the message schema. +2. Check the count gate (count=0 < max=3 ✓). +3. Resolve the triggering PR as the target. +4. Validate `in-review` against `allowed-remove` ✓, check against `blocked` ✓. +5. Validate `approved` against `allowed-add` ✓, check against `blocked` ✓. +6. Fetch the PR and verify `in-review` is present (required-labels gate ✓). +7. Resolve node IDs for both labels. +8. Execute the GraphQL mutation: removes `in-review`, adds `approved` in a single request. + +### 10.2 Missing Source Label: Graceful Add-Only + +If the PR in Example 10.1 does not currently carry `in-review` (e.g., a prior run already removed it), the handler proceeds with add-only: + +``` +Label "in-review" is not present on pull request #42 in owner/repo — will only add "approved" +``` + +The mutation is called with `removeLabelIds: []` and `addLabelIds: []`. The operation succeeds and returns `{ labelRemoved: null, labelAdded: "approved" }`. + +### 10.3 Staged Mode Preview + +With `staged: true` in the configuration: + +```yaml +safe-outputs: + replace-label: + staged: true + allowed-add: ["done"] + allowed-remove: ["in-progress"] +``` + +For the message `{ "label_to_remove": "in-progress", "label_to_add": "done", "item_number": 17 }`, the handler logs: + +``` +[STAGED] Would replace label "in-progress" → "done" on issue #17 in owner/repo +``` + +No API write calls are made. The handler returns `{ success: true, staged: true }`. + +### 10.4 Auto-Created Label + +Given the message `{ "label_to_remove": "needs-review", "label_to_add": "ship-it" }` and `ship-it` not existing in the repository: + +``` +Label "ship-it" not found in owner/repo, creating it +Created label "ship-it" with color #a3b4c2 +``` + +The color `#a3b4c2` is the deterministic pastel color computed for the string `"ship-it"`. + +### 10.5 Cross-Repository Operation + +```yaml +safe-outputs: + replace-label: + target-repo: "owner/infra" + allowed-repos: ["owner/infra", "owner/platform"] + allowed-add: ["deployed"] + allowed-remove: ["pending-deploy"] + github-token: "${{ secrets.INFRA_LABEL_TOKEN }}" +``` + +Agent message: + +```json +{ + "label_to_remove": "pending-deploy", + "label_to_add": "deployed", + "item_number": 88, + "repo": "owner/platform" +} +``` + +The handler resolves the target repository to `owner/platform` (which is in `allowed-repos`), fetches issue #88 from that repository, and executes the GraphQL mutation using the `INFRA_LABEL_TOKEN` credential. + +### 10.6 Blocked Label Rejection + +Configuration: + +```yaml +safe-outputs: + replace-label: + blocked: ["*[bot]", "~*"] +``` + +Agent message: + +```json +{ "label_to_remove": "review-requested", "label_to_add": "~approved" } +``` + +Stage 4 evaluation: +- `allowed-add` is empty → no allowlist restriction. +- `~approved` matches blocked pattern `~*` → **rejected** with warning: + +``` +label_to_add validation failed: label "~approved" matches blocked pattern "~*" +``` + +The message is skipped. The workflow run is not marked as failed. + +--- + +## References + +### Normative References + +- **[RFC 2119]** Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. https://www.ietf.org/rfc/rfc2119.txt + +- **[GRAPHQL]** GraphQL Foundation, "GraphQL Specification", June 2018. https://spec.graphql.org/June2018/ + +- **[GITHUB-GRAPHQL-LABELS]** GitHub, Inc., "addLabelsToLabelable and removeLabelsFromLabelable mutations", GitHub GraphQL API Documentation. https://docs.github.com/en/graphql/reference/mutations#addlabelstolabelable + +- **[GITHUB-REST-LABELS]** GitHub, Inc., "Labels REST API", GitHub REST API Documentation. https://docs.github.com/en/rest/issues/labels + +### Informative References + +- **[GH-AW-SECURITY]** GitHub gh-aw Team, "GitHub Agentic Workflows Security Architecture Specification", 2026. `specs/security-architecture-spec.md` + +- **[GH-AW-CONFIG]** GitHub gh-aw Team, "AWF Config Canonical Sources Specification", 2026. `specs/awf-config-sources-spec.md` + +- **[GOBWAS-GLOB]** Gobwas, "glob — Go glob matching library". https://github.com/gobwas/glob + +- **[RATE-LIMIT]** GitHub, Inc., "Rate limits for the REST API". https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api + +--- + +## Change Log + +### Version 1.0.0 (Candidate Recommendation) — 2026-06-20 + +- Initial publication of the `replace-label` safe-output type specification. +- Covers configuration schema, message schema, eight-stage processing model, GraphQL mutation, error-handling categories, security considerations, and compliance test suite. +- Normative requirement codes RL-001 through RL-056 established. +- Test IDs T-RL-001 through T-RL-072 defined. + +--- + +*Copyright © 2026 GitHub, Inc. All rights reserved.* From 31f5eeea2a13c3d8116b4be34233389332e43f99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:17:07 +0000 Subject: [PATCH 05/16] Apply replace-label spec: docs, type list, and RL-046 partial failure handling Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/safe-outputs-management.md | 16 ++++++++++++++++ .github/aw/syntax-agentic.md | 2 +- actions/setup/js/replace_label.cjs | 9 ++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/aw/safe-outputs-management.md b/.github/aw/safe-outputs-management.md index 73ecbf006e1..a491d479eba 100644 --- a/.github/aw/safe-outputs-management.md +++ b/.github/aw/safe-outputs-management.md @@ -102,6 +102,22 @@ description: Safe-output reference for update, label, milestone, project, releas ``` When `allowed` is omitted, any labels can be removed. +- `replace-label:` - Atomic label state transition — removes one label and adds another in a single GraphQL request, eliminating the race window of separate remove + add operations + + ```yaml + safe-outputs: + replace-label: + allowed-add: [approved, done] # Optional: glob patterns for labels that may be added (any allowed if omitted) + allowed-remove: [in-review, pending] # Optional: glob patterns for labels that may be removed (any allowed if omitted) + blocked: ["~*", "*[bot]"] # Optional: blocked label patterns (glob; applies to both add and remove) + required-labels: [triage] # Optional: ALL of these labels must be present on the issue/PR for the operation to run + required-title-prefix: "[Bug]" # Optional: issue/PR title must start with this prefix + max: 5 # Optional: maximum number of replacements (default: 5) + target: "triggering" # Optional: "triggering" (default), "*" (any issue/PR), or number + target-repo: "owner/repo" # Optional: cross-repository + ``` + + The agent calls `replace_label(label_to_remove, label_to_add)`. If the label to remove is not present on the item, only the add is applied (no failure). Labels that do not yet exist in the repository are auto-created with a deterministic pastel color. - `add-reviewer:` - Add reviewers to pull requests ```yaml diff --git a/.github/aw/syntax-agentic.md b/.github/aw/syntax-agentic.md index 463090c8795..19bb2a7766b 100644 --- a/.github/aw/syntax-agentic.md +++ b/.github/aw/syntax-agentic.md @@ -439,7 +439,7 @@ description: Agentic workflow specific frontmatter fields for GitHub Agentic Wor - `cli-proxy:` - Mount each user-facing MCP server as a standalone CLI tool on `PATH` (boolean, default: `false`). When enabled, the agent can call MCP servers via shell commands (e.g. `github issue_read --method get ...`). CLI-mounted servers remain in the MCP gateway so their containers start normally. -- **`safe-outputs:`** - Safe output processing configuration. See [safe-outputs.md](safe-outputs.md) for complete documentation of all output types: `create-issue`, `create-discussion`, `add-comment`, `create-pull-request`, `push-to-pull-request-branch`, `close-issue`, `close-discussion`, `update-issue`, `update-pull-request`, `add-labels`, `remove-labels`, `dispatch-workflow`, `call-workflow`, `create-code-scanning-alert`, `upload-asset`, `upload-artifact`, `assign-to-agent`, `assign-to-user`, and more. +- **`safe-outputs:`** - Safe output processing configuration. See [safe-outputs.md](safe-outputs.md) for complete documentation of all output types: `create-issue`, `create-discussion`, `add-comment`, `create-pull-request`, `push-to-pull-request-branch`, `close-issue`, `close-discussion`, `update-issue`, `update-pull-request`, `add-labels`, `remove-labels`, `replace-label`, `dispatch-workflow`, `call-workflow`, `create-code-scanning-alert`, `upload-asset`, `upload-artifact`, `assign-to-agent`, `assign-to-user`, and more. **Key safe-outputs global fields** (detail in [safe-outputs-runtime.md](safe-outputs-runtime.md)): `github-token`, `github-app`, `staged` (preview mode, no API calls), `footer`, `threat-detection`, `runs-on` (default `ubuntu-slim`), `messages`, `env`, `max-patch-size` (KB, default `4096`). diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index a01ee91b41b..5fa462e29e6 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -337,7 +337,14 @@ const main = createCountGatedHandler({ ); } catch (err) { const errorMessage = getErrorMessage(err); - core.error(`Failed to replace label: ${errorMessage}`); + // RL-046: detect partial mutation success — remove succeeded but add failed + const errAsAny = /** @type {any} */ err; + const partialData = errAsAny?.data; + if (partialData?.removeLabels && !partialData?.addLabels) { + core.error(`Partial mutation failure on ${contextType} #${itemNumber} in ${itemRepo}: ` + `"${labelToRemove}" was removed but "${labelToAdd}" could not be added: ${errorMessage}`); + } else { + core.error(`Failed to replace label: ${errorMessage}`); + } return { success: false, error: errorMessage }; } }; From 005c2ec2ca714f20e3fd0df73454e3bb3a899870 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:17:49 +0000 Subject: [PATCH 06/16] style: combine template literal in partial mutation error message Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index 5fa462e29e6..150dd2b0f09 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -341,7 +341,7 @@ const main = createCountGatedHandler({ const errAsAny = /** @type {any} */ err; const partialData = errAsAny?.data; if (partialData?.removeLabels && !partialData?.addLabels) { - core.error(`Partial mutation failure on ${contextType} #${itemNumber} in ${itemRepo}: ` + `"${labelToRemove}" was removed but "${labelToAdd}" could not be added: ${errorMessage}`); + core.error(`Partial mutation failure on ${contextType} #${itemNumber} in ${itemRepo}: "${labelToRemove}" was removed but "${labelToAdd}" could not be added: ${errorMessage}`); } else { core.error(`Failed to replace label: ${errorMessage}`); } From 97f35601728f0ba5349ff2e5732f6d5ea0b9485f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:29:54 +0000 Subject: [PATCH 07/16] docs: add draft ADR for replace-label safe-output type Co-Authored-By: Claude Opus 4.8 (1M context) --- ...0423-add-replace-label-safe-output-type.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/adr/40423-add-replace-label-safe-output-type.md diff --git a/docs/adr/40423-add-replace-label-safe-output-type.md b/docs/adr/40423-add-replace-label-safe-output-type.md new file mode 100644 index 00000000000..24b103ec254 --- /dev/null +++ b/docs/adr/40423-add-replace-label-safe-output-type.md @@ -0,0 +1,40 @@ +# ADR-40423: Add `replace-label` Safe-Output Type for Atomic Label State Transitions + +**Date**: 2026-06-20 +**Status**: Draft + +## Context + +Agentic workflows in gh-aw frequently model issue/PR lifecycle as a label-based state machine (e.g. `in-review` → `done`). Today an agent advances such a state by emitting `remove-labels` and `add-labels` as two independent safe outputs, which are applied as two separate GitHub API calls. Between those calls the item is transiently in an inconsistent state — neither label, or (on partial failure) the old label still present and the new one missing — which is observable by other automation watching label events. There was no single safe-output type expressing "transition from label A to label B" as one unit, so workflow authors had to hand-orchestrate the two operations and accept the race window. + +## Decision + +We will add a new first-class safe-output type, `replace-label`, that removes one label and adds another in a single combined GraphQL request (`removeLabelsFromLabelable` + `addLabelsToLabelable`), giving label-based state machines a clear, single-call state transition. The handler reuses the existing `add-labels`/`remove-labels` guard infrastructure (`allowed-add`, `allowed-remove`, `blocked`, `required-labels`, `required-title-prefix`, `target`, `target-repo`, `allowed-repos`, `staged`, `max`, `github-token`), auto-creates a missing target label with a deterministic pastel color, tolerates the source label already being absent (add still applies), and detects the partial-mutation case (remove succeeded, add failed) per requirement RL-046. The type is wired through the standard safe-output infrastructure (Go config struct, handler descriptor, registry, schema, validation, tools/prompt wiring) and specified in a W3C-style document (`specs/replace-label-spec.md`). + +## Alternatives Considered + +### Alternative 1: Keep using sequential `remove-labels` + `add-labels` +Continue requiring workflow authors to compose the two existing safe outputs. Rejected because the two-call sequence leaves a visible race window and shifts the burden of expressing an atomic transition onto every author, with no single audit-able "replace" intent in the output stream. + +### Alternative 2: Add a client-side transaction wrapper around the existing two handlers +Introduce a coordinating helper that calls the existing add/remove handlers back-to-back with retry/rollback rather than a new type and a combined mutation. Rejected because it still issues two REST calls (no atomicity gain), adds rollback complexity, and would not surface a distinct, declaratively-configured `replace-label` schema entry that authors can guard with `allowed-add`/`allowed-remove`. + +## Consequences + +### Positive +- Single combined GraphQL mutation collapses the transition into one request, eliminating the inter-call race window of the two-output approach. +- Reuses the shared safe-output config (`SafeOutputTargetConfig`, `SafeOutputFilterConfig`, guard validation), so behavior and security guards stay consistent with `add-labels`/`remove-labels`. +- Gives label-based state machines an explicit, audit-able "A → B" intent backed by a formal spec and a 44-ID compliance test suite. + +### Negative +- Adds a new safe-output type with broad surface area (~21 files: Go config, JS handler, schema, registry, docs, tests) that must be maintained in lockstep with the existing label handlers. +- GraphQL "atomicity" here is sequential-within-one-request, not a true transaction: a partial failure (remove succeeds, add fails) remains possible and is only detected/logged (RL-046), not rolled back. +- Auto-creating a missing target label can introduce labels into a repository as a side effect of a state transition, which may surprise maintainers who expect labels to be curated explicitly. + +### Neutral +- Inherits `target`/`target-repo`/`allowed-repos`, so cross-repository label transitions are possible and follow the same allow-list semantics as other label outputs. +- `max` defaults to 5 replacements per run, matching the convention of the sibling label handlers. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/27875419304) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* From fc940bd1a5a92e39e352edc7eb87ca4d9d27a711 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:40:32 +0000 Subject: [PATCH 08/16] fix: address PR review feedback on replace-label handler and spec Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 126 ++++++++++-------------- actions/setup/js/replace_label.test.cjs | 71 ++++++------- pkg/workflow/replace_label.go | 2 +- specs/replace-label-spec.md | 101 +++++++------------ 4 files changed, 120 insertions(+), 180 deletions(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index 150dd2b0f09..d62832102e1 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -24,14 +24,18 @@ const HANDLER_TYPE = "replace_label"; /** * GraphQL mutation that removes one label and adds another in a single request. - * Root mutations in a single request are executed sequentially (remove first, then add), - * providing an atomic state transition for label-based state machines. + * Root mutations in a single request are executed sequentially (remove first, then add). + * When $doRemove is false the removeLabels field is skipped entirely, avoiding a + * GitHub API error that would result from passing an empty labelIds array. + * + * Note: this is a sequential operation, not a transaction. If removeLabels succeeds + * but addLabels fails the removal is not reversed (see RL-046 partial-failure handling). * * @type {string} */ const REPLACE_LABEL_MUTATION = /* GraphQL */ ` - mutation ReplaceLabelMutation($labelableId: ID!, $addLabelIds: [ID!]!, $removeLabelIds: [ID!]!) { - removeLabels: removeLabelsFromLabelable(input: { labelableId: $labelableId, labelIds: $removeLabelIds }) { + mutation ReplaceLabelMutation($labelableId: ID!, $addLabelIds: [ID!]!, $removeLabelIds: [ID!]!, $doRemove: Boolean!) { + removeLabels: removeLabelsFromLabelable(input: { labelableId: $labelableId, labelIds: $removeLabelIds }) @include(if: $doRemove) { clientMutationId } addLabels: addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $addLabelIds }) { @@ -55,27 +59,7 @@ const REPLACE_LABEL_MUTATION = /* GraphQL */ ` } `; -/** - * GraphQL query to resolve label node IDs from a repository by name. - * Searches for labels matching the given names so we can get their node IDs - * for the mutation. - * - * @type {string} - */ -const RESOLVE_LABEL_QUERY = /* GraphQL */ ` - query ResolveLabelNodeIds($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - labels(first: 100) { - nodes { - id - name - } - } - } - } -`; - -const { validateLabels } = require("./safe_output_validator.cjs"); +const { matchesSimpleGlob } = require("./glob_pattern_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); @@ -87,22 +71,21 @@ const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); /** - * Resolve or create a label in the repository, returning its GraphQL node ID. - * If the label does not exist, it is created with a deterministic pastel color. + * Resolve a label in the repository, returning its GraphQL node ID. + * If the label does not exist in the repository, a hard error is thrown. * - * @param {any} githubClient - Authenticated GitHub client with REST and graphql + * @param {any} githubClient - Authenticated GitHub client with REST * @param {string} owner - Repository owner * @param {string} repo - Repository name - * @param {string} labelName - Label name to resolve or create + * @param {string} labelName - Label name to resolve * @param {Map} labelNodeIdCache - Cache of label name → node_id * @returns {Promise} The GraphQL node ID of the label */ -async function resolveOrCreateLabel(githubClient, owner, repo, labelName, labelNodeIdCache) { +async function resolveLabel(githubClient, owner, repo, labelName, labelNodeIdCache) { if (labelNodeIdCache.has(labelName)) { return /** @type {string} */ labelNodeIdCache.get(labelName); } - // Try to get the label from the repo try { const { data: label } = await githubClient.rest.issues.getLabel({ owner, @@ -112,47 +95,43 @@ async function resolveOrCreateLabel(githubClient, owner, repo, labelName, labelN labelNodeIdCache.set(labelName, label.node_id); return label.node_id; } catch (err) { - const msg = getErrorMessage(err); - if (!msg.includes("404") && !msg.toLowerCase().includes("not found")) { + if (err?.status !== 404) { throw err; } + throw new Error(`Label "${labelName}" does not exist in ${owner}/${repo}. Create the label in the repository before using it with replace-label.`); } - - // Label does not exist — create it with a deterministic color - core.info(`Label "${labelName}" not found in ${owner}/${repo}, creating it`); - const color = deterministicLabelColor(labelName); - const { data: created } = await githubClient.rest.issues.createLabel({ - owner, - repo, - name: labelName, - color, - }); - core.info(`Created label "${labelName}" with color #${color}`); - labelNodeIdCache.set(labelName, created.node_id); - return created.node_id; } /** - * Generate a deterministic pastel hex color from a label name. - * Produces colors in the pastel range (128–191 per channel) for readability. + * Validate a single label against blocked and allowed-list patterns. + * Uses explicit rejection semantics — does not silently filter or truncate the label name. + * Blocked patterns are evaluated first (security boundary), consistent with safe_output_validator.cjs. * - * @param {string} name - * @returns {string} Six-character hex color (no leading #) + * @param {string} labelName - Label name to validate + * @param {string[]} allowedPatterns - Allowlist patterns (empty = all labels allowed) + * @param {string[]} blockedPatterns - Blocklist patterns + * @param {string} fieldName - Field name for error messages (e.g. "label_to_add") + * @returns {{valid: true} | {valid: false, error: string}} */ -function deterministicLabelColor(name) { - let hash = 0; - for (let i = 0; i < name.length; i++) { - hash = (hash * 31 + name.charCodeAt(i)) >>> 0; +function validateSingleLabel(labelName, allowedPatterns, blockedPatterns, fieldName) { + if (blockedPatterns.length > 0) { + const isBlocked = blockedPatterns.some(pattern => matchesSimpleGlob(labelName, pattern)); + if (isBlocked) { + return { valid: false, error: `${fieldName} "${labelName}" matches a blocked pattern` }; + } + } + if (allowedPatterns.length > 0) { + const isAllowed = allowedPatterns.some(pattern => matchesSimpleGlob(labelName, pattern)); + if (!isAllowed) { + return { valid: false, error: `${fieldName} "${labelName}" is not in the allowed list` }; + } } - const r = 128 + (hash & 0x3f); - const g = 128 + ((hash >> 6) & 0x3f); - const b = 128 + ((hash >> 12) & 0x3f); - return ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0"); + return { valid: true }; } /** * Main handler factory for replace_label. - * Uses the GraphQL API to remove one label and add another in a single atomic request. + * Uses the GraphQL API to remove one label and add another in a single request. * @type {HandlerFactoryFunction} */ const main = createCountGatedHandler({ @@ -221,18 +200,18 @@ const main = createCountGatedHandler({ return { success: false, error }; } - // Validate label_to_remove against allowed-remove and blocked patterns - const removeValidation = validateLabels([labelToRemove], configAllowedRemove, 1, blockedPatterns); + // Validate label_to_remove against blocked patterns and allowed-remove list + const removeValidation = validateSingleLabel(labelToRemove, configAllowedRemove, blockedPatterns, "label_to_remove"); if (!removeValidation.valid) { core.warning(`label_to_remove validation failed: ${removeValidation.error}`); - return { success: false, error: removeValidation.error ?? "Invalid label_to_remove" }; + return { success: false, error: removeValidation.error }; } - // Validate label_to_add against allowed-add and blocked patterns - const addValidation = validateLabels([labelToAdd], configAllowedAdd, 1, blockedPatterns); + // Validate label_to_add against blocked patterns and allowed-add list + const addValidation = validateSingleLabel(labelToAdd, configAllowedAdd, blockedPatterns, "label_to_add"); if (!addValidation.valid) { core.warning(`label_to_add validation failed: ${addValidation.error}`); - return { success: false, error: addValidation.error ?? "Invalid label_to_add" }; + return { success: false, error: addValidation.error }; } // Apply required-labels and required-title-prefix filters @@ -276,14 +255,14 @@ const main = createCountGatedHandler({ } const labelNodeIdCache = /** @type {Map} */ repoCaches.get(itemRepo); - // Resolve the node ID of label_to_add (create if it doesn't exist in the repo) + // Resolve the node ID of label_to_add — fails with hard error if the label does not exist let addLabelNodeId; try { - addLabelNodeId = await withRetry(() => resolveOrCreateLabel(githubClient, repoParts.owner, repoParts.repo, labelToAdd, labelNodeIdCache), RATE_LIMIT_RETRY_CONFIG, `resolve/create label "${labelToAdd}" in ${itemRepo}`); + addLabelNodeId = await withRetry(() => resolveLabel(githubClient, repoParts.owner, repoParts.repo, labelToAdd, labelNodeIdCache), RATE_LIMIT_RETRY_CONFIG, `resolve label "${labelToAdd}" in ${itemRepo}`); } catch (err) { const errorMessage = getErrorMessage(err); - core.error(`Failed to resolve/create label "${labelToAdd}": ${errorMessage}`); - return { success: false, error: `Failed to resolve/create label "${labelToAdd}": ${errorMessage}` }; + core.error(`Failed to resolve label "${labelToAdd}": ${errorMessage}`); + return { success: false, error: `Failed to resolve label "${labelToAdd}": ${errorMessage}` }; } // Find the node ID of label_to_remove from the issue's current labels. @@ -309,6 +288,7 @@ const main = createCountGatedHandler({ labelableId, addLabelIds: [addLabelNodeId], removeLabelIds: removeLabelNodeId ? [removeLabelNodeId] : [], + doRemove: !!removeLabelNodeId, }), RATE_LIMIT_RETRY_CONFIG, `replace_label on ${contextType} #${itemNumber} in ${itemRepo}` @@ -337,9 +317,11 @@ const main = createCountGatedHandler({ ); } catch (err) { const errorMessage = getErrorMessage(err); - // RL-046: detect partial mutation success — remove succeeded but add failed + // RL-046: detect partial mutation success — remove succeeded but add failed. + // withRetry may wrap the original error via enhanceError, so check both + // err.data (direct graphql error) and err.originalError.data (wrapped error). const errAsAny = /** @type {any} */ err; - const partialData = errAsAny?.data; + const partialData = errAsAny?.data ?? errAsAny?.originalError?.data; if (partialData?.removeLabels && !partialData?.addLabels) { core.error(`Partial mutation failure on ${contextType} #${itemNumber} in ${itemRepo}: "${labelToRemove}" was removed but "${labelToAdd}" could not be added: ${errorMessage}`); } else { @@ -351,4 +333,4 @@ const main = createCountGatedHandler({ }, }); -module.exports = { main, deterministicLabelColor, REPLACE_LABEL_MUTATION, RESOLVE_LABEL_QUERY }; +module.exports = { main, REPLACE_LABEL_MUTATION }; diff --git a/actions/setup/js/replace_label.test.cjs b/actions/setup/js/replace_label.test.cjs index 0ef339cb3a8..5037403eadb 100644 --- a/actions/setup/js/replace_label.test.cjs +++ b/actions/setup/js/replace_label.test.cjs @@ -1,6 +1,6 @@ // @ts-check import { describe, it, expect, beforeEach, vi } from "vitest"; -const { main, deterministicLabelColor } = require("./replace_label.cjs"); +const { main } = require("./replace_label.cjs"); describe("replace_label", () => { let mockCore; @@ -105,23 +105,18 @@ describe("replace_label", () => { expect(result.labelAdded).toBe("done"); }); - it("should create label_to_add if it does not exist in the repo", async () => { - let createLabelCalled = false; - mockGithub.rest.issues.getLabel = async ({ name }) => { + it("should return error when label_to_add does not exist in the repo", async () => { + mockGithub.rest.issues.getLabel = async () => { const err = new Error("Not Found"); err.status = 404; - throw err; // All labels "not found" - }; - mockGithub.rest.issues.createLabel = async ({ name, color }) => { - createLabelCalled = true; - return { data: { name, node_id: `LA_${name}_created`, color } }; + throw err; }; const handler = await main({}); const result = await handler({ label_to_remove: "in-progress", label_to_add: "needs-review" }, {}); - expect(result.success).toBe(true); - expect(createLabelCalled).toBe(true); + expect(result.success).toBe(false); + expect(result.error).toContain("needs-review"); }); it("should succeed even when label_to_remove is not present on the issue", async () => { @@ -219,38 +214,36 @@ describe("replace_label", () => { expect(result.success).toBe(false); }); -}); -describe("deterministicLabelColor", () => { - it("should return a 6-char hex string", () => { - const color = deterministicLabelColor("done"); - expect(color).toMatch(/^[0-9a-f]{6}$/); - }); + it("should log partial failure and return error when remove succeeds but add fails (RL-046)", async () => { + const gqlError = new Error("add mutation failed"); + // @octokit/graphql populates err.data with the partial response when some fields succeed + gqlError.data = { + removeLabels: { clientMutationId: null }, // remove succeeded + // addLabels absent — simulates add failure after remove succeeded + }; + mockGithub.graphql = async () => { + throw gqlError; + }; - it("should return different colors for different labels", () => { - const c1 = deterministicLabelColor("done"); - const c2 = deterministicLabelColor("in-progress"); - expect(c1).not.toBe(c2); - }); + const handler = await main({}); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); - it("should return the same color for the same label", () => { - const c1 = deterministicLabelColor("needs-review"); - const c2 = deterministicLabelColor("needs-review"); - expect(c1).toBe(c2); + expect(result.success).toBe(false); + expect(mockCore.errors.some(e => e.includes("in-progress") && e.includes("done"))).toBe(true); }); - it("should return pastel colors (128-191 per channel)", () => { - for (const name of ["done", "in-progress", "approved", "needs-review", "blocked"]) { - const hex = deterministicLabelColor(name); - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - expect(r).toBeGreaterThanOrEqual(128); - expect(r).toBeLessThanOrEqual(191); - expect(g).toBeGreaterThanOrEqual(128); - expect(g).toBeLessThanOrEqual(191); - expect(b).toBeGreaterThanOrEqual(128); - expect(b).toBeLessThanOrEqual(191); - } + it("should return error when getLabel fails with a non-404 error", async () => { + mockGithub.rest.issues.getLabel = async () => { + const err = new Error("Service unavailable"); + err.status = 503; + throw err; + }; + + const handler = await main({}); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain("done"); }); }); diff --git a/pkg/workflow/replace_label.go b/pkg/workflow/replace_label.go index 9f9ec8b7762..45715de05d0 100644 --- a/pkg/workflow/replace_label.go +++ b/pkg/workflow/replace_label.go @@ -7,7 +7,7 @@ import ( var replaceLabelLog = logger.New("workflow:replace_label") // ReplaceLabelConfig holds configuration for replacing one label with another on issues/PRs from agent output. -// It combines the capabilities of add-labels and remove-labels into a single atomic GraphQL operation, +// It combines the capabilities of add-labels and remove-labels into a single GraphQL request, // enabling clear state transitions (e.g. "in-progress" → "done"). type ReplaceLabelConfig struct { BaseSafeOutputConfig `yaml:",inline"` diff --git a/specs/replace-label-spec.md b/specs/replace-label-spec.md index 1464f03023e..6ba6a8749f7 100644 --- a/specs/replace-label-spec.md +++ b/specs/replace-label-spec.md @@ -17,9 +17,9 @@ sidebar: ## Abstract -This specification defines the `replace-label` safe-output type for GitHub Agentic Workflows (gh-aw), a mechanism that enables AI agents to atomically transition label state on GitHub issues and pull requests. The `replace-label` type removes one label and adds another in a single GraphQL request, eliminating the race window that would otherwise exist when using `remove-labels` and `add-labels` as separate sequential operations. +This specification defines the `replace-label` safe-output type for GitHub Agentic Workflows (gh-aw), a mechanism that enables AI agents to transition label state on GitHub issues and pull requests in a single GraphQL request. The `replace-label` type removes one label and adds another in a single GraphQL request, eliminating the HTTP round-trip between the two operations that would otherwise exist when using `remove-labels` and `add-labels` as separate sequential messages. -The specification covers the configuration schema, the message schema produced by AI agents, the multi-stage validation pipeline, the label-resolution and auto-creation mechanism, the GraphQL mutation executed against the GitHub API, error-handling requirements, security controls, and conformance testing requirements. +The specification covers the configuration schema, the message schema produced by AI agents, the multi-stage validation pipeline, the label-resolution mechanism, the GraphQL mutation executed against the GitHub API, error-handling requirements, security controls, and conformance testing requirements. ## Status of This Document @@ -52,7 +52,7 @@ This is a Candidate Recommendation specification representing the design and imp Label-based state machines are a common pattern in GitHub issue and pull request workflows. A triage issue may progress from `pending` → `in-review` → `approved`, or a PR review cycle may move from `needs-revision` → `ready-to-merge`. When these transitions are driven by AI agents operating through gh-aw, the canonical implementation using separate `remove-labels` and `add-labels` safe-output messages introduces a race window: between the removal and the addition, the item carries no label — or may be picked up by a concurrent automation that considers the intermediate label-less state valid. -The `replace-label` type solves this by combining the remove and add operations into a single GraphQL request. The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order, guaranteeing that the removal is applied before the addition and that no external observer can witness the intermediate state within the same request. +The `replace-label` type solves this by combining the remove and add operations into a single GraphQL request. The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order, guaranteeing that the removal is applied before the addition. This eliminates the HTTP round-trip between the two operations, reducing the window in which an external observer could see the item in an intermediate state. Note that GitHub does not provide rollback semantics: if the remove succeeds but the add fails, the state is partially transitioned (see §7.2). ### 1.2 Scope @@ -79,10 +79,10 @@ This specification does NOT cover: The `replace-label` type is designed to satisfy the following goals: -1. **Atomicity**: Remove and add operations MUST execute in a single GitHub API round-trip to eliminate the observable intermediate state. +1. **Reduced race window**: Remove and add operations MUST execute in a single GitHub API round-trip to eliminate the observable intermediate state compared to two separate requests. 2. **Idempotency on missing source label**: If the label to be removed is not present on the target item, the operation MUST still add the new label and succeed, rather than failing. 3. **Least privilege**: Allowlist-based configuration MUST constrain which labels agents may add or remove, limiting the blast radius of a misbehaving agent. -4. **Label hygiene**: If the label to be added does not yet exist in the repository, the handler MUST auto-create it with a deterministic pastel color rather than failing, avoiding manual label-setup requirements. +4. **Pre-existing labels**: Labels referenced in `label_to_add` MUST already exist in the repository. The operation MUST fail with a clear error if the label is missing, avoiding silent repository side effects. 5. **Safe preview**: Staged mode MUST allow operators to review what the agent would do without applying any changes to the GitHub API. 6. **Consistency with the safe-outputs framework**: Configuration fields (`max`, `target`, `target-repo`, `allowed-repos`, `github-token`, `staged`, `required-labels`, `required-title-prefix`) MUST follow the same semantics as all other safe-output types in gh-aw. @@ -111,7 +111,7 @@ Normative requirements are additionally identified by a short requirement code o ## 3. Concepts and Terminology -**Label replacement**: The atomic operation of removing one named label from a GitHub labelable item and adding a different named label in the same GitHub GraphQL API request. +**Label replacement**: The operation of removing one named label from a GitHub labelable item and adding a different named label in the same GitHub GraphQL API request. **Labelable**: A GitHub resource that can carry labels. In this specification, labelable items are GitHub Issues and GitHub Pull Requests. The GitHub GraphQL type `Labelable` is the interface implemented by both. @@ -177,7 +177,7 @@ safe-outputs: **RL-002**: A label name MUST match at least one pattern in an allowlist (`allowed-add` or `allowed-remove`) when the list is non-empty. An empty or absent allowlist permits any label name. -**RL-003**: A label name MUST NOT match any pattern in the `blocked` list. Blocklist evaluation MUST occur after allowlist evaluation. A label that passes the allowlist but matches a blocked pattern MUST be rejected. +**RL-003**: A label name MUST NOT match any pattern in the `blocked` list. Blocklist evaluation MUST occur before allowlist evaluation (it is a security boundary). A label matching a blocked pattern MUST be rejected immediately, regardless of any allowlist entry. ### 4.2 Message Schema @@ -267,13 +267,13 @@ A conforming implementation MUST execute the following pipeline for each `replac ▼ ┌─────────────────────┐ │ Stage 7 │ - │ Label Resolution │ ← resolve or auto-create labels + │ Label Resolution │ ← resolve labels (fail if not found) └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Stage 8 │ - │ GraphQL Mutation │ ← single atomic request + │ GraphQL Mutation │ ← single request (sequential) └─────────────────────┘ ``` @@ -371,9 +371,9 @@ Gate checks require fetching the current state of the target item via the GitHub - The label that would be added (`label_to_add`) - The item type (issue or pull request) -**RL-028**: Staged mode execution MUST return `{ success: true, staged: true }` from the handler, and MUST NOT decrement any rate-limit budget or increment the operation count. +**RL-028**: Staged mode execution MUST return `{ success: true, staged: true }` from the handler. Note: the operation count IS incremented for staged messages by the `createCountGatedHandler` scaffold (which counts every processed message before delegating to the handler), so staged messages count toward the `max` budget. -### 5.7 Stage 7: Label Resolution and Auto-Creation +### 5.7 Stage 7: Label Resolution Label resolution converts human-readable label names into the GraphQL node IDs required by the mutation. The implementation maintains a per-repository in-memory cache of `labelName → nodeId` to avoid redundant API calls within a single workflow run. @@ -381,35 +381,17 @@ Label resolution converts human-readable label names into the GraphQL node IDs r **RL-029**: The implementation MUST attempt to resolve the `label_to_add` name to a GraphQL node ID using the GitHub REST API (`GET /repos/{owner}/{repo}/labels/{name}`). -**RL-030**: When the label does not exist in the target repository (HTTP 404), the implementation MUST automatically create it using the GitHub REST API (`POST /repos/{owner}/{repo}/labels`) with: -- `name`: the exact label name from the message -- `color`: the deterministic pastel color computed by the algorithm in §5.7.3 +**RL-030**: When the label does not exist in the target repository (HTTP 404), the implementation MUST return a hard error and the message MUST be rejected. Labels referenced in `label_to_add` MUST be created in the repository before use. -**RL-031**: Label resolution and auto-creation MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy defined in `actions/setup/js/error_recovery.cjs`. +**RL-031**: Label resolution MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy defined in `actions/setup/js/error_recovery.cjs`. -**RL-032**: If resolution or creation fails for any reason other than a rate-limit or 404, the implementation MUST return a hard error and the message MUST be rejected. +**RL-032**: If resolution fails for any reason other than a rate-limit response, the implementation MUST return a hard error and the message MUST be rejected. #### 5.7.2 Resolving `label_to_remove` **RL-033**: The node ID of `label_to_remove` MUST be looked up from the current labels already attached to the target item (returned by the gate-check REST call in Stage 5), rather than from a separate API call. -**RL-034**: When `label_to_remove` is not currently attached to the target item, the implementation MUST proceed with the `add` operation only, passing an empty `removeLabelIds` array to the mutation. The operation MUST NOT fail in this case (see §1.3, Design Goal 2). - -#### 5.7.3 Deterministic Pastel Color Algorithm - -The color assigned to auto-created labels is derived from the label name using the following algorithm: - -``` -hash = 0 -for each character c in labelName: - hash = (hash * 31 + charCode(c)) unsigned-right-shifted 0 -r = 128 + (hash & 0x3F) # 128–191 -g = 128 + ((hash >> 6) & 0x3F) # 128–191 -b = 128 + ((hash >> 12) & 0x3F) # 128–191 -color = hex(r << 16 | g << 8 | b) # zero-padded to 6 characters -``` - -**RL-035**: A conforming implementation MUST use this algorithm (or one producing identical outputs) for all auto-created label colors, ensuring cross-run reproducibility. +**RL-034**: When `label_to_remove` is not currently attached to the target item, the implementation MUST proceed with the `add` operation only, setting `$doRemove: false` so that the `removeLabelsFromLabelable` field is omitted from the mutation via the `@include(if: $doRemove)` directive. The operation MUST NOT fail in this case (see §1.3, Design Goal 2). ### 5.8 Stage 8: GraphQL Mutation @@ -440,10 +422,11 @@ mutation ReplaceLabelMutation( $labelableId: ID! $addLabelIds: [ID!]! $removeLabelIds: [ID!]! + $doRemove: Boolean! ) { removeLabels: removeLabelsFromLabelable( input: { labelableId: $labelableId, labelIds: $removeLabelIds } - ) { + ) @include(if: $doRemove) { clientMutationId } addLabels: addLabelsToLabelable( @@ -471,34 +454,18 @@ mutation ReplaceLabelMutation( |----------|------|--------| | `$labelableId` | `ID!` | GraphQL node ID of the target issue or pull request (`item.node_id` from REST response) | | `$addLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_add`, as resolved in Stage 7 | -| `$removeLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_remove` when present on the item; empty array when the label is absent | +| `$removeLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_remove` when present on the item; empty array otherwise (ignored when `$doRemove` is `false`) | +| `$doRemove` | `Boolean!` | `true` when `label_to_remove` is currently on the item; `false` to skip the remove field via `@include` | **RL-041**: `$addLabelIds` MUST always contain exactly one element. -**RL-042**: `$removeLabelIds` MUST contain exactly one element when the label to remove is present on the target item, and MUST be an empty array (`[]`) otherwise. - -### 6.2 Atomicity Semantics +**RL-042**: `$doRemove` MUST be `true` (and `$removeLabelIds` MUST contain exactly one element) when the label to remove is currently on the target item. When the label is absent, `$doRemove` MUST be `false` so the `removeLabelsFromLabelable` field is omitted from the request via the `@include(if: $doRemove)` directive. -**RL-043**: The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order. A conforming implementation MUST rely on this sequential guarantee: `removeLabelsFromLabelable` executes before `addLabelsToLabelable` within the same request, preventing any external observer from reading the item in a state where neither label is present. +### 6.2 Execution Semantics -> **Informative note**: The term "atomic" in this specification refers to the single-request delivery guarantee, not to database transaction atomicity. GitHub does not provide rollback semantics: if `removeLabels` succeeds but `addLabels` fails, the remove is not reversed. Implementations SHOULD log this partial-success condition clearly when it occurs. +**RL-043**: The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order. When `$doRemove` is `true`, `removeLabelsFromLabelable` executes before `addLabelsToLabelable` within the same request, preventing any external observer from reading the item in a state where neither label is present. -### 6.3 Label Fetch Query - -The following read-only GraphQL query is used during label cache population (where needed) to bulk-fetch label node IDs from a repository. This query is supplementary; the primary resolution path uses the REST API (§5.7.1–5.7.2). - -```graphql -query ResolveLabelNodeIds($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - labels(first: 100) { - nodes { - id - name - } - } - } -} -``` +> **Informative note**: This is a sequential operation in a single HTTP request, not a database transaction. GitHub does not provide rollback semantics: if `removeLabels` succeeds but `addLabels` fails, the removal is not reversed. Implementations MUST log this partial-success condition clearly when it occurs (see §7.2, RL-046). --- @@ -515,7 +482,7 @@ query ResolveLabelNodeIds($owner: String!, $repo: String!) { | Label validation failure | `LABEL_BLOCKED` or `LABEL_NOT_ALLOWED` | Skip message with warning; do not fail run | | Gate check (required-labels) | `GATE_REQUIRED_LABELS` | Skip message (skipped=true); do not fail run | | Gate check (title prefix) | `GATE_TITLE_PREFIX` | Skip message (skipped=true); do not fail run | -| Label resolution / creation failure | `LABEL_RESOLUTION_FAILED` | Return hard error; message fails | +| Label resolution failure | `LABEL_RESOLUTION_FAILED` | Return hard error; message fails | | GraphQL mutation failure | `MUTATION_FAILED` | Return hard error; message fails | | Rate-limit exhausted after retries | `RATE_LIMIT_EXHAUSTED` | Return hard error; message fails | @@ -553,11 +520,11 @@ By default, `replace-label` operates on the repository of the triggering workflo **RL-051**: The GitHub token used for cross-repository operations MUST have `issues: write` permission on the target repository. The implementation SHOULD validate this at startup or log a clear error when the token lacks the required scope. -### 8.3 Label Auto-Creation Risk +### 8.3 Label Requirements -Auto-creating labels on behalf of an AI agent carries a risk of repository label pollution. Workflow authors SHOULD use `allowed-add` to restrict the set of labels that may be auto-created. +Labels referenced in `label_to_add` must pre-exist in the repository. The implementation returns a hard error when a label is not found, consistent with the `add-labels` safe-output type and preventing silent repository side effects. -**RL-052**: Label auto-creation MUST only be performed when the `label_to_add` value has passed the allowlist and blocklist checks in Stage 4. The implementation MUST NOT auto-create labels that are blocked or not in the allowlist. +**RL-052**: The implementation MUST NOT create new labels on behalf of an AI agent. If `label_to_add` does not exist in the target repository, the operation MUST fail with a hard error and the message MUST be rejected. ### 8.4 Required-Labels as an Execution Gate @@ -625,14 +592,13 @@ The test suite for `replace-label` spans two layers: #### 9.2.5 Label Resolution Tests - **T-RL-040**: Verify that an existing label is resolved via REST and its node ID is used. -- **T-RL-041**: Verify that a missing label is auto-created with the correct deterministic pastel color. -- **T-RL-042**: Verify that the deterministic color algorithm produces stable, reproducible output for the same label name across invocations. +- **T-RL-041**: Verify that a missing `label_to_add` (HTTP 404) returns a hard error with `success: false`. - **T-RL-043**: Verify that the label node-ID cache prevents redundant API calls for the same label name within a run. -- **T-RL-044**: Verify that when `label_to_remove` is not on the item, `removeLabelIds` is `[]` and the operation proceeds. +- **T-RL-044**: Verify that when `label_to_remove` is not on the item, `$doRemove` is `false` and the remove field is omitted from the mutation. #### 9.2.6 GraphQL Mutation Tests -- **T-RL-050**: Verify the GraphQL mutation is called with the correct `labelableId`, `addLabelIds`, and `removeLabelIds` variables. +- **T-RL-050**: Verify the GraphQL mutation is called with the correct `labelableId`, `addLabelIds`, `removeLabelIds`, and `doRemove` variables. - **T-RL-051**: Verify that the mutation result's updated label list is logged. - **T-RL-052**: Verify that a GraphQL mutation error produces a hard error result with `success: false`. - **T-RL-053**: Verify that `$addLabelIds` always has exactly one element. @@ -668,16 +634,15 @@ The test suite for `replace-label` spans two layers: | RL-025 required-title-prefix gate | T-RL-032, T-RL-033 | 1 | Required | | RL-027 Staged mode no writes | T-RL-060 | 1 | Required | | RL-029 label_to_add resolution | T-RL-040 | 1 | Required | -| RL-030 label_to_add auto-creation | T-RL-041 | 1 | Required | +| RL-030 label_to_add not found → hard error | T-RL-041 | 1 | Required | | RL-034 Missing label_to_remove proceeds | T-RL-044 | 1 | Required | -| RL-035 Deterministic color algorithm | T-RL-042 | 1 | Required | | RL-036 Single GraphQL mutation | T-RL-050 | 1 | Required | | RL-037 Rate-limit retry on mutation | T-RL-054 | 2 | Recommended | | RL-041 addLabelIds single element | T-RL-053 | 1 | Required | -| RL-042 removeLabelIds empty when absent | T-RL-044 | 1 | Required | +| RL-042 doRemove false when label absent | T-RL-044 | 1 | Required | | RL-043 Sequential mutation semantics | T-RL-050 | 1 | Required | | RL-050 Cross-repo restrictions | T-RL-070 – T-RL-072 | 1 | Required | -| RL-052 No auto-create blocked labels | T-RL-023, T-RL-041 | 1 | Required | +| RL-052 No label auto-creation | T-RL-041 | 1 | Required | --- From fb5c9e7e28740767e3600e24877c23be46440e1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:43:03 +0000 Subject: [PATCH 09/16] fix: clarify 404 error message and RL-046 partial-data detection comment Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index d62832102e1..7bc34a822f3 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -98,7 +98,7 @@ async function resolveLabel(githubClient, owner, repo, labelName, labelNodeIdCac if (err?.status !== 404) { throw err; } - throw new Error(`Label "${labelName}" does not exist in ${owner}/${repo}. Create the label in the repository before using it with replace-label.`); + throw new Error(`Label "${labelName}" does not exist in ${owner}/${repo} (${getErrorMessage(err)}). Create the label in the repository before using it with replace-label.`); } } @@ -318,8 +318,12 @@ const main = createCountGatedHandler({ } catch (err) { const errorMessage = getErrorMessage(err); // RL-046: detect partial mutation success — remove succeeded but add failed. - // withRetry may wrap the original error via enhanceError, so check both - // err.data (direct graphql error) and err.originalError.data (wrapped error). + // withRetry may wrap the original error via enhanceError, so check both: + // err.data — present on direct @octokit/graphql GraphQLResponseError + // err.originalError.data — present when withRetry has wrapped the graphql error + // The nullish-coalescing order is intentional: prefer err.data (the closest + // error to the API boundary); fall back to err.originalError.data only when + // err.data is absent (i.e. the error has been wrapped by enhanceError). const errAsAny = /** @type {any} */ err; const partialData = errAsAny?.data ?? errAsAny?.originalError?.data; if (partialData?.removeLabels && !partialData?.addLabels) { From 7dbe530fc8056e17b34123bff200c580e2ab1bee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:52:09 +0000 Subject: [PATCH 10/16] feat: add allowed-transitions support to replace-label safe-output type Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 17 ++++++ actions/setup/js/replace_label.test.cjs | 54 +++++++++++++++++++ pkg/parser/schemas/main_workflow_schema.json | 21 ++++++++ pkg/workflow/compiler_safe_outputs_builder.go | 9 ++++ pkg/workflow/replace_label.go | 18 +++++-- pkg/workflow/safe_outputs_handler_registry.go | 5 ++ pkg/workflow/tool_description_enhancer.go | 7 +++ 7 files changed, 126 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index 7bc34a822f3..ac258598b60 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -146,8 +146,11 @@ const main = createCountGatedHandler({ // Config keys use snake_case (set by the Go handler config builder) const configAllowedAdd = Array.isArray(config.allowed_add) ? config.allowed_add : []; const configAllowedRemove = Array.isArray(config.allowed_remove) ? config.allowed_remove : []; + /** @type {{from: string, to: string}[]} */ + const configAllowedTransitions = Array.isArray(config.allowed_transitions) ? config.allowed_transitions : []; core.info(`Replace label configuration: max=${maxCount}`); + if (configAllowedTransitions.length > 0) core.info(`Allowed transitions: ${configAllowedTransitions.map(t => `"${t.from}" → "${t.to}"`).join(", ")}`); if (configAllowedAdd.length > 0) core.info(`Allowed labels to add: ${configAllowedAdd.join(", ")}`); if (configAllowedRemove.length > 0) core.info(`Allowed labels to remove: ${configAllowedRemove.join(", ")}`); if (blockedPatterns.length > 0) core.info(`Blocked patterns: ${blockedPatterns.join(", ")}`); @@ -214,6 +217,20 @@ const main = createCountGatedHandler({ return { success: false, error: addValidation.error }; } + // Validate the (from, to) pair against the allowed-transitions list. + // When allowed-transitions is configured, the pair must match at least one entry exactly. + // This check is applied after individual label validation so blocked/allowlist guards + // run first (they are security boundaries); transition validation is an additional + // state-machine constraint on top of them. + if (configAllowedTransitions.length > 0) { + const transitionAllowed = configAllowedTransitions.some(t => t.from === labelToRemove && t.to === labelToAdd); + if (!transitionAllowed) { + const error = `Transition "${labelToRemove}" → "${labelToAdd}" is not in the allowed-transitions list`; + core.warning(error); + return { success: false, error }; + } + } + // Apply required-labels and required-title-prefix filters const { data: item } = await githubClient.rest.issues.get({ owner: repoParts.owner, diff --git a/actions/setup/js/replace_label.test.cjs b/actions/setup/js/replace_label.test.cjs index 5037403eadb..f9e879dc140 100644 --- a/actions/setup/js/replace_label.test.cjs +++ b/actions/setup/js/replace_label.test.cjs @@ -246,4 +246,58 @@ describe("replace_label", () => { expect(result.success).toBe(false); expect(result.error).toContain("done"); }); + + describe("allowed-transitions", () => { + it("should allow a transition that is in the allowed-transitions list", async () => { + const handler = await main({ + allowed_transitions: [ + { from: "in-progress", to: "done" }, + { from: "pending", to: "in-progress" }, + ], + }); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(true); + }); + + it("should reject a transition that is not in the allowed-transitions list", async () => { + const handler = await main({ + allowed_transitions: [{ from: "in-progress", to: "done" }], + }); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "rejected" }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain('"in-progress" → "rejected"'); + expect(result.error).toContain("allowed-transitions"); + }); + + it("should reject a reversed transition when only forward direction is listed", async () => { + const handler = await main({ + allowed_transitions: [{ from: "in-progress", to: "done" }], + }); + // Reversed: done → in-progress is NOT in the list + const result = await handler({ label_to_remove: "done", label_to_add: "in-progress" }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain("allowed-transitions"); + }); + + it("should enforce both allowed-transitions and blocked patterns", async () => { + const handler = await main({ + allowed_transitions: [{ from: "in-progress", to: "~internal" }], + blocked: ["~*"], + }); + // Even though the transition is listed, the blocked pattern must reject it first + const result = await handler({ label_to_remove: "in-progress", label_to_add: "~internal" }, {}); + + expect(result.success).toBe(false); + }); + + it("should allow any transition when allowed-transitions is empty", async () => { + const handler = await main({ allowed_transitions: [] }); + const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); + + expect(result.success).toBe(true); + }); + }); }); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a76ba8c5968..1b85ef1edd6 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -10551,6 +10551,27 @@ "type": "object", "description": "Configuration for replacing one label with another on issues/PRs in a single atomic operation. Enables clear state transitions (e.g. 'in-progress' \u2192 'done').", "properties": { + "allowed-transitions": { + "type": "array", + "description": "Optional list of allowed label state transitions. Each entry specifies a (from, to) pair that is permitted. When specified, the agent may ONLY perform the listed transitions, regardless of allowed-add/allowed-remove. Useful for enforcing a strict state machine (e.g. only 'in-review' → 'approved' is allowed).", + "items": { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "The label that must currently be on the item and will be removed." + }, + "to": { + "type": "string", + "description": "The label that will be added." + } + }, + "required": ["from", "to"], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 100 + }, "allowed-add": { "type": "array", "description": "Optional list of allowed label patterns that can be added (supports glob patterns like 'state-*'). Labels will be created if they don't already exist in the repository. If omitted, any labels are allowed.", diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index d85c7f5f3bc..7c3dcb5b451 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -45,6 +45,15 @@ func (b *handlerConfigBuilder) AddStringSlice(key string, value []string) *handl return b } +// AddMapSlice adds a slice of string maps field only if the slice is not empty. +// Useful for structured list fields such as allowed-transitions. +func (b *handlerConfigBuilder) AddMapSlice(key string, value []map[string]string) *handlerConfigBuilder { + if len(value) > 0 { + b.config[key] = value + } + return b +} + // AddTemplatableStringSlice adds a string slice field that may contain a GitHub Actions // expression. When the slice has exactly one element and that element is a GitHub Actions // expression (as produced by preprocessStringArrayFieldAsTemplatable or diff --git a/pkg/workflow/replace_label.go b/pkg/workflow/replace_label.go index 45715de05d0..95f49725696 100644 --- a/pkg/workflow/replace_label.go +++ b/pkg/workflow/replace_label.go @@ -6,6 +6,13 @@ import ( var replaceLabelLog = logger.New("workflow:replace_label") +// LabelTransition represents an allowed label state transition. +// When allowed-transitions is configured, only listed (from → to) pairs are permitted. +type LabelTransition struct { + From string `yaml:"from"` // Label that must be present and will be removed + To string `yaml:"to"` // Label that will be added +} + // ReplaceLabelConfig holds configuration for replacing one label with another on issues/PRs from agent output. // It combines the capabilities of add-labels and remove-labels into a single GraphQL request, // enabling clear state transitions (e.g. "in-progress" → "done"). @@ -13,9 +20,10 @@ type ReplaceLabelConfig struct { BaseSafeOutputConfig `yaml:",inline"` SafeOutputTargetConfig `yaml:",inline"` SafeOutputFilterConfig `yaml:",inline"` - AllowedAdd []string `yaml:"allowed-add,omitempty"` // Optional list of allowed label patterns that can be added (supports glob patterns like "state-*"). If omitted, any labels are allowed. - AllowedRemove []string `yaml:"allowed-remove,omitempty"` // Optional list of allowed label patterns that can be removed (supports glob patterns like "state-*"). If omitted, any labels can be removed. - Blocked []string `yaml:"blocked,omitempty"` // Optional list of blocked label patterns (supports glob patterns like "~*", "*[bot]"). Applied to both add and remove labels. + AllowedAdd []string `yaml:"allowed-add,omitempty"` // Optional list of allowed label patterns that can be added (supports glob patterns like "state-*"). If omitted, any labels are allowed. + AllowedRemove []string `yaml:"allowed-remove,omitempty"` // Optional list of allowed label patterns that can be removed (supports glob patterns like "state-*"). If omitted, any labels can be removed. + Blocked []string `yaml:"blocked,omitempty"` // Optional list of blocked label patterns (supports glob patterns like "~*", "*[bot]"). Applied to both add and remove labels. + AllowedTransitions []LabelTransition `yaml:"allowed-transitions,omitempty"` // Optional list of allowed (from → to) label transitions. When specified, only these exact pairs are permitted regardless of allowed-add/allowed-remove. } // parseReplaceLabelConfig handles replace-label configuration @@ -27,8 +35,8 @@ func (c *Compiler) parseReplaceLabelConfig(outputMap map[string]any) *ReplaceLab return &ReplaceLabelConfig{} }) if config != nil { - replaceLabelLog.Printf("Parsed configuration: allowed_add_count=%d, allowed_remove_count=%d, blocked_count=%d, target=%s", - len(config.AllowedAdd), len(config.AllowedRemove), len(config.Blocked), config.Target) + replaceLabelLog.Printf("Parsed configuration: allowed_add_count=%d, allowed_remove_count=%d, blocked_count=%d, allowed_transitions_count=%d, target=%s", + len(config.AllowedAdd), len(config.AllowedRemove), len(config.Blocked), len(config.AllowedTransitions), config.Target) } return config } diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index 0d8bb2774b5..9f1f17b0275 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -174,11 +174,16 @@ var handlerRegistry = map[string]handlerBuilder{ return nil } c := cfg.ReplaceLabel + transitions := make([]map[string]string, len(c.AllowedTransitions)) + for i, t := range c.AllowedTransitions { + transitions[i] = map[string]string{"from": t.From, "to": t.To} + } config := newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddStringSlice("allowed_add", c.AllowedAdd). AddStringSlice("allowed_remove", c.AllowedRemove). AddStringSlice("blocked", c.Blocked). + AddMapSlice("allowed_transitions", transitions). AddIfNotEmpty("target", c.Target). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). diff --git a/pkg/workflow/tool_description_enhancer.go b/pkg/workflow/tool_description_enhancer.go index 764ebcae396..726c743b2dc 100644 --- a/pkg/workflow/tool_description_enhancer.go +++ b/pkg/workflow/tool_description_enhancer.go @@ -343,6 +343,13 @@ func enhanceToolDescription(toolName, baseDescription string, safeOutputs *SafeO if templatableIntValue(config.Max) > 0 { constraints = append(constraints, fmt.Sprintf("Maximum %d label replacement(s) allowed.", templatableIntValue(config.Max))) } + if len(config.AllowedTransitions) > 0 { + pairs := make([]string, len(config.AllowedTransitions)) + for i, t := range config.AllowedTransitions { + pairs[i] = fmt.Sprintf("%q → %q", t.From, t.To) + } + constraints = append(constraints, fmt.Sprintf("Only these label transitions are allowed: %s.", formatStringList(pairs))) + } if len(config.AllowedAdd) > 0 { constraints = append(constraints, fmt.Sprintf("Only these labels can be added: %s.", formatStringList(config.AllowedAdd))) } From 3362044bf72c858cdfcd75f0001c48db01818f09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:37:32 +0000 Subject: [PATCH 11/16] refactor: replace add-labels + remove-labels with replace-label in smoke workflows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-codex.lock.yml | 71 +++++++----------- .github/workflows/smoke-codex.md | 13 ++-- .../smoke-copilot-aoai-apikey.lock.yml | 73 ++++++++----------- .../workflows/smoke-copilot-aoai-apikey.md | 13 ++-- .../smoke-copilot-aoai-entra.lock.yml | 73 ++++++++----------- .github/workflows/smoke-copilot-aoai-entra.md | 13 ++-- .github/workflows/smoke-copilot-arm.lock.yml | 69 +++++++----------- .github/workflows/smoke-copilot-arm.md | 13 ++-- .github/workflows/smoke-copilot.lock.yml | 73 ++++++++----------- .github/workflows/smoke-copilot.md | 13 ++-- 10 files changed, 177 insertions(+), 247 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 32066efe4ac..642b1bffe41 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"62900eff958020333b395edbdae55c949ee9656c06079f237543837c373134c5","body_hash":"06fe382c4f4e0750f9c992b2cb9f23f50a05f3c4f79daaf4af4a35bde67b3abd","agent_id":"codex","engine_versions":{"codex":"0.140.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"63de35fe12859f423f35c756df4f9c3a794120d5ffbe4c4dc4bdb097094dc776","body_hash":"f2fe3c4aadad137ceec637f5405c932d087e6b04546ccb535aea10b089ce03a7","agent_id":"codex","engine_versions":{"codex":"0.140.0"}} # gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions-ecosystem/action-add-labels","sha":"c96b68fec76a0987cd93957189e9abd0b9a72ff1","version":"v1.1.3"},{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -323,25 +323,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' + cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' - GH_AW_PROMPT_370ba7656cfd048e_EOF + GH_AW_PROMPT_e2e18d3d905a1850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' + cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' - Tools: add_comment(max:2), create_issue, add_labels, remove_labels, unassign_from_user, hide_comment(max:5), set_issue_field, missing_tool, missing_data, noop, add_smoked_label - GH_AW_PROMPT_370ba7656cfd048e_EOF + Tools: add_comment(max:2), create_issue, replace_label, unassign_from_user, hide_comment(max:5), set_issue_field, missing_tool, missing_data, noop, add_smoked_label + GH_AW_PROMPT_e2e18d3d905a1850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' + cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' - GH_AW_PROMPT_370ba7656cfd048e_EOF + GH_AW_PROMPT_e2e18d3d905a1850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' + cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -383,12 +383,12 @@ jobs: stop immediately and report the limitation rather than spending turns trying to work around it. - GH_AW_PROMPT_370ba7656cfd048e_EOF + GH_AW_PROMPT_e2e18d3d905a1850_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' + cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' {{#runtime-import .github/workflows/shared/gh.md}} {{#runtime-import .github/workflows/shared/reporting-otlp.md}} @@ -401,7 +401,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-codex.md}} - GH_AW_PROMPT_370ba7656cfd048e_EOF + GH_AW_PROMPT_e2e18d3d905a1850_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -686,18 +686,17 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_65a8c075d253af1f_EOF' - {"add_comment":{"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-codex"]},"add_smoked_label":true,"comment_memory":{"max":1,"memory_id":"default"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-codex","expires":2,"labels":["automation","testing"],"max":1},"create_report_incomplete_issue":{},"hide_comment":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"report_incomplete":{},"set_issue_field":{"allowed_fields":["*"],"max":1},"unassign_from_user":{"allowed":["githubactionagent"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_65a8c075d253af1f_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_40bb5d289ebcf534_EOF' + {"add_comment":{"hide_older_comments":true,"max":2},"add_smoked_label":true,"comment_memory":{"max":1,"memory_id":"default"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-codex","expires":2,"labels":["automation","testing"],"max":1},"create_report_incomplete_issue":{},"hide_comment":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-codex"],"allowed_remove":["smoke"],"allowed_transitions":[{"from":"smoke","to":"smoke-codex"}]},"report_incomplete":{},"set_issue_field":{"allowed_fields":["*"],"max":1},"unassign_from_user":{"allowed":["githubactionagent"],"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_40bb5d289ebcf534_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-codex\"].", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", - "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", + "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-codex\\\"\"]. Only these labels can be added: [\"smoke-codex\"]. Only these labels can be removed: [\"smoke\"].", "set_issue_field": " CONSTRAINTS: Maximum 1 issue field update(s) can be made. Any issue field is allowed." }, "repo_params": {}, @@ -749,25 +748,6 @@ jobs: } } }, - "add_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, "comment_memory": { "defaultMax": 1, "fields": { @@ -913,18 +893,23 @@ jobs: } } }, - "remove_labels": { + "replace_label": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "labels": { + "label_to_add": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "label_to_remove": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 }, "repo": { "type": "string", @@ -2228,7 +2213,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_ACTIONS: "{\"add_smoked_label\":\"add_smoked_label\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-codex\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-codex\",\"expires\":2,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_report_incomplete_issue\":{},\"hide_comment\":{\"max\":5},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"report_incomplete\":{},\"set_issue_field\":{\"allowed_fields\":[\"*\"],\"max\":1},\"unassign_from_user\":{\"allowed\":[\"githubactionagent\"],\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-codex\",\"expires\":2,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_report_incomplete_issue\":{},\"hide_comment\":{\"max\":5},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-codex\"],\"allowed_remove\":[\"smoke\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-codex\"}]},\"report_incomplete\":{},\"set_issue_field\":{\"allowed_fields\":[\"*\"],\"max\":1},\"unassign_from_user\":{\"allowed\":[\"githubactionagent\"],\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index a5333b56660..8456a663adc 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -65,10 +65,12 @@ safe-outputs: set-issue-field: max: 1 allowed-fields: ["*"] - add-labels: - allowed: [smoke-codex] - remove-labels: - allowed: [smoke] + replace-label: + allowed-add: [smoke-codex] + allowed-remove: [smoke] + allowed-transitions: + - from: smoke + to: smoke-codex unassign-from-user: allowed: [githubactionagent] max: 1 @@ -140,8 +142,7 @@ checkout: - Overall status: PASS or FAIL If all tests pass and this workflow was triggered by a `pull_request` event: -- Use the `add_labels` safe-output tool to add the label `smoke-codex` to the pull request (use `item_number: ${{ github.event.pull_request.number }}`) -- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (use `item_number: ${{ github.event.pull_request.number }}`) +- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-codex` on the pull request (use `item_number: ${{ github.event.pull_request.number }}`) - Use the `unassign_from_user` safe-output tool to unassign the user `githubactionagent` from the pull request (this is a fictitious user used for testing; use `item_number: ${{ github.event.pull_request.number }}`) - Use the `add_smoked_label` safe-output action tool to add the label `smoked` to the pull request (call it with `{"labels": "smoked", "number": "${{ github.event.pull_request.number }}"}`) diff --git a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml index 0ca534a65cc..5675cd72500 100644 --- a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"adec50051d237ce19b106b86753422a89cf1a1370e1334f108e38307f0daf4a6","body_hash":"6112c1aeca076815d2a84819df94447ce3611986d2a41f6034877d0c160c6db5","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bda75347a82c4b2bad71fdd4a49ee062f8c3d9fcefa7a90917dc5fb00464f82d","body_hash":"7c6f2b368c82d204473ef599b4ec03036e7bbd512ef858a27597b095e066922a","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} # gh-aw-manifest: {"version":1,"secrets":["FOUNDRY_API_KEY","FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -366,25 +366,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' + cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' - GH_AW_PROMPT_763df7d7cf5ba93e_EOF + GH_AW_PROMPT_17ac1063c511310a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' + cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_763df7d7cf5ba93e_EOF + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), replace_label, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + GH_AW_PROMPT_17ac1063c511310a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' + cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' - GH_AW_PROMPT_763df7d7cf5ba93e_EOF + GH_AW_PROMPT_17ac1063c511310a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' + cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -413,12 +413,12 @@ jobs: {{/if}} - GH_AW_PROMPT_763df7d7cf5ba93e_EOF + GH_AW_PROMPT_17ac1063c511310a_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' + cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/gh.md}} @@ -431,7 +431,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot-aoai-apikey.md}} - GH_AW_PROMPT_763df7d7cf5ba93e_EOF + GH_AW_PROMPT_17ac1063c511310a_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -779,21 +779,20 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7b97d0434cfe56f6_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot-aoai-apikey"],"allowed_repos":["github/gh-aw"]},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (apikey)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} - GH_AW_SAFE_OUTPUTS_CONFIG_7b97d0434cfe56f6_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_064e1f4fd49cf414_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (apikey)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot-aoai-apikey"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot-aoai-apikey"}]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} + GH_AW_SAFE_OUTPUTS_CONFIG_064e1f4fd49cf414_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot-aoai-apikey\"].", "create_check_run": " CONSTRAINTS: Maximum 1 check run(s) can be created. Check run name: \"Smoke Copilot - AOAI (apikey)\".", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", + "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot-aoai-apikey\\\"\"]. Only these labels can be added: [\"smoke-copilot-aoai-apikey\"]. Only these labels can be removed: [\"smoke\"].", "reply_to_pull_request_review_comment": " CONSTRAINTS: Maximum 5 reply/replies can be created.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -802,7 +801,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "add_labels": { + "replace_label": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -867,25 +866,6 @@ jobs: } } }, - "add_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, "comment_memory": { "defaultMax": 1, "fields": { @@ -1086,18 +1066,23 @@ jobs: } } }, - "remove_labels": { + "replace_label": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "labels": { + "label_to_add": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "label_to_remove": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 }, "repo": { "type": "string", @@ -2900,7 +2885,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot-aoai-apikey\"],\"allowed_repos\":[\"github/gh-aw\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (apikey)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (apikey)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot-aoai-apikey\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot-aoai-apikey\"}]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot-aoai-apikey.md b/.github/workflows/smoke-copilot-aoai-apikey.md index fe416382454..d2a612a09d1 100644 --- a/.github/workflows/smoke-copilot-aoai-apikey.md +++ b/.github/workflows/smoke-copilot-aoai-apikey.md @@ -93,11 +93,13 @@ safe-outputs: submit-pull-request-review: reply-to-pull-request-review-comment: max: 5 - add-labels: - allowed: [smoke-copilot-aoai-apikey] + replace-label: + allowed-add: [smoke-copilot-aoai-apikey] + allowed-remove: [smoke] allowed-repos: ["github/gh-aw"] - remove-labels: - allowed: [smoke] + allowed-transitions: + - from: smoke + to: smoke-copilot-aoai-apikey set-issue-type: dispatch-workflow: workflows: @@ -233,8 +235,7 @@ Run each check NOW and mark as ✅/❌. Do NOT create files to automate this — 5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass and this workflow was triggered by a pull_request event: -- Use the `add_labels` safe-output tool to add the label `smoke-copilot-aoai-apikey` to the pull request (omit the `item_number` parameter to auto-target the triggering PR) -- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot-aoai-apikey` on the pull request (omit the `item_number` parameter to auto-target the triggering PR) {{#runtime-import shared/noop-reminder.md}} diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index f058bd0a489..86a19c01375 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8a372ae4d246382cd40293696d36b43ecd62009994f2c19160710a768c3ea291","body_hash":"28391c920f1e2f0de620798cc3026b8a4ab4412ee5a9d9d0a158f5a06fc976f5","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ad94d3102b5fee4a2a3e3ab794c52469a275b51cb8ad52f5bd5e36b46d51f970","body_hash":"8a2d059201a3201d4a3f321c56142610f391371f471ef271d7538af98807019d","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} # gh-aw-manifest: {"version":1,"secrets":["FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -365,25 +365,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' + cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' - GH_AW_PROMPT_9d9b05f821ba5ebb_EOF + GH_AW_PROMPT_3aba86ca617741a0_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' + cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_9d9b05f821ba5ebb_EOF + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), replace_label, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + GH_AW_PROMPT_3aba86ca617741a0_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' + cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' - GH_AW_PROMPT_9d9b05f821ba5ebb_EOF + GH_AW_PROMPT_3aba86ca617741a0_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' + cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -412,12 +412,12 @@ jobs: {{/if}} - GH_AW_PROMPT_9d9b05f821ba5ebb_EOF + GH_AW_PROMPT_3aba86ca617741a0_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' + cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/gh.md}} @@ -430,7 +430,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot-aoai-entra.md}} - GH_AW_PROMPT_9d9b05f821ba5ebb_EOF + GH_AW_PROMPT_3aba86ca617741a0_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -780,21 +780,20 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6cc1d02bf954b2e9_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot-aoai-entra"],"allowed_repos":["github/gh-aw"]},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (Entra)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} - GH_AW_SAFE_OUTPUTS_CONFIG_6cc1d02bf954b2e9_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b288fbf7c4ec69d0_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (Entra)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot-aoai-entra"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot-aoai-entra"}]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} + GH_AW_SAFE_OUTPUTS_CONFIG_b288fbf7c4ec69d0_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot-aoai-entra\"].", "create_check_run": " CONSTRAINTS: Maximum 1 check run(s) can be created. Check run name: \"Smoke Copilot - AOAI (Entra)\".", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", + "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot-aoai-entra\\\"\"]. Only these labels can be added: [\"smoke-copilot-aoai-entra\"]. Only these labels can be removed: [\"smoke\"].", "reply_to_pull_request_review_comment": " CONSTRAINTS: Maximum 5 reply/replies can be created.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -803,7 +802,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "add_labels": { + "replace_label": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -868,25 +867,6 @@ jobs: } } }, - "add_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, "comment_memory": { "defaultMax": 1, "fields": { @@ -1087,18 +1067,23 @@ jobs: } } }, - "remove_labels": { + "replace_label": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "labels": { + "label_to_add": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "label_to_remove": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 }, "repo": { "type": "string", @@ -2911,7 +2896,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot-aoai-entra\"],\"allowed_repos\":[\"github/gh-aw\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (Entra)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (Entra)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot-aoai-entra\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot-aoai-entra\"}]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot-aoai-entra.md b/.github/workflows/smoke-copilot-aoai-entra.md index 8aaf8646573..d14c7ef0675 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.md +++ b/.github/workflows/smoke-copilot-aoai-entra.md @@ -100,11 +100,13 @@ safe-outputs: submit-pull-request-review: reply-to-pull-request-review-comment: max: 5 - add-labels: - allowed: [smoke-copilot-aoai-entra] + replace-label: + allowed-add: [smoke-copilot-aoai-entra] + allowed-remove: [smoke] allowed-repos: ["github/gh-aw"] - remove-labels: - allowed: [smoke] + allowed-transitions: + - from: smoke + to: smoke-copilot-aoai-entra set-issue-type: dispatch-workflow: workflows: @@ -240,8 +242,7 @@ Run each check NOW and mark as ✅/❌. Do NOT create files to automate this — 5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass and this workflow was triggered by a pull_request event: -- Use the `add_labels` safe-output tool to add the label `smoke-copilot-aoai-entra` to the pull request (omit the `item_number` parameter to auto-target the triggering PR) -- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot-aoai-entra` on the pull request (omit the `item_number` parameter to auto-target the triggering PR) {{#runtime-import shared/noop-reminder.md}} diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index 23e802331bf..760a85898cf 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0372a070145eab26489a17238aaa81aaba8dbc554970c2f2bd5676ea60dabe4","body_hash":"26c548a1139a15cdaed6d80164ccd26e682ca333ff33ce2e701aa7e60337e0e6","agent_id":"copilot","engine_versions":{"copilot":"1.0.63"},"agent_image_runner":"ubuntu-24.04-arm"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a9f2da39bca9ad93058e439c8e1600c1394abd7d7f5d94d700f47c194d8ca3b8","body_hash":"2caa1b835f92ebfb936b1e1ebfb5299917de7c899911318b65f49dbbc7274be4","agent_id":"copilot","engine_versions":{"copilot":"1.0.63"},"agent_image_runner":"ubuntu-24.04-arm"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -320,22 +320,22 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' + cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' - GH_AW_PROMPT_6fee0e2dcfafe586_EOF + GH_AW_PROMPT_b245fafb12a8a7c2_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' + cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, add_labels, remove_labels, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, replace_label, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_6fee0e2dcfafe586_EOF + GH_AW_PROMPT_b245fafb12a8a7c2_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' + cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -364,12 +364,12 @@ jobs: {{/if}} - GH_AW_PROMPT_6fee0e2dcfafe586_EOF + GH_AW_PROMPT_b245fafb12a8a7c2_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' + cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' {{#runtime-import .github/workflows/shared/gh.md}} {{#runtime-import .github/workflows/shared/reporting-otlp.md}} @@ -382,7 +382,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot-arm.md}} - GH_AW_PROMPT_6fee0e2dcfafe586_EOF + GH_AW_PROMPT_b245fafb12a8a7c2_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -715,20 +715,19 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2f0487f43e4d8733_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot-arm"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-arm","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_2f0487f43e4d8733_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e9da21bbcd6560b7_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"create_discussion":{"category":"announcements","close_older_discussions":true,"expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-arm","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot-arm"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot-arm"}]},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"submit_pull_request_review":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_e9da21bbcd6560b7_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot-arm\"].", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", + "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot-arm\\\"\"]. Only these labels can be added: [\"smoke-copilot-arm\"]. Only these labels can be removed: [\"smoke\"].", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, "repo_params": { @@ -736,7 +735,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "add_labels": { + "replace_label": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -801,25 +800,6 @@ jobs: } } }, - "add_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, "create_discussion": { "defaultMax": 1, "fields": { @@ -995,18 +975,23 @@ jobs: } } }, - "remove_labels": { + "replace_label": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "labels": { + "label_to_add": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "label_to_remove": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 }, "repo": { "type": "string", @@ -2671,7 +2656,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot-arm\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-arm\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-arm\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot-arm\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot-arm\"}]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot-arm.md b/.github/workflows/smoke-copilot-arm.md index b4bf0ab9ede..b14a9a60e2d 100644 --- a/.github/workflows/smoke-copilot-arm.md +++ b/.github/workflows/smoke-copilot-arm.md @@ -69,11 +69,13 @@ safe-outputs: create-pull-request-review-comment: max: 5 submit-pull-request-review: - add-labels: - allowed: [smoke-copilot-arm] + replace-label: + allowed-add: [smoke-copilot-arm] + allowed-remove: [smoke] allowed-repos: ["github/gh-aw"] - remove-labels: - allowed: [smoke] + allowed-transitions: + - from: smoke + to: smoke-copilot-arm dispatch-workflow: workflows: - haiku-printer @@ -168,7 +170,6 @@ strict: false 4. Use the `send_slack_message` tool to send a brief summary message (e.g., "ARM64 smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass: -- Use the `add_labels` safe-output tool to add the label `smoke-copilot-arm` to the pull request -- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request +- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot-arm` on the pull request {{#runtime-import shared/noop-reminder.md}} \ No newline at end of file diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 53774424d7b..0ae772848ea 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6669d3fc0bdfdd7a9e54e10d8c1e41c04dc17e1d162f2e639dabdb1255ee8a78","body_hash":"3ed9a0835f964798b12a6c4c8600addbbf66518ae974a9e6ed7efefc76b0dc43","agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.63"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"69de9ca0cf180b886f15c11918e6cb883374fe68b30844a406de62206e4ada5d","body_hash":"90f5c03342d569af98c3f5e3a033a77227d920a3c3cbcc7b850a00585840fe0c","agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.63"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -371,25 +371,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' + cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' - GH_AW_PROMPT_0dd8e4888c4252ce_EOF + GH_AW_PROMPT_d63047d65119b660_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' + cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_0dd8e4888c4252ce_EOF + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), replace_label, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + GH_AW_PROMPT_d63047d65119b660_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' + cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' - GH_AW_PROMPT_0dd8e4888c4252ce_EOF + GH_AW_PROMPT_d63047d65119b660_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' + cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -418,12 +418,12 @@ jobs: {{/if}} - GH_AW_PROMPT_0dd8e4888c4252ce_EOF + GH_AW_PROMPT_d63047d65119b660_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' + cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/gh.md}} @@ -436,7 +436,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot.md}} - GH_AW_PROMPT_0dd8e4888c4252ce_EOF + GH_AW_PROMPT_d63047d65119b660_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -784,21 +784,20 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2f2ebc5159973ed6_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} - GH_AW_SAFE_OUTPUTS_CONFIG_2f2ebc5159973ed6_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3db27f0c94ac7d18_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot"}]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} + GH_AW_SAFE_OUTPUTS_CONFIG_3db27f0c94ac7d18_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot\"].", "create_check_run": " CONSTRAINTS: Maximum 1 check run(s) can be created. Check run name: \"Smoke Copilot\".", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", + "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot\\\"\"]. Only these labels can be added: [\"smoke-copilot\"]. Only these labels can be removed: [\"smoke\"].", "reply_to_pull_request_review_comment": " CONSTRAINTS: Maximum 5 reply/replies can be created.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -807,7 +806,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "add_labels": { + "replace_label": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -872,25 +871,6 @@ jobs: } } }, - "add_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, "comment_memory": { "defaultMax": 1, "fields": { @@ -1091,18 +1071,23 @@ jobs: } } }, - "remove_labels": { + "replace_label": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "labels": { + "label_to_add": { "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "label_to_remove": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 }, "repo": { "type": "string", @@ -2857,7 +2842,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot\"}]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index e5fd95e1f03..990864834ed 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -96,11 +96,13 @@ safe-outputs: submit-pull-request-review: reply-to-pull-request-review-comment: max: 5 - add-labels: - allowed: [smoke-copilot] + replace-label: + allowed-add: [smoke-copilot] + allowed-remove: [smoke] allowed-repos: ["github/gh-aw"] - remove-labels: - allowed: [smoke] + allowed-transitions: + - from: smoke + to: smoke-copilot set-issue-type: dispatch-workflow: workflows: @@ -216,8 +218,7 @@ Run these checks and mark each as ✅/❌: 5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass and this workflow was triggered by a pull_request event: -- Use the `add_labels` safe-output tool to add the label `smoke-copilot` to the pull request (omit the `item_number` parameter to auto-target the triggering PR) -- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot` on the pull request (omit the `item_number` parameter to auto-target the triggering PR) {{#runtime-import shared/noop-reminder.md}} From 4195b8f0b652ef3e7609f0b7f3dd35c490259628 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:49:35 +0000 Subject: [PATCH 12/16] revert: restore smoke workflows to add-labels + remove-labels Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-codex.lock.yml | 71 +++++++++++------- .github/workflows/smoke-codex.md | 13 ++-- .../smoke-copilot-aoai-apikey.lock.yml | 73 +++++++++++-------- .../workflows/smoke-copilot-aoai-apikey.md | 13 ++-- .../smoke-copilot-aoai-entra.lock.yml | 73 +++++++++++-------- .github/workflows/smoke-copilot-aoai-entra.md | 13 ++-- .github/workflows/smoke-copilot-arm.lock.yml | 69 +++++++++++------- .github/workflows/smoke-copilot-arm.md | 13 ++-- .github/workflows/smoke-copilot.lock.yml | 73 +++++++++++-------- .github/workflows/smoke-copilot.md | 13 ++-- 10 files changed, 247 insertions(+), 177 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 642b1bffe41..32066efe4ac 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"63de35fe12859f423f35c756df4f9c3a794120d5ffbe4c4dc4bdb097094dc776","body_hash":"f2fe3c4aadad137ceec637f5405c932d087e6b04546ccb535aea10b089ce03a7","agent_id":"codex","engine_versions":{"codex":"0.140.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"62900eff958020333b395edbdae55c949ee9656c06079f237543837c373134c5","body_hash":"06fe382c4f4e0750f9c992b2cb9f23f50a05f3c4f79daaf4af4a35bde67b3abd","agent_id":"codex","engine_versions":{"codex":"0.140.0"}} # gh-aw-manifest: {"version":1,"secrets":["CODEX_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN","OPENAI_API_KEY"],"actions":[{"repo":"actions-ecosystem/action-add-labels","sha":"c96b68fec76a0987cd93957189e9abd0b9a72ff1","version":"v1.1.3"},{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -323,25 +323,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' + cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' - GH_AW_PROMPT_e2e18d3d905a1850_EOF + GH_AW_PROMPT_370ba7656cfd048e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' + cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' - Tools: add_comment(max:2), create_issue, replace_label, unassign_from_user, hide_comment(max:5), set_issue_field, missing_tool, missing_data, noop, add_smoked_label - GH_AW_PROMPT_e2e18d3d905a1850_EOF + Tools: add_comment(max:2), create_issue, add_labels, remove_labels, unassign_from_user, hide_comment(max:5), set_issue_field, missing_tool, missing_data, noop, add_smoked_label + GH_AW_PROMPT_370ba7656cfd048e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' + cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' - GH_AW_PROMPT_e2e18d3d905a1850_EOF + GH_AW_PROMPT_370ba7656cfd048e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' + cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -383,12 +383,12 @@ jobs: stop immediately and report the limitation rather than spending turns trying to work around it. - GH_AW_PROMPT_e2e18d3d905a1850_EOF + GH_AW_PROMPT_370ba7656cfd048e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_e2e18d3d905a1850_EOF' + cat << 'GH_AW_PROMPT_370ba7656cfd048e_EOF' {{#runtime-import .github/workflows/shared/gh.md}} {{#runtime-import .github/workflows/shared/reporting-otlp.md}} @@ -401,7 +401,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-codex.md}} - GH_AW_PROMPT_e2e18d3d905a1850_EOF + GH_AW_PROMPT_370ba7656cfd048e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -686,17 +686,18 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_40bb5d289ebcf534_EOF' - {"add_comment":{"hide_older_comments":true,"max":2},"add_smoked_label":true,"comment_memory":{"max":1,"memory_id":"default"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-codex","expires":2,"labels":["automation","testing"],"max":1},"create_report_incomplete_issue":{},"hide_comment":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-codex"],"allowed_remove":["smoke"],"allowed_transitions":[{"from":"smoke","to":"smoke-codex"}]},"report_incomplete":{},"set_issue_field":{"allowed_fields":["*"],"max":1},"unassign_from_user":{"allowed":["githubactionagent"],"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_40bb5d289ebcf534_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_65a8c075d253af1f_EOF' + {"add_comment":{"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-codex"]},"add_smoked_label":true,"comment_memory":{"max":1,"memory_id":"default"},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-codex","expires":2,"labels":["automation","testing"],"max":1},"create_report_incomplete_issue":{},"hide_comment":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"report_incomplete":{},"set_issue_field":{"allowed_fields":["*"],"max":1},"unassign_from_user":{"allowed":["githubactionagent"],"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_65a8c075d253af1f_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-codex\"].", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", - "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-codex\\\"\"]. Only these labels can be added: [\"smoke-codex\"]. Only these labels can be removed: [\"smoke\"].", + "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", "set_issue_field": " CONSTRAINTS: Maximum 1 issue field update(s) can be made. Any issue field is allowed." }, "repo_params": {}, @@ -748,6 +749,25 @@ jobs: } } }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "comment_memory": { "defaultMax": 1, "fields": { @@ -893,23 +913,18 @@ jobs: } } }, - "replace_label": { + "remove_labels": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "label_to_add": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "label_to_remove": { + "labels": { "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 }, "repo": { "type": "string", @@ -2213,7 +2228,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_ACTIONS: "{\"add_smoked_label\":\"add_smoked_label\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-codex\",\"expires\":2,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_report_incomplete_issue\":{},\"hide_comment\":{\"max\":5},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-codex\"],\"allowed_remove\":[\"smoke\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-codex\"}]},\"report_incomplete\":{},\"set_issue_field\":{\"allowed_fields\":[\"*\"],\"max\":1},\"unassign_from_user\":{\"allowed\":[\"githubactionagent\"],\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-codex\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-codex\",\"expires\":2,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_report_incomplete_issue\":{},\"hide_comment\":{\"max\":5},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"report_incomplete\":{},\"set_issue_field\":{\"allowed_fields\":[\"*\"],\"max\":1},\"unassign_from_user\":{\"allowed\":[\"githubactionagent\"],\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index 8456a663adc..a5333b56660 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -65,12 +65,10 @@ safe-outputs: set-issue-field: max: 1 allowed-fields: ["*"] - replace-label: - allowed-add: [smoke-codex] - allowed-remove: [smoke] - allowed-transitions: - - from: smoke - to: smoke-codex + add-labels: + allowed: [smoke-codex] + remove-labels: + allowed: [smoke] unassign-from-user: allowed: [githubactionagent] max: 1 @@ -142,7 +140,8 @@ checkout: - Overall status: PASS or FAIL If all tests pass and this workflow was triggered by a `pull_request` event: -- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-codex` on the pull request (use `item_number: ${{ github.event.pull_request.number }}`) +- Use the `add_labels` safe-output tool to add the label `smoke-codex` to the pull request (use `item_number: ${{ github.event.pull_request.number }}`) +- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (use `item_number: ${{ github.event.pull_request.number }}`) - Use the `unassign_from_user` safe-output tool to unassign the user `githubactionagent` from the pull request (this is a fictitious user used for testing; use `item_number: ${{ github.event.pull_request.number }}`) - Use the `add_smoked_label` safe-output action tool to add the label `smoked` to the pull request (call it with `{"labels": "smoked", "number": "${{ github.event.pull_request.number }}"}`) diff --git a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml index 5675cd72500..0ca534a65cc 100644 --- a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bda75347a82c4b2bad71fdd4a49ee062f8c3d9fcefa7a90917dc5fb00464f82d","body_hash":"7c6f2b368c82d204473ef599b4ec03036e7bbd512ef858a27597b095e066922a","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"adec50051d237ce19b106b86753422a89cf1a1370e1334f108e38307f0daf4a6","body_hash":"6112c1aeca076815d2a84819df94447ce3611986d2a41f6034877d0c160c6db5","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} # gh-aw-manifest: {"version":1,"secrets":["FOUNDRY_API_KEY","FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -366,25 +366,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' + cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' - GH_AW_PROMPT_17ac1063c511310a_EOF + GH_AW_PROMPT_763df7d7cf5ba93e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' + cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), replace_label, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_17ac1063c511310a_EOF + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + GH_AW_PROMPT_763df7d7cf5ba93e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' + cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' - GH_AW_PROMPT_17ac1063c511310a_EOF + GH_AW_PROMPT_763df7d7cf5ba93e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' + cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -413,12 +413,12 @@ jobs: {{/if}} - GH_AW_PROMPT_17ac1063c511310a_EOF + GH_AW_PROMPT_763df7d7cf5ba93e_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_17ac1063c511310a_EOF' + cat << 'GH_AW_PROMPT_763df7d7cf5ba93e_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/gh.md}} @@ -431,7 +431,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot-aoai-apikey.md}} - GH_AW_PROMPT_17ac1063c511310a_EOF + GH_AW_PROMPT_763df7d7cf5ba93e_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -779,20 +779,21 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_064e1f4fd49cf414_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (apikey)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot-aoai-apikey"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot-aoai-apikey"}]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} - GH_AW_SAFE_OUTPUTS_CONFIG_064e1f4fd49cf414_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7b97d0434cfe56f6_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot-aoai-apikey"],"allowed_repos":["github/gh-aw"]},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (apikey)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-apikey","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} + GH_AW_SAFE_OUTPUTS_CONFIG_7b97d0434cfe56f6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot-aoai-apikey\"].", "create_check_run": " CONSTRAINTS: Maximum 1 check run(s) can be created. Check run name: \"Smoke Copilot - AOAI (apikey)\".", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot-aoai-apikey\\\"\"]. Only these labels can be added: [\"smoke-copilot-aoai-apikey\"]. Only these labels can be removed: [\"smoke\"].", + "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", "reply_to_pull_request_review_comment": " CONSTRAINTS: Maximum 5 reply/replies can be created.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -801,7 +802,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "replace_label": { + "add_labels": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -866,6 +867,25 @@ jobs: } } }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "comment_memory": { "defaultMax": 1, "fields": { @@ -1066,23 +1086,18 @@ jobs: } } }, - "replace_label": { + "remove_labels": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "label_to_add": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "label_to_remove": { + "labels": { "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 }, "repo": { "type": "string", @@ -2885,7 +2900,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (apikey)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot-aoai-apikey\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot-aoai-apikey\"}]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot-aoai-apikey\"],\"allowed_repos\":[\"github/gh-aw\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (apikey)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-apikey\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot-aoai-apikey.md b/.github/workflows/smoke-copilot-aoai-apikey.md index d2a612a09d1..fe416382454 100644 --- a/.github/workflows/smoke-copilot-aoai-apikey.md +++ b/.github/workflows/smoke-copilot-aoai-apikey.md @@ -93,13 +93,11 @@ safe-outputs: submit-pull-request-review: reply-to-pull-request-review-comment: max: 5 - replace-label: - allowed-add: [smoke-copilot-aoai-apikey] - allowed-remove: [smoke] + add-labels: + allowed: [smoke-copilot-aoai-apikey] allowed-repos: ["github/gh-aw"] - allowed-transitions: - - from: smoke - to: smoke-copilot-aoai-apikey + remove-labels: + allowed: [smoke] set-issue-type: dispatch-workflow: workflows: @@ -235,7 +233,8 @@ Run each check NOW and mark as ✅/❌. Do NOT create files to automate this — 5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass and this workflow was triggered by a pull_request event: -- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot-aoai-apikey` on the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `add_labels` safe-output tool to add the label `smoke-copilot-aoai-apikey` to the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (omit the `item_number` parameter to auto-target the triggering PR) {{#runtime-import shared/noop-reminder.md}} diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index 86a19c01375..f058bd0a489 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ad94d3102b5fee4a2a3e3ab794c52469a275b51cb8ad52f5bd5e36b46d51f970","body_hash":"8a2d059201a3201d4a3f321c56142610f391371f471ef271d7538af98807019d","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8a372ae4d246382cd40293696d36b43ecd62009994f2c19160710a768c3ea291","body_hash":"28391c920f1e2f0de620798cc3026b8a4ab4412ee5a9d9d0a158f5a06fc976f5","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.63"}} # gh-aw-manifest: {"version":1,"secrets":["FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -365,25 +365,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' + cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' - GH_AW_PROMPT_3aba86ca617741a0_EOF + GH_AW_PROMPT_9d9b05f821ba5ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' + cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), replace_label, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_3aba86ca617741a0_EOF + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + GH_AW_PROMPT_9d9b05f821ba5ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' + cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' - GH_AW_PROMPT_3aba86ca617741a0_EOF + GH_AW_PROMPT_9d9b05f821ba5ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' + cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -412,12 +412,12 @@ jobs: {{/if}} - GH_AW_PROMPT_3aba86ca617741a0_EOF + GH_AW_PROMPT_9d9b05f821ba5ebb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_3aba86ca617741a0_EOF' + cat << 'GH_AW_PROMPT_9d9b05f821ba5ebb_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/gh.md}} @@ -430,7 +430,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot-aoai-entra.md}} - GH_AW_PROMPT_3aba86ca617741a0_EOF + GH_AW_PROMPT_9d9b05f821ba5ebb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -780,20 +780,21 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b288fbf7c4ec69d0_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (Entra)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot-aoai-entra"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot-aoai-entra"}]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} - GH_AW_SAFE_OUTPUTS_CONFIG_b288fbf7c4ec69d0_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6cc1d02bf954b2e9_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot-aoai-entra"],"allowed_repos":["github/gh-aw"]},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot - AOAI (Entra)"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-aoai-entra","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} + GH_AW_SAFE_OUTPUTS_CONFIG_6cc1d02bf954b2e9_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot-aoai-entra\"].", "create_check_run": " CONSTRAINTS: Maximum 1 check run(s) can be created. Check run name: \"Smoke Copilot - AOAI (Entra)\".", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot-aoai-entra\\\"\"]. Only these labels can be added: [\"smoke-copilot-aoai-entra\"]. Only these labels can be removed: [\"smoke\"].", + "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", "reply_to_pull_request_review_comment": " CONSTRAINTS: Maximum 5 reply/replies can be created.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -802,7 +803,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "replace_label": { + "add_labels": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -867,6 +868,25 @@ jobs: } } }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "comment_memory": { "defaultMax": 1, "fields": { @@ -1067,23 +1087,18 @@ jobs: } } }, - "replace_label": { + "remove_labels": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "label_to_add": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "label_to_remove": { + "labels": { "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 }, "repo": { "type": "string", @@ -2896,7 +2911,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (Entra)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot-aoai-entra\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot-aoai-entra\"}]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot-aoai-entra\"],\"allowed_repos\":[\"github/gh-aw\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot - AOAI (Entra)\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-aoai-entra\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot-aoai-entra.md b/.github/workflows/smoke-copilot-aoai-entra.md index d14c7ef0675..8aaf8646573 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.md +++ b/.github/workflows/smoke-copilot-aoai-entra.md @@ -100,13 +100,11 @@ safe-outputs: submit-pull-request-review: reply-to-pull-request-review-comment: max: 5 - replace-label: - allowed-add: [smoke-copilot-aoai-entra] - allowed-remove: [smoke] + add-labels: + allowed: [smoke-copilot-aoai-entra] allowed-repos: ["github/gh-aw"] - allowed-transitions: - - from: smoke - to: smoke-copilot-aoai-entra + remove-labels: + allowed: [smoke] set-issue-type: dispatch-workflow: workflows: @@ -242,7 +240,8 @@ Run each check NOW and mark as ✅/❌. Do NOT create files to automate this — 5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass and this workflow was triggered by a pull_request event: -- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot-aoai-entra` on the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `add_labels` safe-output tool to add the label `smoke-copilot-aoai-entra` to the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (omit the `item_number` parameter to auto-target the triggering PR) {{#runtime-import shared/noop-reminder.md}} diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index 760a85898cf..23e802331bf 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a9f2da39bca9ad93058e439c8e1600c1394abd7d7f5d94d700f47c194d8ca3b8","body_hash":"2caa1b835f92ebfb936b1e1ebfb5299917de7c899911318b65f49dbbc7274be4","agent_id":"copilot","engine_versions":{"copilot":"1.0.63"},"agent_image_runner":"ubuntu-24.04-arm"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0372a070145eab26489a17238aaa81aaba8dbc554970c2f2bd5676ea60dabe4","body_hash":"26c548a1139a15cdaed6d80164ccd26e682ca333ff33ce2e701aa7e60337e0e6","agent_id":"copilot","engine_versions":{"copilot":"1.0.63"},"agent_image_runner":"ubuntu-24.04-arm"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -320,22 +320,22 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' + cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' - GH_AW_PROMPT_b245fafb12a8a7c2_EOF + GH_AW_PROMPT_6fee0e2dcfafe586_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' + cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, replace_label, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, add_labels, remove_labels, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_b245fafb12a8a7c2_EOF + GH_AW_PROMPT_6fee0e2dcfafe586_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' + cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -364,12 +364,12 @@ jobs: {{/if}} - GH_AW_PROMPT_b245fafb12a8a7c2_EOF + GH_AW_PROMPT_6fee0e2dcfafe586_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_b245fafb12a8a7c2_EOF' + cat << 'GH_AW_PROMPT_6fee0e2dcfafe586_EOF' {{#runtime-import .github/workflows/shared/gh.md}} {{#runtime-import .github/workflows/shared/reporting-otlp.md}} @@ -382,7 +382,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot-arm.md}} - GH_AW_PROMPT_b245fafb12a8a7c2_EOF + GH_AW_PROMPT_6fee0e2dcfafe586_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -715,19 +715,20 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e9da21bbcd6560b7_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"create_discussion":{"category":"announcements","close_older_discussions":true,"expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-arm","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot-arm"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot-arm"}]},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"submit_pull_request_review":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_e9da21bbcd6560b7_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2f0487f43e4d8733_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot-arm"],"allowed_repos":["github/gh-aw"]},"create_discussion":{"category":"announcements","close_older_discussions":true,"expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-arm","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"submit_pull_request_review":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_2f0487f43e4d8733_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot-arm\"].", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot-arm\\\"\"]. Only these labels can be added: [\"smoke-copilot-arm\"]. Only these labels can be removed: [\"smoke\"].", + "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, "repo_params": { @@ -735,7 +736,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "replace_label": { + "add_labels": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -800,6 +801,25 @@ jobs: } } }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "create_discussion": { "defaultMax": 1, "fields": { @@ -975,23 +995,18 @@ jobs: } } }, - "replace_label": { + "remove_labels": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "label_to_add": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "label_to_remove": { + "labels": { "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 }, "repo": { "type": "string", @@ -2656,7 +2671,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-arm\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot-arm\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot-arm\"}]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot-arm\"],\"allowed_repos\":[\"github/gh-aw\"]},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-arm\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot-arm.md b/.github/workflows/smoke-copilot-arm.md index b14a9a60e2d..b4bf0ab9ede 100644 --- a/.github/workflows/smoke-copilot-arm.md +++ b/.github/workflows/smoke-copilot-arm.md @@ -69,13 +69,11 @@ safe-outputs: create-pull-request-review-comment: max: 5 submit-pull-request-review: - replace-label: - allowed-add: [smoke-copilot-arm] - allowed-remove: [smoke] + add-labels: + allowed: [smoke-copilot-arm] allowed-repos: ["github/gh-aw"] - allowed-transitions: - - from: smoke - to: smoke-copilot-arm + remove-labels: + allowed: [smoke] dispatch-workflow: workflows: - haiku-printer @@ -170,6 +168,7 @@ strict: false 4. Use the `send_slack_message` tool to send a brief summary message (e.g., "ARM64 smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass: -- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot-arm` on the pull request +- Use the `add_labels` safe-output tool to add the label `smoke-copilot-arm` to the pull request +- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request {{#runtime-import shared/noop-reminder.md}} \ No newline at end of file diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 0ae772848ea..53774424d7b 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"69de9ca0cf180b886f15c11918e6cb883374fe68b30844a406de62206e4ada5d","body_hash":"90f5c03342d569af98c3f5e3a033a77227d920a3c3cbcc7b850a00585840fe0c","agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.63"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6669d3fc0bdfdd7a9e54e10d8c1e41c04dc17e1d162f2e639dabdb1255ee8a78","body_hash":"3ed9a0835f964798b12a6c4c8600addbbf66518ae974a9e6ed7efefc76b0dc43","agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.63"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7","digest":"sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.7@sha256:aae231e4635c8999d039c132f1602d3df850fe9b84a00aa2b5ac981179b5661c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7","digest":"sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.7@sha256:009caf2e3d88fa77b64e9a03a95a228fc58db0f1701c6d324b29ba5a3c7c79b6"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7","digest":"sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.7@sha256:4757f198a3fa20f88bdbe70be7ae1a05f127d9c0a9e96a5d6460ef40c08fc83d"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7","digest":"sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.7@sha256:deb1d4e19de62d51cee0508057a596a19315c3423ada4d675cad136dc8037c96"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.27","digest":"sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.27@sha256:fe984bddde4ec05d756d9043edb0a32912e6b7b72f6a121b1082f29221421cc7"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.3.0","digest":"sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80","pinned_image":"ghcr.io/github/github-mcp-server:v1.3.0@sha256:5c83359327a0bacc3d34db730bea6557d39d341cee0bf6c58c9a896e33150e80"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -371,25 +371,25 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' + cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' - GH_AW_PROMPT_d63047d65119b660_EOF + GH_AW_PROMPT_0dd8e4888c4252ce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' + cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' - Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), replace_label, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message - GH_AW_PROMPT_d63047d65119b660_EOF + Tools: add_comment(max:2), create_issue, create_discussion, create_pull_request_review_comment(max:5), submit_pull_request_review, reply_to_pull_request_review_comment(max:5), add_labels, remove_labels, create_check_run, set_issue_type, dispatch_workflow, missing_tool, missing_data, noop, send_slack_message + GH_AW_PROMPT_0dd8e4888c4252ce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_comment_memory.md" - cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' + cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' - GH_AW_PROMPT_d63047d65119b660_EOF + GH_AW_PROMPT_0dd8e4888c4252ce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' + cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -418,12 +418,12 @@ jobs: {{/if}} - GH_AW_PROMPT_d63047d65119b660_EOF + GH_AW_PROMPT_0dd8e4888c4252ce_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" fi - cat << 'GH_AW_PROMPT_d63047d65119b660_EOF' + cat << 'GH_AW_PROMPT_0dd8e4888c4252ce_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/gh.md}} @@ -436,7 +436,7 @@ jobs: Serena is enabled for **["go"]** in `__GH_AW_GITHUB_WORKSPACE__`. Start by calling `activate_project` with that workspace path, then prefer Serena semantic tools for symbol lookup, references, docs, diagnostics, and structured edits. {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/smoke-copilot.md}} - GH_AW_PROMPT_d63047d65119b660_EOF + GH_AW_PROMPT_0dd8e4888c4252ce_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -784,20 +784,21 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs/upload-artifacts" - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3db27f0c94ac7d18_EOF' - {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"replace_label":{"allowed_add":["smoke-copilot"],"allowed_remove":["smoke"],"allowed_repos":["github/gh-aw"],"allowed_transitions":[{"from":"smoke","to":"smoke-copilot"}]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} - GH_AW_SAFE_OUTPUTS_CONFIG_3db27f0c94ac7d18_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2f2ebc5159973ed6_EOF' + {"add_comment":{"allowed_repos":["github/gh-aw"],"hide_older_comments":true,"max":2},"add_labels":{"allowed":["smoke-copilot"],"allowed_repos":["github/gh-aw"]},"comment_memory":{"max":1,"memory_id":"default"},"create_check_run":{"max":1,"name":"Smoke Copilot"},"create_discussion":{"category":"announcements","close_older_discussions":true,"close_older_key":"smoke-copilot","expires":2,"fallback_to_issue":true,"labels":["ai-generated"],"max":1},"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_pull_request_review_comment":{"max":5,"side":"RIGHT"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"haiku-printer":".yml"},"workflows":["haiku-printer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"remove_labels":{"allowed":["smoke"]},"reply_to_pull_request_review_comment":{"max":5},"report_incomplete":{},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"description":"The message to send","required":false,"type":"string"}},"output":"Slack message stub executed!"},"set_issue_type":{},"submit_pull_request_review":{"max":1},"upload_artifact":{"max-size-bytes":104857600,"max-uploads":1,"retention-days":1,"skip-archive":true}} + GH_AW_SAFE_OUTPUTS_CONFIG_2f2ebc5159973ed6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"smoke-copilot\"].", "create_check_run": " CONSTRAINTS: Maximum 1 check run(s) can be created. Check run name: \"Smoke Copilot\".", "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Discussions will be created in category \"announcements\".", "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added.", "create_pull_request_review_comment": " CONSTRAINTS: Maximum 5 review comment(s) can be created. Comments will be on the RIGHT side of the diff.", - "replace_label": " CONSTRAINTS: Only these label transitions are allowed: [\"\\\"smoke\\\" → \\\"smoke-copilot\\\"\"]. Only these labels can be added: [\"smoke-copilot\"]. Only these labels can be removed: [\"smoke\"].", + "remove_labels": " CONSTRAINTS: Only these labels can be removed: [smoke].", "reply_to_pull_request_review_comment": " CONSTRAINTS: Maximum 5 reply/replies can be created.", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted." }, @@ -806,7 +807,7 @@ jobs: "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" }, - "replace_label": { + "add_labels": { "description": "Target repository for this operation in 'owner/repo' format. Must be the target-repo or in the allowed-repos list.", "type": "string" } @@ -871,6 +872,25 @@ jobs: } } }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "comment_memory": { "defaultMax": 1, "fields": { @@ -1071,23 +1091,18 @@ jobs: } } }, - "replace_label": { + "remove_labels": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, - "label_to_add": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "label_to_remove": { + "labels": { "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 }, "repo": { "type": "string", @@ -2842,7 +2857,7 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} GH_AW_SAFE_OUTPUT_JOBS: "{\"send_slack_message\":\"\"}" - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"replace_label\":{\"allowed_add\":[\"smoke-copilot\"],\"allowed_remove\":[\"smoke\"],\"allowed_repos\":[\"github/gh-aw\"],\"allowed_transitions\":[{\"from\":\"smoke\",\"to\":\"smoke-copilot\"}]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"allowed_repos\":[\"github/gh-aw\"],\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"],\"allowed_repos\":[\"github/gh-aw\"]},\"comment_memory\":{\"max\":1,\"memory_id\":\"default\"},\"create_check_run\":{\"max\":1,\"name\":\"Smoke Copilot\"},\"create_discussion\":{\"category\":\"announcements\",\"close_older_discussions\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"fallback_to_issue\":true,\"labels\":[\"ai-generated\"],\"max\":1},\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_pull_request_review_comment\":{\"max\":5,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"haiku-printer\":\".yml\"},\"workflows\":[\"haiku-printer\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"remove_labels\":{\"allowed\":[\"smoke\"]},\"reply_to_pull_request_review_comment\":{\"max\":5},\"report_incomplete\":{},\"set_issue_type\":{},\"submit_pull_request_review\":{\"max\":1},\"upload_artifact\":{\"max-size-bytes\":104857600,\"max-uploads\":1,\"retention-days\":1,\"skip-archive\":true}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 990864834ed..e5fd95e1f03 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -96,13 +96,11 @@ safe-outputs: submit-pull-request-review: reply-to-pull-request-review-comment: max: 5 - replace-label: - allowed-add: [smoke-copilot] - allowed-remove: [smoke] + add-labels: + allowed: [smoke-copilot] allowed-repos: ["github/gh-aw"] - allowed-transitions: - - from: smoke - to: smoke-copilot + remove-labels: + allowed: [smoke] set-issue-type: dispatch-workflow: workflows: @@ -218,7 +216,8 @@ Run these checks and mark each as ✅/❌: 5. Use the `send_slack_message` tool to send a brief summary message (e.g., "Smoke test ${{ github.run_id }}: All tests passed! ✅") If all tests pass and this workflow was triggered by a pull_request event: -- Use the `replace_label` safe-output tool to replace the label `smoke` with `smoke-copilot` on the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `add_labels` safe-output tool to add the label `smoke-copilot` to the pull request (omit the `item_number` parameter to auto-target the triggering PR) +- Use the `remove_labels` safe-output tool to remove the label `smoke` from the pull request (omit the `item_number` parameter to auto-target the triggering PR) {{#runtime-import shared/noop-reminder.md}} From f8e35c634a8b86220b7628d529c33e300c340e26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:57:32 +0000 Subject: [PATCH 13/16] fix: add parentheses to JSDoc type casts in replace_label.cjs to fix TypeScript errors Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index ac258598b60..dc7bf964991 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -83,7 +83,7 @@ const { resolveInvocationContext } = require("./invocation_context_helpers.cjs") */ async function resolveLabel(githubClient, owner, repo, labelName, labelNodeIdCache) { if (labelNodeIdCache.has(labelName)) { - return /** @type {string} */ labelNodeIdCache.get(labelName); + return /** @type {string} */ (labelNodeIdCache.get(labelName)); } try { @@ -270,7 +270,7 @@ const main = createCountGatedHandler({ if (!repoCaches.has(itemRepo)) { repoCaches.set(itemRepo, new Map()); } - const labelNodeIdCache = /** @type {Map} */ repoCaches.get(itemRepo); + const labelNodeIdCache = /** @type {Map} */ (repoCaches.get(itemRepo)); // Resolve the node ID of label_to_add — fails with hard error if the label does not exist let addLabelNodeId; From 6929d6c3916770117116081591d8cad035afb62d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:09:33 +0000 Subject: [PATCH 14/16] fix: replace JSDoc casts with undefined-safe patterns to fix typecheck + prettier conflict in replace_label.cjs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index dc7bf964991..dad8b476495 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -82,8 +82,9 @@ const { resolveInvocationContext } = require("./invocation_context_helpers.cjs") * @returns {Promise} The GraphQL node ID of the label */ async function resolveLabel(githubClient, owner, repo, labelName, labelNodeIdCache) { - if (labelNodeIdCache.has(labelName)) { - return /** @type {string} */ (labelNodeIdCache.get(labelName)); + const cached = labelNodeIdCache.get(labelName); + if (cached !== undefined) { + return cached; } try { @@ -267,10 +268,11 @@ const main = createCountGatedHandler({ } // Get or initialize the per-repo label cache - if (!repoCaches.has(itemRepo)) { - repoCaches.set(itemRepo, new Map()); + let labelNodeIdCache = repoCaches.get(itemRepo); + if (!labelNodeIdCache) { + labelNodeIdCache = new Map(); + repoCaches.set(itemRepo, labelNodeIdCache); } - const labelNodeIdCache = /** @type {Map} */ (repoCaches.get(itemRepo)); // Resolve the node ID of label_to_add — fails with hard error if the label does not exist let addLabelNodeId; From ca882f8973bbf445eb7ef1e47d8a8f693e827e93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:21:53 +0000 Subject: [PATCH 15/16] feat: mark replace-label safe output as experimental Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_validators.go | 1 + ...replace_label_experimental_warning_test.go | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 pkg/workflow/replace_label_experimental_warning_test.go diff --git a/pkg/workflow/compiler_validators.go b/pkg/workflow/compiler_validators.go index bf7732c5fc9..4a0a529e9a4 100644 --- a/pkg/workflow/compiler_validators.go +++ b/pkg/workflow/compiler_validators.go @@ -301,6 +301,7 @@ func (c *Compiler) emitExperimentalFeatureWarnings(workflowData *WorkflowData) { {enabled: workflowData.RateLimit != nil, message: "Using experimental feature: rate limiting"}, {enabled: workflowData.SafeOutputs != nil && workflowData.SafeOutputs.DispatchRepository != nil, message: "Using experimental feature: dispatch_repository"}, {enabled: workflowData.SafeOutputs != nil && workflowData.SafeOutputs.MergePullRequest != nil, message: "Using experimental feature: merge-pull-request"}, + {enabled: workflowData.SafeOutputs != nil && workflowData.SafeOutputs.ReplaceLabel != nil, message: "Using experimental feature: replace-label"}, {enabled: workflowData.EngineConfig != nil && workflowData.EngineConfig.CopilotSDK, message: "Using experimental feature: engine.copilot-sdk"}, {enabled: isFeatureEnabled(constants.GHAWDetectionFeatureFlag, workflowData), message: "Using experimental feature: gh-aw-detection"}, } diff --git a/pkg/workflow/replace_label_experimental_warning_test.go b/pkg/workflow/replace_label_experimental_warning_test.go new file mode 100644 index 00000000000..7acbcf7d250 --- /dev/null +++ b/pkg/workflow/replace_label_experimental_warning_test.go @@ -0,0 +1,94 @@ +//go:build integration + +package workflow + +import ( + "bytes" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/github/gh-aw/pkg/testutil" +) + +func TestReplaceLabelExperimentalWarning(t *testing.T) { + tests := []struct { + name string + content string + expectWarning bool + }{ + { + name: "replace-label enabled produces experimental warning", + content: `--- +on: workflow_dispatch +engine: copilot +safe-outputs: + replace-label: + allowed-add: ["approved"] + allowed-remove: ["in-review"] +--- + +# Test Workflow +`, + expectWarning: true, + }, + { + name: "no replace-label does not produce experimental warning", + content: `--- +on: workflow_dispatch +engine: copilot +--- + +# Test Workflow +`, + expectWarning: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "replace-label-experimental-warning-test") + + testFile := filepath.Join(tmpDir, "test-workflow.md") + if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil { + t.Fatal(err) + } + + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + compiler := NewCompiler() + compiler.SetStrictMode(false) + err := compiler.CompileWorkflow(testFile) + + w.Close() + os.Stderr = oldStderr + var buf bytes.Buffer + io.Copy(&buf, r) + stderrOutput := buf.String() + + if err != nil { + t.Errorf("expected compilation to succeed but it failed: %v", err) + return + } + + expectedMessage := "Using experimental feature: replace-label" + if tt.expectWarning { + if !strings.Contains(stderrOutput, expectedMessage) { + t.Errorf("expected warning containing %q, got stderr:\n%s", expectedMessage, stderrOutput) + } + if compiler.GetWarningCount() == 0 { + t.Error("expected warning count > 0 but got 0") + } + return + } + + if strings.Contains(stderrOutput, expectedMessage) { + t.Errorf("did not expect warning %q, but got stderr:\n%s", expectedMessage, stderrOutput) + } + }) + } +} From c391662b84fb410a022aaddf78db425472118124 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:44:32 +0000 Subject: [PATCH 16/16] refactor: switch replace_label from GraphQL mutation to single REST setLabels call Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/replace_label.cjs | 144 +++-------------- actions/setup/js/replace_label.test.cjs | 65 +------- specs/replace-label-spec.md | 202 ++++++++++-------------- 3 files changed, 108 insertions(+), 303 deletions(-) diff --git a/actions/setup/js/replace_label.cjs b/actions/setup/js/replace_label.cjs index dad8b476495..b4cb6fb4d35 100644 --- a/actions/setup/js/replace_label.cjs +++ b/actions/setup/js/replace_label.cjs @@ -22,43 +22,6 @@ /** @type {string} Safe output type handled by this module */ const HANDLER_TYPE = "replace_label"; -/** - * GraphQL mutation that removes one label and adds another in a single request. - * Root mutations in a single request are executed sequentially (remove first, then add). - * When $doRemove is false the removeLabels field is skipped entirely, avoiding a - * GitHub API error that would result from passing an empty labelIds array. - * - * Note: this is a sequential operation, not a transaction. If removeLabels succeeds - * but addLabels fails the removal is not reversed (see RL-046 partial-failure handling). - * - * @type {string} - */ -const REPLACE_LABEL_MUTATION = /* GraphQL */ ` - mutation ReplaceLabelMutation($labelableId: ID!, $addLabelIds: [ID!]!, $removeLabelIds: [ID!]!, $doRemove: Boolean!) { - removeLabels: removeLabelsFromLabelable(input: { labelableId: $labelableId, labelIds: $removeLabelIds }) @include(if: $doRemove) { - clientMutationId - } - addLabels: addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $addLabelIds }) { - labelable { - ... on Issue { - labels(first: 25) { - nodes { - name - } - } - } - ... on PullRequest { - labels(first: 25) { - nodes { - name - } - } - } - } - } - } -`; - const { matchesSimpleGlob } = require("./glob_pattern_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); @@ -70,39 +33,6 @@ const { createCountGatedHandler } = require("./handler_scaffold.cjs"); const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); -/** - * Resolve a label in the repository, returning its GraphQL node ID. - * If the label does not exist in the repository, a hard error is thrown. - * - * @param {any} githubClient - Authenticated GitHub client with REST - * @param {string} owner - Repository owner - * @param {string} repo - Repository name - * @param {string} labelName - Label name to resolve - * @param {Map} labelNodeIdCache - Cache of label name → node_id - * @returns {Promise} The GraphQL node ID of the label - */ -async function resolveLabel(githubClient, owner, repo, labelName, labelNodeIdCache) { - const cached = labelNodeIdCache.get(labelName); - if (cached !== undefined) { - return cached; - } - - try { - const { data: label } = await githubClient.rest.issues.getLabel({ - owner, - repo, - name: labelName, - }); - labelNodeIdCache.set(labelName, label.node_id); - return label.node_id; - } catch (err) { - if (err?.status !== 404) { - throw err; - } - throw new Error(`Label "${labelName}" does not exist in ${owner}/${repo} (${getErrorMessage(err)}). Create the label in the repository before using it with replace-label.`); - } -} - /** * Validate a single label against blocked and allowed-list patterns. * Uses explicit rejection semantics — does not silently filter or truncate the label name. @@ -132,7 +62,7 @@ function validateSingleLabel(labelName, allowedPatterns, blockedPatterns, fieldN /** * Main handler factory for replace_label. - * Uses the GraphQL API to remove one label and add another in a single request. + * Uses a single REST API call (`issues.setLabels`) to replace one label with another. * @type {HandlerFactoryFunction} */ const main = createCountGatedHandler({ @@ -160,10 +90,6 @@ const main = createCountGatedHandler({ core.info(`Default target repo: ${defaultTargetRepo}`); if (allowedRepos.size > 0) core.info(`Allowed repos: ${[...allowedRepos].join(", ")}`); - /** Cache of repo label name → node_id, keyed per repo to avoid cross-repo conflicts */ - /** @type {Map>} */ - const repoCaches = new Map(); - /** * Message handler function that processes a single replace_label message. * @param {ReplaceLabelMessage} message - The replace_label message to process @@ -267,54 +193,33 @@ const main = createCountGatedHandler({ }; } - // Get or initialize the per-repo label cache - let labelNodeIdCache = repoCaches.get(itemRepo); - if (!labelNodeIdCache) { - labelNodeIdCache = new Map(); - repoCaches.set(itemRepo, labelNodeIdCache); - } - - // Resolve the node ID of label_to_add — fails with hard error if the label does not exist - let addLabelNodeId; - try { - addLabelNodeId = await withRetry(() => resolveLabel(githubClient, repoParts.owner, repoParts.repo, labelToAdd, labelNodeIdCache), RATE_LIMIT_RETRY_CONFIG, `resolve label "${labelToAdd}" in ${itemRepo}`); - } catch (err) { - const errorMessage = getErrorMessage(err); - core.error(`Failed to resolve label "${labelToAdd}": ${errorMessage}`); - return { success: false, error: `Failed to resolve label "${labelToAdd}": ${errorMessage}` }; - } - - // Find the node ID of label_to_remove from the issue's current labels. - // If the label is not on the issue we can still proceed (just won't remove anything). - const currentLabelMap = new Map((item.labels || []).map(/** @param {any} l */ l => [l.name || "", l.node_id || ""])); - const removeLabelNodeId = currentLabelMap.get(labelToRemove); - - if (!removeLabelNodeId) { + // Compute the new label set: current labels minus labelToRemove, plus labelToAdd (deduped). + // If labelToRemove is not on the issue we still proceed — it simply won't appear in the set. + const currentLabelNames = (item.labels || []).map(/** @param {any} l */ l => (typeof l === "string" ? l : l.name || "")).filter(Boolean); + const labelToRemoveIsPresent = currentLabelNames.includes(labelToRemove); + if (!labelToRemoveIsPresent) { core.info(`Label "${labelToRemove}" is not present on ${contextType} #${itemNumber} in ${itemRepo} — will only add "${labelToAdd}"`); } + const newLabelNames = [...new Set([...currentLabelNames.filter(n => n !== labelToRemove), labelToAdd])]; - // Issue node_id for the GraphQL mutation - const labelableId = item.node_id; - - core.info(`Executing combined GraphQL mutation: remove="${labelToRemove}", add="${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); + core.info(`Executing REST setLabels: remove="${labelToRemove}", add="${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); const beforeState = await fetchIssueState(githubClient, repoParts, itemNumber); try { - const mutationResult = await withRetry( + const { data: updatedLabels } = await withRetry( () => - githubClient.graphql(REPLACE_LABEL_MUTATION, { - labelableId, - addLabelIds: [addLabelNodeId], - removeLabelIds: removeLabelNodeId ? [removeLabelNodeId] : [], - doRemove: !!removeLabelNodeId, + githubClient.rest.issues.setLabels({ + owner: repoParts.owner, + repo: repoParts.repo, + issue_number: itemNumber, + labels: newLabelNames, }), RATE_LIMIT_RETRY_CONFIG, `replace_label on ${contextType} #${itemNumber} in ${itemRepo}` ); - const updatedLabels = mutationResult?.addLabels?.labelable?.labels?.nodes || []; - const updatedLabelNames = updatedLabels.map((/** @param {any} l */ l) => l.name || "").filter(Boolean); + const updatedLabelNames = (updatedLabels || []).map((/** @param {any} l */ l) => l.name || "").filter(Boolean); core.info(`Successfully replaced label "${labelToRemove}" → "${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); core.info(`Updated labels: ${JSON.stringify(updatedLabelNames)}`); @@ -324,7 +229,7 @@ const main = createCountGatedHandler({ success: true, number: itemNumber, repo: itemRepo, - labelRemoved: removeLabelNodeId ? labelToRemove : null, + labelRemoved: labelToRemoveIsPresent ? labelToRemove : null, labelAdded: labelToAdd, contextType, }, @@ -336,24 +241,11 @@ const main = createCountGatedHandler({ ); } catch (err) { const errorMessage = getErrorMessage(err); - // RL-046: detect partial mutation success — remove succeeded but add failed. - // withRetry may wrap the original error via enhanceError, so check both: - // err.data — present on direct @octokit/graphql GraphQLResponseError - // err.originalError.data — present when withRetry has wrapped the graphql error - // The nullish-coalescing order is intentional: prefer err.data (the closest - // error to the API boundary); fall back to err.originalError.data only when - // err.data is absent (i.e. the error has been wrapped by enhanceError). - const errAsAny = /** @type {any} */ err; - const partialData = errAsAny?.data ?? errAsAny?.originalError?.data; - if (partialData?.removeLabels && !partialData?.addLabels) { - core.error(`Partial mutation failure on ${contextType} #${itemNumber} in ${itemRepo}: "${labelToRemove}" was removed but "${labelToAdd}" could not be added: ${errorMessage}`); - } else { - core.error(`Failed to replace label: ${errorMessage}`); - } + core.error(`Failed to replace label: ${errorMessage}`); return { success: false, error: errorMessage }; } }; }, }); -module.exports = { main, REPLACE_LABEL_MUTATION }; +module.exports = { main }; diff --git a/actions/setup/js/replace_label.test.cjs b/actions/setup/js/replace_label.test.cjs index f9e879dc140..dea3e0ef26a 100644 --- a/actions/setup/js/replace_label.test.cjs +++ b/actions/setup/js/replace_label.test.cjs @@ -45,38 +45,11 @@ describe("replace_label", () => { node_id: "I_issue_789", }, }), - getLabel: async ({ name }) => { - const labels = { - done: { name: "done", node_id: "LA_done_789", color: "0075ca" }, - "in-progress": { name: "in-progress", node_id: "LA_in_progress_123", color: "e4e669" }, - }; - if (labels[name]) { - return { data: labels[name] }; - } - const err = new Error("Not Found"); - err.status = 404; - throw err; - }, - createLabel: async ({ name, color }) => ({ - data: { name, node_id: `LA_${name}_new`, color }, + setLabels: async ({ labels }) => ({ + data: labels.map(name => ({ name, node_id: `LA_${name}_id` })), }), }, }, - graphql: async (mutation, variables) => { - if (mutation.includes("ReplaceLabelMutation")) { - return { - removeLabels: { clientMutationId: null }, - addLabels: { - labelable: { - labels: { - nodes: [{ name: "done" }], - }, - }, - }, - }; - } - throw new Error("Unknown query"); - }, }; mockContext = { @@ -106,9 +79,9 @@ describe("replace_label", () => { }); it("should return error when label_to_add does not exist in the repo", async () => { - mockGithub.rest.issues.getLabel = async () => { - const err = new Error("Not Found"); - err.status = 404; + mockGithub.rest.issues.setLabels = async () => { + const err = new Error("Validation Failed"); + err.status = 422; throw err; }; @@ -116,7 +89,6 @@ describe("replace_label", () => { const result = await handler({ label_to_remove: "in-progress", label_to_add: "needs-review" }, {}); expect(result.success).toBe(false); - expect(result.error).toContain("needs-review"); }); it("should succeed even when label_to_remove is not present on the issue", async () => { @@ -215,36 +187,15 @@ describe("replace_label", () => { expect(result.success).toBe(false); }); - it("should log partial failure and return error when remove succeeds but add fails (RL-046)", async () => { - const gqlError = new Error("add mutation failed"); - // @octokit/graphql populates err.data with the partial response when some fields succeed - gqlError.data = { - removeLabels: { clientMutationId: null }, // remove succeeded - // addLabels absent — simulates add failure after remove succeeded - }; - mockGithub.graphql = async () => { - throw gqlError; - }; - - const handler = await main({}); - const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); - - expect(result.success).toBe(false); - expect(mockCore.errors.some(e => e.includes("in-progress") && e.includes("done"))).toBe(true); - }); - - it("should return error when getLabel fails with a non-404 error", async () => { - mockGithub.rest.issues.getLabel = async () => { - const err = new Error("Service unavailable"); - err.status = 503; - throw err; + it("should return error when setLabels API call fails", async () => { + mockGithub.rest.issues.setLabels = async () => { + throw new Error("Service unavailable"); }; const handler = await main({}); const result = await handler({ label_to_remove: "in-progress", label_to_add: "done" }, {}); expect(result.success).toBe(false); - expect(result.error).toContain("done"); }); describe("allowed-transitions", () => { diff --git a/specs/replace-label-spec.md b/specs/replace-label-spec.md index 6ba6a8749f7..c67c57ff097 100644 --- a/specs/replace-label-spec.md +++ b/specs/replace-label-spec.md @@ -17,9 +17,9 @@ sidebar: ## Abstract -This specification defines the `replace-label` safe-output type for GitHub Agentic Workflows (gh-aw), a mechanism that enables AI agents to transition label state on GitHub issues and pull requests in a single GraphQL request. The `replace-label` type removes one label and adds another in a single GraphQL request, eliminating the HTTP round-trip between the two operations that would otherwise exist when using `remove-labels` and `add-labels` as separate sequential messages. +This specification defines the `replace-label` safe-output type for GitHub Agentic Workflows (gh-aw), a mechanism that enables AI agents to transition label state on GitHub issues and pull requests in a single REST API call. The `replace-label` type removes one label and adds another in a single `PUT /repos/{owner}/{repo}/issues/{issue_number}/labels` REST call, eliminating the HTTP round-trip between the two operations that would otherwise exist when using `remove-labels` and `add-labels` as separate sequential messages. -The specification covers the configuration schema, the message schema produced by AI agents, the multi-stage validation pipeline, the label-resolution mechanism, the GraphQL mutation executed against the GitHub API, error-handling requirements, security controls, and conformance testing requirements. +The specification covers the configuration schema, the message schema produced by AI agents, the multi-stage validation pipeline, the REST API call executed against the GitHub API, error-handling requirements, security controls, and conformance testing requirements. ## Status of This Document @@ -36,7 +36,7 @@ This is a Candidate Recommendation specification representing the design and imp 3. [Concepts and Terminology](#3-concepts-and-terminology) 4. [Data Model](#4-data-model) 5. [Processing Model](#5-processing-model) -6. [GraphQL Interface](#6-graphql-interface) +6. [REST Interface](#6-rest-interface) 7. [Error Handling](#7-error-handling) 8. [Security Considerations](#8-security-considerations) 9. [Compliance Testing](#9-compliance-testing) @@ -52,7 +52,7 @@ This is a Candidate Recommendation specification representing the design and imp Label-based state machines are a common pattern in GitHub issue and pull request workflows. A triage issue may progress from `pending` → `in-review` → `approved`, or a PR review cycle may move from `needs-revision` → `ready-to-merge`. When these transitions are driven by AI agents operating through gh-aw, the canonical implementation using separate `remove-labels` and `add-labels` safe-output messages introduces a race window: between the removal and the addition, the item carries no label — or may be picked up by a concurrent automation that considers the intermediate label-less state valid. -The `replace-label` type solves this by combining the remove and add operations into a single GraphQL request. The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order, guaranteeing that the removal is applied before the addition. This eliminates the HTTP round-trip between the two operations, reducing the window in which an external observer could see the item in an intermediate state. Note that GitHub does not provide rollback semantics: if the remove succeeds but the add fails, the state is partially transitioned (see §7.2). +The `replace-label` type solves this by combining the remove and add operations into a single REST API call (`PUT /repos/{owner}/{repo}/issues/{issue_number}/labels`). This endpoint replaces the entire label set of the target item atomically — the new desired state (current labels minus the removed label, plus the added label) is applied in a single request. This eliminates the HTTP round-trip between the two operations and provides true atomicity: either the entire label set update succeeds or it fails, with no intermediate state where neither label is present. ### 1.2 Scope @@ -60,8 +60,8 @@ This specification covers: - The YAML configuration schema for the `replace-label` key within the `safe-outputs` frontmatter block - The JSON message schema produced by AI agents when requesting a label replacement -- The multi-stage processing pipeline: schema validation, label allowlist/blocklist enforcement, required-label and title-prefix gate checks, staged-mode preview, label node-ID resolution and auto-creation, and GraphQL mutation execution -- The exact GraphQL mutation used against the GitHub API +- The multi-stage processing pipeline: schema validation, label allowlist/blocklist enforcement, required-label and title-prefix gate checks, staged-mode preview, label set computation, and REST API call execution +- The exact REST API call used against the GitHub API - Rate-limit retry semantics and error propagation - Security controls, including cross-repository access restrictions - Conformance requirements and test procedures @@ -82,7 +82,7 @@ The `replace-label` type is designed to satisfy the following goals: 1. **Reduced race window**: Remove and add operations MUST execute in a single GitHub API round-trip to eliminate the observable intermediate state compared to two separate requests. 2. **Idempotency on missing source label**: If the label to be removed is not present on the target item, the operation MUST still add the new label and succeed, rather than failing. 3. **Least privilege**: Allowlist-based configuration MUST constrain which labels agents may add or remove, limiting the blast radius of a misbehaving agent. -4. **Pre-existing labels**: Labels referenced in `label_to_add` MUST already exist in the repository. The operation MUST fail with a clear error if the label is missing, avoiding silent repository side effects. +4. **Pre-existing labels**: Labels referenced in `label_to_add` MUST already exist in the repository; the REST `setLabels` call will fail with a 422 error if the label is missing. 5. **Safe preview**: Staged mode MUST allow operators to review what the agent would do without applying any changes to the GitHub API. 6. **Consistency with the safe-outputs framework**: Configuration fields (`max`, `target`, `target-repo`, `allowed-repos`, `github-token`, `staged`, `required-labels`, `required-title-prefix`) MUST follow the same semantics as all other safe-output types in gh-aw. @@ -111,11 +111,9 @@ Normative requirements are additionally identified by a short requirement code o ## 3. Concepts and Terminology -**Label replacement**: The operation of removing one named label from a GitHub labelable item and adding a different named label in the same GitHub GraphQL API request. +**Label replacement**: The operation of removing one named label from a GitHub labelable item and adding a different named label in the same GitHub REST API call (`PUT /repos/{owner}/{repo}/issues/{issue_number}/labels`). -**Labelable**: A GitHub resource that can carry labels. In this specification, labelable items are GitHub Issues and GitHub Pull Requests. The GitHub GraphQL type `Labelable` is the interface implemented by both. - -**Label node ID**: The globally unique GraphQL node ID assigned by GitHub to each label object within a repository (e.g., `LA_kwDOABCD123`). Node IDs are required by the GraphQL mutation API and differ from the integer REST API label ID. +**Labelable**: A GitHub resource that can carry labels. In this specification, labelable items are GitHub Issues and GitHub Pull Requests. **Label allowlist** (`allowed-add`, `allowed-remove`): Optional glob pattern lists in the workflow configuration that constrain which labels an agent may add or remove. When absent, no label-name restriction applies. @@ -267,13 +265,14 @@ A conforming implementation MUST execute the following pipeline for each `replac ▼ ┌─────────────────────┐ │ Stage 7 │ - │ Label Resolution │ ← resolve labels (fail if not found) + │ Label Set │ ← compute new label set (current - remove + add) + │ Computation │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Stage 8 │ - │ GraphQL Mutation │ ← single request (sequential) + │ REST setLabels │ ← single REST call (PUT /issues/{n}/labels) └─────────────────────┘ ``` @@ -373,37 +372,27 @@ Gate checks require fetching the current state of the target item via the GitHub **RL-028**: Staged mode execution MUST return `{ success: true, staged: true }` from the handler. Note: the operation count IS incremented for staged messages by the `createCountGatedHandler` scaffold (which counts every processed message before delegating to the handler), so staged messages count toward the `max` budget. -### 5.7 Stage 7: Label Resolution - -Label resolution converts human-readable label names into the GraphQL node IDs required by the mutation. The implementation maintains a per-repository in-memory cache of `labelName → nodeId` to avoid redundant API calls within a single workflow run. - -#### 5.7.1 Resolving `label_to_add` - -**RL-029**: The implementation MUST attempt to resolve the `label_to_add` name to a GraphQL node ID using the GitHub REST API (`GET /repos/{owner}/{repo}/labels/{name}`). - -**RL-030**: When the label does not exist in the target repository (HTTP 404), the implementation MUST return a hard error and the message MUST be rejected. Labels referenced in `label_to_add` MUST be created in the repository before use. +### 5.7 Stage 7: Label Set Computation -**RL-031**: Label resolution MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy defined in `actions/setup/js/error_recovery.cjs`. +The implementation derives the new label set from the current labels already attached to the target item (returned by the gate-check REST call in Stage 5). No additional API calls are required at this stage. -**RL-032**: If resolution fails for any reason other than a rate-limit response, the implementation MUST return a hard error and the message MUST be rejected. +**RL-029**: The implementation MUST compute the new label set as: `(current_labels − {label_to_remove}) ∪ {label_to_add}`, deduplicating the result. Label names MUST be preserved exactly as they appear on the item; no truncation or normalisation is applied. -#### 5.7.2 Resolving `label_to_remove` +**RL-033**: The presence of `label_to_remove` on the item MUST be determined from the current labels returned by the `GET /repos/{owner}/{repo}/issues/{issue_number}` REST call already performed in Stage 5. -**RL-033**: The node ID of `label_to_remove` MUST be looked up from the current labels already attached to the target item (returned by the gate-check REST call in Stage 5), rather than from a separate API call. +**RL-034**: When `label_to_remove` is not currently attached to the target item, the implementation MUST proceed with the operation, producing a new label set that simply adds `label_to_add` without removing anything. The operation MUST NOT fail in this case (see §1.3, Design Goal 2). -**RL-034**: When `label_to_remove` is not currently attached to the target item, the implementation MUST proceed with the `add` operation only, setting `$doRemove: false` so that the `removeLabelsFromLabelable` field is omitted from the mutation via the `@include(if: $doRemove)` directive. The operation MUST NOT fail in this case (see §1.3, Design Goal 2). +### 5.8 Stage 8: REST Label Update -### 5.8 Stage 8: GraphQL Mutation +**RL-036**: The implementation MUST execute the label replacement using a single `PUT /repos/{owner}/{repo}/issues/{issue_number}/labels` REST API call (`issues.setLabels`), passing the complete new label set computed in Stage 7. Separate add and remove calls MUST NOT be used. -**RL-036**: The implementation MUST execute the label replacement using the single GraphQL mutation defined in §6 rather than two separate REST or GraphQL API calls. +**RL-037**: The REST call MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy. -**RL-037**: The mutation MUST be retried on GitHub API rate-limit responses using the `RATE_LIMIT_RETRY_CONFIG` policy. - -**RL-038**: On successful mutation, the implementation MUST log: +**RL-038**: On a successful call, the implementation MUST log: - The item type (issue or pull request), item number, and repository - The label that was removed (or a note that the source label was absent and no removal occurred) - The label that was added -- The complete updated label set returned by the `addLabels` mutation result +- The complete updated label set returned by the REST response **RL-039**: The handler result for a successful operation MUST include the fields `success: true`, `number`, `repo`, `labelRemoved` (null when the source label was absent), `labelAdded`, and `contextType`. @@ -411,61 +400,36 @@ Label resolution converts human-readable label names into the GraphQL node IDs r --- -## 6. GraphQL Interface - -### 6.1 Mutation - -The following GraphQL mutation is the normative interface used by the `replace-label` handler to execute label replacement operations. - -```graphql -mutation ReplaceLabelMutation( - $labelableId: ID! - $addLabelIds: [ID!]! - $removeLabelIds: [ID!]! - $doRemove: Boolean! -) { - removeLabels: removeLabelsFromLabelable( - input: { labelableId: $labelableId, labelIds: $removeLabelIds } - ) @include(if: $doRemove) { - clientMutationId - } - addLabels: addLabelsToLabelable( - input: { labelableId: $labelableId, labelIds: $addLabelIds } - ) { - labelable { - ... on Issue { - labels(first: 25) { - nodes { name } - } - } - ... on PullRequest { - labels(first: 25) { - nodes { name } - } - } - } - } -} +## 6. REST Interface + +### 6.1 API Call + +The label replacement is executed via a single REST API call: + ``` +PUT /repos/{owner}/{repo}/issues/{issue_number}/labels +``` + +In the Octokit client this is `githubClient.rest.issues.setLabels(params)`. -#### 6.1.1 Variable Bindings +#### 6.1.1 Parameters -| Variable | Type | Source | -|----------|------|--------| -| `$labelableId` | `ID!` | GraphQL node ID of the target issue or pull request (`item.node_id` from REST response) | -| `$addLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_add`, as resolved in Stage 7 | -| `$removeLabelIds` | `[ID!]!` | Array containing the single node ID of `label_to_remove` when present on the item; empty array otherwise (ignored when `$doRemove` is `false`) | -| `$doRemove` | `Boolean!` | `true` when `label_to_remove` is currently on the item; `false` to skip the remove field via `@include` | +| Parameter | Source | +|-----------|--------| +| `owner` | Repository owner, resolved from target configuration | +| `repo` | Repository name, resolved from target configuration | +| `issue_number` | Target item number, resolved in Stage 3 | +| `labels` | New label name array computed in Stage 7 | -**RL-041**: `$addLabelIds` MUST always contain exactly one element. +**RL-041**: The `labels` array MUST contain all labels that should be present on the item after the operation: current labels minus `label_to_remove` (if present), plus `label_to_add`, deduplicated. -**RL-042**: `$doRemove` MUST be `true` (and `$removeLabelIds` MUST contain exactly one element) when the label to remove is currently on the target item. When the label is absent, `$doRemove` MUST be `false` so the `removeLabelsFromLabelable` field is omitted from the request via the `@include(if: $doRemove)` directive. +**RL-042**: `label_to_add` MUST always appear exactly once in the `labels` array (after deduplication). ### 6.2 Execution Semantics -**RL-043**: The GitHub GraphQL API executes root-level mutations within a single request sequentially in declaration order. When `$doRemove` is `true`, `removeLabelsFromLabelable` executes before `addLabelsToLabelable` within the same request, preventing any external observer from reading the item in a state where neither label is present. +**RL-043**: The `PUT /repos/{owner}/{repo}/issues/{issue_number}/labels` REST call replaces the entire label set of the target item in a single atomic operation. Either the new label set is applied successfully or the call fails — there is no intermediate state where neither label is present. -> **Informative note**: This is a sequential operation in a single HTTP request, not a database transaction. GitHub does not provide rollback semantics: if `removeLabels` succeeds but `addLabels` fails, the removal is not reversed. Implementations MUST log this partial-success condition clearly when it occurs (see §7.2, RL-046). +> **Informative note**: Unlike the former GraphQL approach (two root mutations in one request), this REST call provides true atomicity: both the removal and addition are reflected by a single server-side update. There is no partial-success scenario (see §7.2). --- @@ -482,25 +446,20 @@ mutation ReplaceLabelMutation( | Label validation failure | `LABEL_BLOCKED` or `LABEL_NOT_ALLOWED` | Skip message with warning; do not fail run | | Gate check (required-labels) | `GATE_REQUIRED_LABELS` | Skip message (skipped=true); do not fail run | | Gate check (title prefix) | `GATE_TITLE_PREFIX` | Skip message (skipped=true); do not fail run | -| Label resolution failure | `LABEL_RESOLUTION_FAILED` | Return hard error; message fails | -| GraphQL mutation failure | `MUTATION_FAILED` | Return hard error; message fails | +| REST setLabels failure | `SETLABELS_FAILED` | Return hard error; message fails | | Rate-limit exhausted after retries | `RATE_LIMIT_EXHAUSTED` | Return hard error; message fails | **RL-044**: A conforming implementation MUST NOT mark the workflow run as failed for soft-skip errors (schema invalid, count gate, target unresolvable, repo not allowed, label validation failure, gate check failures). These errors MUST be surfaced as workflow warnings only. -**RL-045**: A conforming implementation MUST surface hard errors (label resolution failure, mutation failure, rate-limit exhaustion) as `core.error()` entries in the GitHub Actions log. - -### 7.2 Partial Mutation Success +**RL-045**: A conforming implementation MUST surface hard errors (REST call failure, rate-limit exhaustion) as `core.error()` entries in the GitHub Actions log. -As noted in §6.2, the GraphQL API provides no rollback guarantee. The following rules govern partial-success handling: +### 7.2 REST Call Failure -**RL-046**: When `removeLabelsFromLabelable` succeeds but `addLabelsToLabelable` returns an error, the implementation MUST log a `core.error()` entry clearly indicating that the remove succeeded but the add failed, and MUST return `{ success: false, error: }`. - -**RL-047**: Implementors SHOULD provide the full GraphQL error detail in the error log entry to aid diagnosis. +**RL-046**: When the `setLabels` REST call fails (e.g., HTTP 422 for an invalid label name), the implementation MUST log a `core.error()` entry and MUST return `{ success: false, error: }`. Because this is a single atomic REST call, there is no partial-success scenario: either all label changes are applied or none are. ### 7.3 Rate-Limit Retry Policy -**RL-048**: Both the label-resolution step (Stage 7) and the GraphQL mutation step (Stage 8) MUST apply the `RATE_LIMIT_RETRY_CONFIG` retry policy from `actions/setup/js/error_recovery.cjs`. This policy covers secondary rate-limit responses (HTTP 403 with Retry-After header) and primary rate-limit responses (HTTP 429). +**RL-048**: The `setLabels` REST call (Stage 8) MUST apply the `RATE_LIMIT_RETRY_CONFIG` retry policy from `actions/setup/js/error_recovery.cjs`. This policy covers secondary rate-limit responses (HTTP 403 with Retry-After header) and primary rate-limit responses (HTTP 429). --- @@ -589,19 +548,18 @@ The test suite for `replace-label` spans two layers: - **T-RL-032**: Verify that an item with a title matching `required-title-prefix` proceeds. - **T-RL-033**: Verify that an item whose title does not match `required-title-prefix` is skipped without failing. -#### 9.2.5 Label Resolution Tests +#### 9.2.5 Label Set Computation Tests -- **T-RL-040**: Verify that an existing label is resolved via REST and its node ID is used. -- **T-RL-041**: Verify that a missing `label_to_add` (HTTP 404) returns a hard error with `success: false`. -- **T-RL-043**: Verify that the label node-ID cache prevents redundant API calls for the same label name within a run. -- **T-RL-044**: Verify that when `label_to_remove` is not on the item, `$doRemove` is `false` and the remove field is omitted from the mutation. +- **T-RL-040**: Verify that when `label_to_remove` is on the item, the computed new label set excludes it and includes `label_to_add`. +- **T-RL-041**: Verify that when `label_to_add` is passed to `setLabels` and the label does not exist in the repository, the call fails with a hard error. +- **T-RL-044**: Verify that when `label_to_remove` is not on the item, the computed new label set adds `label_to_add` without removing any label. -#### 9.2.6 GraphQL Mutation Tests +#### 9.2.6 REST setLabels Tests -- **T-RL-050**: Verify the GraphQL mutation is called with the correct `labelableId`, `addLabelIds`, `removeLabelIds`, and `doRemove` variables. -- **T-RL-051**: Verify that the mutation result's updated label list is logged. -- **T-RL-052**: Verify that a GraphQL mutation error produces a hard error result with `success: false`. -- **T-RL-053**: Verify that `$addLabelIds` always has exactly one element. +- **T-RL-050**: Verify that `setLabels` is called with the correct `owner`, `repo`, `issue_number`, and `labels` array. +- **T-RL-051**: Verify that the updated label list returned by `setLabels` is logged. +- **T-RL-052**: Verify that a `setLabels` failure produces a hard error result with `success: false`. +- **T-RL-053**: Verify that `label_to_add` always appears exactly once in the `labels` array. - **T-RL-054**: Verify that rate-limit responses trigger retry behavior. #### 9.2.7 Staged Mode Tests @@ -633,16 +591,14 @@ The test suite for `replace-label` spans two layers: | RL-024 required-labels gate | T-RL-030, T-RL-031 | 1 | Required | | RL-025 required-title-prefix gate | T-RL-032, T-RL-033 | 1 | Required | | RL-027 Staged mode no writes | T-RL-060 | 1 | Required | -| RL-029 label_to_add resolution | T-RL-040 | 1 | Required | -| RL-030 label_to_add not found → hard error | T-RL-041 | 1 | Required | +| RL-029 Label set computation | T-RL-040 | 1 | Required | | RL-034 Missing label_to_remove proceeds | T-RL-044 | 1 | Required | -| RL-036 Single GraphQL mutation | T-RL-050 | 1 | Required | -| RL-037 Rate-limit retry on mutation | T-RL-054 | 2 | Recommended | -| RL-041 addLabelIds single element | T-RL-053 | 1 | Required | -| RL-042 doRemove false when label absent | T-RL-044 | 1 | Required | -| RL-043 Sequential mutation semantics | T-RL-050 | 1 | Required | +| RL-036 Single REST setLabels call | T-RL-050 | 1 | Required | +| RL-037 Rate-limit retry on REST call | T-RL-054 | 2 | Recommended | +| RL-041 label_to_add in labels array | T-RL-053 | 1 | Required | +| RL-043 Atomic REST operation | T-RL-050 | 1 | Required | | RL-050 Cross-repo restrictions | T-RL-070 – T-RL-072 | 1 | Required | -| RL-052 No label auto-creation | T-RL-041 | 1 | Required | +| RL-052 setLabels fails for missing label | T-RL-041 | 1 | Required | --- @@ -679,8 +635,8 @@ The handler will: 4. Validate `in-review` against `allowed-remove` ✓, check against `blocked` ✓. 5. Validate `approved` against `allowed-add` ✓, check against `blocked` ✓. 6. Fetch the PR and verify `in-review` is present (required-labels gate ✓). -7. Resolve node IDs for both labels. -8. Execute the GraphQL mutation: removes `in-review`, adds `approved` in a single request. +7. Compute new label set: `["approved"] ∪ (current_labels − {"in-review"})`. +8. Execute `setLabels` REST call with the new label set in a single request. ### 10.2 Missing Source Label: Graceful Add-Only @@ -690,7 +646,9 @@ If the PR in Example 10.1 does not currently carry `in-review` (e.g., a prior ru Label "in-review" is not present on pull request #42 in owner/repo — will only add "approved" ``` -The mutation is called with `removeLabelIds: []` and `addLabelIds: []`. The operation succeeds and returns `{ labelRemoved: null, labelAdded: "approved" }`. +The handler logs `Label "in-review" is not present on pull request #42 in owner/repo — will only add "approved"`. + +The `setLabels` call is made with a label array that does not contain `in-review` (it was never present) but does contain `approved` plus any other existing labels. The operation succeeds and returns `{ labelRemoved: null, labelAdded: "approved" }`. ### 10.3 Staged Mode Preview @@ -712,16 +670,15 @@ For the message `{ "label_to_remove": "in-progress", "label_to_add": "done", "it No API write calls are made. The handler returns `{ success: true, staged: true }`. -### 10.4 Auto-Created Label +### 10.4 Non-Existent Label -Given the message `{ "label_to_remove": "needs-review", "label_to_add": "ship-it" }` and `ship-it` not existing in the repository: +Given the message `{ "label_to_remove": "needs-review", "label_to_add": "ship-it" }` and `ship-it` not existing in the repository, the `setLabels` REST call returns HTTP 422 (Unprocessable Entity). The handler logs: ``` -Label "ship-it" not found in owner/repo, creating it -Created label "ship-it" with color #a3b4c2 +Failed to replace label: Validation Failed ``` -The color `#a3b4c2` is the deterministic pastel color computed for the string `"ship-it"`. +The message is rejected with `{ success: false }`. The label must be created in the repository before use. ### 10.5 Cross-Repository Operation @@ -746,7 +703,7 @@ Agent message: } ``` -The handler resolves the target repository to `owner/platform` (which is in `allowed-repos`), fetches issue #88 from that repository, and executes the GraphQL mutation using the `INFRA_LABEL_TOKEN` credential. +The handler resolves the target repository to `owner/platform` (which is in `allowed-repos`), fetches issue #88 from that repository, and executes the `setLabels` REST call using the `INFRA_LABEL_TOKEN` credential. ### 10.6 Blocked Label Rejection @@ -782,10 +739,6 @@ The message is skipped. The workflow run is not marked as failed. - **[RFC 2119]** Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. https://www.ietf.org/rfc/rfc2119.txt -- **[GRAPHQL]** GraphQL Foundation, "GraphQL Specification", June 2018. https://spec.graphql.org/June2018/ - -- **[GITHUB-GRAPHQL-LABELS]** GitHub, Inc., "addLabelsToLabelable and removeLabelsFromLabelable mutations", GitHub GraphQL API Documentation. https://docs.github.com/en/graphql/reference/mutations#addlabelstolabelable - - **[GITHUB-REST-LABELS]** GitHub, Inc., "Labels REST API", GitHub REST API Documentation. https://docs.github.com/en/rest/issues/labels ### Informative References @@ -802,6 +755,15 @@ The message is skipped. The workflow run is not marked as failed. ## Change Log +### Version 1.0.1 (Revision) — 2026-06-22 + +- Replaced GraphQL mutation (Stage 8) with a single REST `PUT /repos/{owner}/{repo}/issues/{issue_number}/labels` call (`setLabels`), achieving true atomicity: either the entire label set update succeeds or fails with no partial-success scenario. +- Removed Stage 7 label-resolution step (no longer requires node-ID lookup); replaced with label-set computation from the already-fetched issue state. +- Removed §6 GraphQL Interface; added §6 REST Interface describing the `setLabels` endpoint, parameters, and execution semantics. +- Removed RL-046/RL-047 partial-mutation-success requirements (not applicable to single-call REST approach). +- Updated RL-029, RL-034, RL-036, RL-041 – RL-043 to reflect REST semantics. +- Updated examples, compliance checklist, and references accordingly. + ### Version 1.0.0 (Candidate Recommendation) — 2026-06-20 - Initial publication of the `replace-label` safe-output type specification.