From 063c629347c0ce446519fe88e38f8f664f9bdb95 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 06:15:30 +0000 Subject: [PATCH] chore: sync actions from gh-aw@v0.85.4 --- .github/aw/compat.json | 2 +- setup/js/action_setup_otlp.cjs | 2 + setup/js/actions_secret_masking.cjs | 40 +++++++++ setup/js/add_labels.cjs | 93 ++++++++++++++++++-- setup/js/check_workflow_recompile_needed.cjs | 2 +- setup/js/close_entity_helpers.cjs | 4 +- setup/js/convert_gateway_config_claude.cjs | 29 ++---- setup/js/convert_gateway_config_codex.cjs | 59 +++++-------- setup/js/convert_gateway_config_copilot.cjs | 29 ++---- setup/js/convert_gateway_config_gemini.cjs | 67 +++++--------- setup/js/convert_gateway_config_shared.cjs | 46 ++++++++++ setup/js/copilot_harness.cjs | 2 + setup/js/dynamic_checkout.cjs | 7 +- setup/js/exchange_otlp_workload_identity.cjs | 15 +++- setup/js/generate_mcp_scripts_config.cjs | 1 + setup/js/git_auth_env.cjs | 25 ++++++ setup/js/git_auth_helpers.cjs | 65 ++++++++++---- setup/js/git_helpers.cjs | 9 +- setup/js/process_safe_outputs.cjs | 73 ++------------- setup/js/push_experiment_state.cjs | 3 +- setup/js/push_repo_memory.cjs | 3 +- setup/js/replace_label.cjs | 11 +++ setup/js/reply_to_pr_review_comment.cjs | 2 +- setup/js/run_operation_update_upgrade.cjs | 3 +- setup/js/shim.cjs | 8 ++ setup/setup.sh | 2 +- setup/sh/download_docker_images.sh | 44 +++++++-- setup/sh/install_copilot_cli.sh | 2 +- 28 files changed, 409 insertions(+), 239 deletions(-) create mode 100644 setup/js/actions_secret_masking.cjs create mode 100644 setup/js/git_auth_env.cjs diff --git a/.github/aw/compat.json b/.github/aw/compat.json index 44a679cb..f1066508 100644 --- a/.github/aw/compat.json +++ b/.github/aw/compat.json @@ -10,7 +10,7 @@ "min-gh-aw": "0.72.0", "max-gh-aw": "*", "min-agent": "1.0.21", - "max-agent": "1.0.77", + "max-agent": "1.0.78", "open": true }, { diff --git a/setup/js/action_setup_otlp.cjs b/setup/js/action_setup_otlp.cjs index 6681a1de..76759462 100644 --- a/setup/js/action_setup_otlp.cjs +++ b/setup/js/action_setup_otlp.cjs @@ -29,6 +29,7 @@ require("./shim.cjs"); const { appendFileSync } = require("fs"); const { nowMs } = require("./performance_now.cjs"); const { getActionInput } = require("./action_input_utils.cjs"); +const { maskSecret } = require("./actions_secret_masking.cjs"); /** * Append a key=value line to a GitHub Actions file (GITHUB_OUTPUT or GITHUB_ENV) @@ -134,6 +135,7 @@ async function run() { const inputOTLPOIDCToken = getActionInput("OTLP_OIDC_TOKEN"); if (inputOTLPOIDCToken) { + maskSecret(inputOTLPOIDCToken); const existingHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS || ""; const mergedHeaders = mergeAuthorizationHeader(existingHeaders, inputOTLPOIDCToken); diff --git a/setup/js/actions_secret_masking.cjs b/setup/js/actions_secret_masking.cjs new file mode 100644 index 00000000..ea3256a9 --- /dev/null +++ b/setup/js/actions_secret_masking.cjs @@ -0,0 +1,40 @@ +// @ts-check + +/** + * Escape a value for a GitHub Actions workflow command payload. + * + * @param {string} value + * @returns {string} + */ +function escapeWorkflowCommandValue(value) { + return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A"); +} + +/** + * Mask a secret in the surrounding GitHub Actions step when masking is available. + * + * Plain Node entry points such as setup.sh-loaded scripts do not have the real + * @actions/core object, but GitHub Actions still processes add-mask workflow + * commands emitted by the process. + * + * @param {unknown} value + */ +function maskSecret(value) { + if (value === undefined || value === null) return; + const secret = String(value); + if (!secret) return; + + const setSecret = global.core?.setSecret; + if (typeof setSecret === "function" && !setSecret.__ghAwUnavailable) { + setSecret.call(global.core, secret); + return; + } + + // shim.cjs marks its throwing placeholder so Actions-side plain Node callers + // can still mask via workflow commands without enabling masking in MCP shims. + if (process.env.GITHUB_ACTIONS === "true") { + process.stdout.write(`::add-mask::${escapeWorkflowCommandValue(secret)}\n`); + } +} + +module.exports = { escapeWorkflowCommandValue, maskSecret }; diff --git a/setup/js/add_labels.cjs b/setup/js/add_labels.cjs index 15e5e33f..49f7cd9d 100644 --- a/setup/js/add_labels.cjs +++ b/setup/js/add_labels.cjs @@ -33,7 +33,8 @@ const { MAX_LABELS } = require("./constants.cjs"); const { createCountGatedHandler } = require("./handler_scaffold.cjs"); const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); -const { normalizeIssueIntentLabelInputs } = require("./issue_intents.cjs"); +const { normalizeIssueIntentLabelInputs, buildIssueIntentLabelUpdates } = require("./issue_intents.cjs"); +const { fetchAllRepoLabels } = require("./github_api_helpers.cjs"); /** * @param {{ rationale?: string, confidence?: string, suggest?: boolean } | null | undefined} spec @@ -223,11 +224,18 @@ const main = createCountGatedHandler({ }; } - const labelsRequestPayload = uniqueLabels.map(name => { - const labelSpec = requestedLabelSpecByLowerName.get(name.toLowerCase()) ?? { name }; - const hasIntentMetadata = hasLabelIntentMetadata(labelSpec); - return issueIntentEnabled && hasIntentMetadata ? labelSpec : labelSpec.name; - }); + // Build the resolved label specs (name + optional intent metadata) for the validated + // unique labels, preserving the order returned by validation. + const uniqueLabelSpecs = uniqueLabels.map(name => requestedLabelSpecByLowerName.get(name.toLowerCase()) ?? { name }); + const intentLabelSpecs = uniqueLabelSpecs.filter(spec => hasLabelIntentMetadata(spec)); + const useIssueIntentPath = issueIntentEnabled && intentLabelSpecs.length > 0; + + // The REST issues.addLabels endpoint only accepts label name strings; it does not + // support issue-intent metadata (rationale/confidence/suggest). Passing objects with + // those extra keys causes GitHub to return success while silently applying no labels. + // When intent metadata is present, route through the GraphQL updateIssue/LabelUpdateInput + // mutation instead (see update_issue.cjs), which does support intent metadata. + const labelsRequestPayload = uniqueLabels; core.info(`Adding ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo}: ${JSON.stringify(labelsRequestPayload)}`); @@ -248,6 +256,79 @@ const main = createCountGatedHandler({ try { const beforeState = await fetchIssueState(githubClient, repoParts, itemNumber); + + if (useIssueIntentPath) { + // Intent metadata is only supported via the GraphQL updateIssue mutation. That + // mutation replaces the issue's label set, so merge the newly requested labels with + // the issue's existing labels to preserve add-only semantics. Existing labels are + // sent without intent metadata; newly requested labels carry their metadata. + const { data: issueData } = await withRetry( + () => + githubClient.rest.issues.get({ + owner: repoParts.owner, + repo: repoParts.repo, + issue_number: itemNumber, + }), + RATE_LIMIT_RETRY_CONFIG, + `get ${contextType} #${itemNumber} in ${itemRepo}` + ); + + const issueNodeId = issueData?.node_id; + if (!issueNodeId) { + throw new Error(`Failed to resolve GraphQL node ID for ${contextType} #${itemNumber}`); + } + + const repoLabels = await fetchAllRepoLabels(githubClient, repoParts.owner, repoParts.repo); + const labelIdByName = new Map(repoLabels.map(label => [label.name.toLowerCase(), label.id])); + + // Merge existing labels (metadata-free) with the requested specs, de-duplicating by + // lowercased name and favouring the requested specs so their intent metadata wins. + const requestedNamesLower = new Set(uniqueLabelSpecs.map(spec => spec.name.toLowerCase())); + const existingLabelNames = normalizeLabelNames(issueData.labels || []); + const mergedSpecs = [...uniqueLabelSpecs, ...existingLabelNames.filter(name => !requestedNamesLower.has(name.toLowerCase())).map(name => ({ name }))]; + + const labelIntentUpdates = buildIssueIntentLabelUpdates(mergedSpecs, labelIdByName); + + core.info(`Adding ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo} via GraphQL intent mutation`); + const result = await withRetry( + () => + githubClient.graphql( + `mutation($issueId: ID!, $labels: [LabelUpdateInput!]!) { + updateIssue(input: { id: $issueId, labels: $labels }) { + issue { + id + labels(first: 100) { + nodes { + name + } + } + } + } + }`, + { issueId: issueNodeId, labels: labelIntentUpdates, headers: { "GraphQL-Features": "update_issue_suggestions" } } + ), + RATE_LIMIT_RETRY_CONFIG, + `add_labels to ${contextType} #${itemNumber} in ${itemRepo}` + ); + + core.info(`Successfully added ${uniqueLabels.length} labels to ${contextType} #${itemNumber} in ${itemRepo}`); + const afterLabels = result?.updateIssue?.issue?.labels?.nodes || []; + return attachExecutionState( + { + success: true, + number: itemNumber, + repo: itemRepo, + labelsAdded: uniqueLabels, + contextType, + }, + beforeState, + { + ...beforeState, + labels: normalizeLabelNames(afterLabels), + } + ); + } + const { data: labels } = await withRetry( () => githubClient.rest.issues.addLabels({ diff --git a/setup/js/check_workflow_recompile_needed.cjs b/setup/js/check_workflow_recompile_needed.cjs index 9653b482..19cb159e 100644 --- a/setup/js/check_workflow_recompile_needed.cjs +++ b/setup/js/check_workflow_recompile_needed.cjs @@ -4,7 +4,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { getFooterWorkflowRecompileMessage, getFooterWorkflowRecompileCommentMessage, generateXMLMarker, getDetectionCautionAlert } = require("./messages_footer.cjs"); const fs = require("fs"); -const { getGitAuthEnv } = require("./git_helpers.cjs"); +const { getGitAuthEnv } = require("./git_auth_helpers.cjs"); const { resolvePullRequestRepo } = require("./pr_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); diff --git a/setup/js/close_entity_helpers.cjs b/setup/js/close_entity_helpers.cjs index a751e57b..2b9bdc1b 100644 --- a/setup/js/close_entity_helpers.cjs +++ b/setup/js/close_entity_helpers.cjs @@ -57,7 +57,9 @@ function buildCommentBody(body, triggeringIssueNumber, triggeringPRNumber) { // Caller is responsible for sanitizing body before passing it here. const detectionCaution = getDetectionCautionAlert(workflowName, runUrl); const bodyWithCaution = detectionCaution ? detectionCaution + "\n\n" + body.trim() : body.trim(); - return bodyWithCaution + getTrackerID("markdown") + generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined, undefined, { skipDetectionCaution: true }); + return ( + bodyWithCaution + getTrackerID("markdown") + "\n\n" + generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, triggeringIssueNumber, triggeringPRNumber, undefined, undefined, { skipDetectionCaution: true }) + ); } /** diff --git a/setup/js/convert_gateway_config_claude.cjs b/setup/js/convert_gateway_config_claude.cjs index 3aa2041d..ccf7a144 100644 --- a/setup/js/convert_gateway_config_claude.cjs +++ b/setup/js/convert_gateway_config_claude.cjs @@ -23,7 +23,7 @@ require("./shim.cjs"); */ const path = require("path"); -const { normalizeGatewayEntry, loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs"); +const { normalizeGatewayEntry, runGatewayConversion } = require("./convert_gateway_config_shared.cjs"); const OUTPUT_PATH = path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw/mcp-config/mcp-servers.json"); @@ -43,26 +43,13 @@ function transformClaudeEntry(entry, urlPrefix) { } function main() { - const { gatewayOutput, domain, port, urlPrefix, cliServers, servers } = loadGatewayContext(); - - core.info("Converting gateway configuration to Claude format..."); - core.info(`Input: ${gatewayOutput}`); - core.info(`Target domain: ${domain}:${port}`); - logCLIFilters(cliServers); - const result = filterAndTransformServers(servers, cliServers, (_name, entry) => transformClaudeEntry(entry, urlPrefix)); - - const output = JSON.stringify({ mcpServers: result }, null, 2); - logServerStats(servers, Object.keys(result).length); - - // Write with owner-only permissions (0o600) to protect the gateway bearer token. - // An attacker who reads mcp-servers.json could bypass --allowed-tools by issuing - // raw JSON-RPC calls directly to the gateway. - writeSecureOutput(OUTPUT_PATH, output); - - core.info(`Claude configuration written to ${OUTPUT_PATH}`); - core.info(""); - core.info("Converted configuration:"); - core.info(output); + return runGatewayConversion({ + format: "Claude", + engine: "Claude", + outputPath: OUTPUT_PATH, + transformServer: (_name, entry, urlPrefix) => transformClaudeEntry(entry, urlPrefix), + serialize: servers => JSON.stringify({ mcpServers: servers }, null, 2), + }); } if (require.main === module) { diff --git a/setup/js/convert_gateway_config_codex.cjs b/setup/js/convert_gateway_config_codex.cjs index 2b63f616..33c598e0 100644 --- a/setup/js/convert_gateway_config_codex.cjs +++ b/setup/js/convert_gateway_config_codex.cjs @@ -23,7 +23,7 @@ require("./shim.cjs"); */ const path = require("path"); -const { loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs"); +const { runGatewayConversion } = require("./convert_gateway_config_shared.cjs"); const OUTPUT_PATH = path.join(process.env.RUNNER_TEMP || "/tmp", "gh-aw/mcp-config/config.toml"); @@ -47,43 +47,26 @@ function toCodexTomlSection(name, value, urlPrefix) { } function main() { - const { gatewayOutput, domain, port, cliServers, servers } = loadGatewayContext(); - - core.info("Converting gateway configuration to Codex TOML format..."); - core.info(`Input: ${gatewayOutput}`); - core.info(`Target domain: ${domain}:${port}`); - - // For host.docker.internal, resolve to the gateway IP to avoid DNS resolution - // issues in Rust - let resolvedDomain = domain; - if (domain === "host.docker.internal") { - // AWF network gateway IP is always 172.30.0.1 - resolvedDomain = "172.30.0.1"; - core.info(`Resolving host.docker.internal to gateway IP: ${resolvedDomain}`); - } - - const urlPrefix = `http://${resolvedDomain}:${port}`; - logCLIFilters(cliServers); - const filteredServers = filterAndTransformServers(servers, cliServers, (_name, entry) => entry); - - // Build the TOML output - let toml = '[history]\npersistence = "none"\n\n'; - - for (const [name, value] of Object.entries(filteredServers)) { - toml += toCodexTomlSection(name, value, urlPrefix); - } - - logServerStats(servers, Object.keys(filteredServers).length); - - // Write with owner-only permissions (0o600) to protect the gateway bearer token. - // An attacker who reads config.toml could issue raw JSON-RPC calls directly - // to the gateway. - writeSecureOutput(OUTPUT_PATH, toml); - - core.info(`Codex configuration written to ${OUTPUT_PATH}`); - core.info(""); - core.info("Converted configuration:"); - core.info(toml); + return runGatewayConversion({ + format: "Codex TOML", + engine: "Codex", + outputPath: OUTPUT_PATH, + getUrlPrefix: ({ domain, port }) => { + if (domain === "host.docker.internal") { + core.info("Resolving host.docker.internal to gateway IP: 172.30.0.1"); + return `http://172.30.0.1:${port}`; + } + return `http://${domain}:${port}`; + }, + transformServer: (_name, entry) => entry, + serialize: (servers, _context, urlPrefix) => { + let toml = '[history]\npersistence = "none"\n\n'; + for (const [name, value] of Object.entries(servers)) { + toml += toCodexTomlSection(name, value, urlPrefix); + } + return toml; + }, + }); } if (require.main === module) { diff --git a/setup/js/convert_gateway_config_copilot.cjs b/setup/js/convert_gateway_config_copilot.cjs index cec7002b..60f0eac7 100644 --- a/setup/js/convert_gateway_config_copilot.cjs +++ b/setup/js/convert_gateway_config_copilot.cjs @@ -25,7 +25,7 @@ require("./shim.cjs"); */ const path = require("path"); -const { rewriteUrl, normalizeGatewayEntry, loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs"); +const { rewriteUrl, normalizeGatewayEntry, runGatewayConversion } = require("./convert_gateway_config_shared.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); /** @@ -72,26 +72,13 @@ function main() { return; } - const { gatewayOutput, domain, port, urlPrefix, cliServers, servers } = loadGatewayContext(); - - core.info("Converting gateway configuration to Copilot format..."); - core.info(`Input: ${gatewayOutput}`); - core.info(`Target domain: ${domain}:${port}`); - logCLIFilters(cliServers); - const result = filterAndTransformServers(servers, cliServers, (_name, entry) => transformCopilotEntry(entry, urlPrefix)); - - const output = JSON.stringify({ mcpServers: result }, null, 2); - logServerStats(servers, Object.keys(result).length); - - // Write with owner-only permissions (0o600) to protect the gateway bearer token. - // An attacker who reads mcp-config.json could bypass --allowed-tools by issuing - // raw JSON-RPC calls directly to the gateway. - writeSecureOutput(outputPath, output); - - core.info(`Copilot configuration written to ${outputPath}`); - core.info(""); - core.info("Converted configuration:"); - core.info(output); + return runGatewayConversion({ + format: "Copilot", + engine: "Copilot", + outputPath, + transformServer: (_name, entry, urlPrefix) => transformCopilotEntry(entry, urlPrefix), + serialize: servers => JSON.stringify({ mcpServers: servers }, null, 2), + }); } if (require.main === module) { diff --git a/setup/js/convert_gateway_config_gemini.cjs b/setup/js/convert_gateway_config_gemini.cjs index b4038f6e..92aa0579 100644 --- a/setup/js/convert_gateway_config_gemini.cjs +++ b/setup/js/convert_gateway_config_gemini.cjs @@ -31,7 +31,7 @@ require("./shim.cjs"); */ const path = require("path"); -const { rewriteUrl, normalizeGatewayEntry, loadGatewayContext, logCLIFilters, filterAndTransformServers, logServerStats, writeSecureOutput } = require("./convert_gateway_config_shared.cjs"); +const { rewriteUrl, normalizeGatewayEntry, runGatewayConversion } = require("./convert_gateway_config_shared.cjs"); /** * @param {Record} entry @@ -45,51 +45,30 @@ function transformGeminiEntry(entry, urlPrefix) { }); } +function getGeminiHostDomain() { + return process.env.MCP_GATEWAY_HOST_DOMAIN || "localhost"; +} + function main() { - const { gatewayOutput, port, cliServers, servers, extraEnv } = loadGatewayContext({ - extraRequiredEnv: ["GITHUB_WORKSPACE"], + const hostDomain = getGeminiHostDomain(); + return runGatewayConversion({ + format: "Gemini", + engine: "Gemini", + contextOptions: { extraRequiredEnv: ["GITHUB_WORKSPACE"] }, + outputPath: ({ extraEnv }) => path.join(extraEnv.GITHUB_WORKSPACE, ".gemini", "settings.json"), + getTargetDomain: () => hostDomain, + getUrlPrefix: ({ port }) => `http://${hostDomain}:${port}`, + transformServer: (_name, entry, urlPrefix) => transformGeminiEntry(entry, urlPrefix), + serialize: servers => + JSON.stringify( + { + mcpServers: servers, + context: { includeDirectories: ["/tmp/"] }, + }, + null, + 2 + ), }); - const workspace = extraEnv.GITHUB_WORKSPACE; - - // Gemini runs directly on the host runner (not inside a Docker container), so use - // MCP_GATEWAY_HOST_DOMAIN (localhost) instead of MCP_GATEWAY_DOMAIN (host.docker.internal). - // host.docker.internal does not resolve on the host runner on Linux. - const hostDomain = process.env.MCP_GATEWAY_HOST_DOMAIN || "localhost"; - const urlPrefix = `http://${hostDomain}:${port}`; - - core.info("Converting gateway configuration to Gemini format..."); - core.info(`Input: ${gatewayOutput}`); - core.info(`Target domain: ${hostDomain}:${port}`); - logCLIFilters(cliServers); - const result = filterAndTransformServers(servers, cliServers, (_name, entry) => transformGeminiEntry(entry, urlPrefix)); - - // Build settings with mcpServers and context.includeDirectories - // Allow Gemini CLI to read/write files from /tmp/ (e.g. MCP payload files, - // cache-memory, agent outputs) - const settings = { - mcpServers: result, - context: { - includeDirectories: ["/tmp/"], - }, - }; - - const output = JSON.stringify(settings, null, 2); - - logServerStats(servers, Object.keys(result).length); - - // Create .gemini directory in the workspace (project-level settings) - const settingsFile = path.join(workspace, ".gemini", "settings.json"); - - // Write with owner-only permissions (0o600) to protect the gateway bearer token. - // settings.json contains the bearer token for the MCP gateway; an attacker - // who reads it could bypass the --allowed-tools constraint by issuing raw - // JSON-RPC calls directly to the gateway. - writeSecureOutput(settingsFile, output); - - core.info(`Gemini configuration written to ${settingsFile}`); - core.info(""); - core.info("Converted configuration:"); - core.info(output); } if (require.main === module) { diff --git a/setup/js/convert_gateway_config_shared.cjs b/setup/js/convert_gateway_config_shared.cjs index ac5b7c83..21b41c88 100644 --- a/setup/js/convert_gateway_config_shared.cjs +++ b/setup/js/convert_gateway_config_shared.cjs @@ -162,6 +162,51 @@ function writeSecureOutput(outputPath, output) { } } +/** + * Run the common gateway configuration conversion pipeline. + * + * `getTargetDomain` and `getUrlPrefix` are intentionally separate so that + * engines can diverge the log label from the actual URL prefix (for example, + * Codex logs `host.docker.internal` but builds URLs with `172.30.0.1`). + * When both are provided they MUST be kept consistent; when omitted, both + * default to `context.domain` / `context.urlPrefix` respectively. + * + * @param {{ + * format: string; + * engine: string; + * outputPath: string | ((context: ReturnType) => string); + * contextOptions?: { extraRequiredEnv?: string[] }; + * getTargetDomain?: (context: ReturnType) => string; + * getUrlPrefix?: (context: ReturnType) => string; + * transformServer: (name: string, entry: Record, urlPrefix: string, context: ReturnType) => Record; + * serialize: (servers: Record>, context: ReturnType, urlPrefix: string) => string; + * }} options + * @returns {string} + */ +function runGatewayConversion(options) { + const context = loadGatewayContext(options.contextOptions); + const targetDomain = options.getTargetDomain ? options.getTargetDomain(context) : context.domain; + + core.info(`Converting gateway configuration to ${options.format} format...`); + core.info(`Input: ${context.gatewayOutput}`); + core.info(`Target domain: ${targetDomain}:${context.port}`); + + const urlPrefix = options.getUrlPrefix ? options.getUrlPrefix(context) : context.urlPrefix; + logCLIFilters(context.cliServers); + const servers = filterAndTransformServers(context.servers, context.cliServers, (name, entry) => options.transformServer(name, entry, urlPrefix, context)); + const output = options.serialize(servers, context, urlPrefix); + + logServerStats(context.servers, Object.keys(servers).length); + const outputPath = typeof options.outputPath === "function" ? options.outputPath(context) : options.outputPath; + writeSecureOutput(outputPath, output); + + core.info(`${options.engine} configuration written to ${outputPath}`); + core.info(""); + core.info("Converted configuration:"); + core.info(output); + return output; +} + module.exports = { rewriteUrl, normalizeGatewayEntry, @@ -170,4 +215,5 @@ module.exports = { filterAndTransformServers, logServerStats, writeSecureOutput, + runGatewayConversion, }; diff --git a/setup/js/copilot_harness.cjs b/setup/js/copilot_harness.cjs index 688471f1..5ba57248 100644 --- a/setup/js/copilot_harness.cjs +++ b/setup/js/copilot_harness.cjs @@ -42,6 +42,7 @@ require("./shim.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { maskSecret } = require("./actions_secret_masking.cjs"); const fs = require("fs"); const crypto = require("crypto"); const { getPromptPath, renderTemplateFromFile } = require("./messages_core.cjs"); @@ -948,6 +949,7 @@ async function main() { // The token is injected into the driver subprocess env so the harness-managed // sidecar and the driver's SDK client share the same token. copilotConnectionToken = generateCopilotConnectionToken(); + maskSecret(copilotConnectionToken); log("copilot-sdk mode active: generated per-run COPILOT_CONNECTION_TOKEN"); log(`copilot-sdk mode active: COPILOT_SDK_URI=${sdkEnv.COPILOT_SDK_URI || "(not set)"}`); } diff --git a/setup/js/dynamic_checkout.cjs b/setup/js/dynamic_checkout.cjs index 0b8fe903..d2a843d1 100644 --- a/setup/js/dynamic_checkout.cjs +++ b/setup/js/dynamic_checkout.cjs @@ -4,7 +4,8 @@ const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs"); const { ERR_VALIDATION } = require("./error_codes.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); -const { checkoutHasPersistedExtraheader } = require("./git_auth_helpers.cjs"); +const { checkoutHasPersistedExtraheader, gitExecSilent } = require("./git_auth_helpers.cjs"); +const { maskSecret } = require("./actions_secret_masking.cjs"); /** * Dynamic repository checkout utilities for multi-repo scenarios @@ -111,8 +112,10 @@ async function checkoutRepo(repoSlug, token, options = {}) { const hasPersistedAuth = await checkoutHasPersistedExtraheader(serverUrl); if (!hasPersistedAuth) { // Use extraheader to pass the token without embedding it in the URL (more secure). + maskSecret(token); const tokenBase64 = Buffer.from(`x-access-token:${token}`).toString("base64"); - await exec.exec("git", ["config", `http.${serverUrl}/.extraheader`, `Authorization: basic ${tokenBase64}`]); + maskSecret(tokenBase64); + await gitExecSilent(["config", `http.${serverUrl}/.extraheader`, `Authorization: basic ${tokenBase64}`]); } else { core.info("Reusing persisted git credential for authentication (skipping extraheader injection)"); } diff --git a/setup/js/exchange_otlp_workload_identity.cjs b/setup/js/exchange_otlp_workload_identity.cjs index b52b09f0..49fbc63a 100644 --- a/setup/js/exchange_otlp_workload_identity.cjs +++ b/setup/js/exchange_otlp_workload_identity.cjs @@ -13,12 +13,22 @@ const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; +/** + * Mask a token discovered by this inline github-script before it can be used + * or exposed as an output. + * + * @param {unknown} value + */ +function maskSecret(value) { + core.setSecret(String(value)); +} + async function main() { const oidcToken = process.env.GH_AW_OTLP_OIDC_TOKEN; if (!oidcToken) { throw new Error("Missing GitHub OIDC token for Google workload identity token exchange"); } - core.setSecret(oidcToken); + maskSecret(oidcToken); const response = await fetch("https://sts.googleapis.com/v1/token", { method: "POST", @@ -44,6 +54,7 @@ async function main() { if (!accessToken) { throw new Error("Google workload identity token exchange returned no access token"); } + maskSecret(accessToken); const serviceAccount = process.env.GH_AW_OTLP_WIF_SERVICE_ACCOUNT; if (serviceAccount) { @@ -63,9 +74,9 @@ async function main() { if (!accessToken) { throw new Error("Google service account impersonation returned no access token"); } + maskSecret(accessToken); } - core.setSecret(accessToken); core.setOutput("token", accessToken); return accessToken; } diff --git a/setup/js/generate_mcp_scripts_config.cjs b/setup/js/generate_mcp_scripts_config.cjs index 29869f58..efb070b1 100644 --- a/setup/js/generate_mcp_scripts_config.cjs +++ b/setup/js/generate_mcp_scripts_config.cjs @@ -14,6 +14,7 @@ function generateMCPScriptsConfig({ core, crypto }) { // after base64 encoding and removing special characters (base64 of 45 bytes = 60 chars) const apiKeyBuffer = crypto.randomBytes(45); const apiKey = apiKeyBuffer.toString("base64").replace(/[/+=]/g, ""); + core.setSecret(apiKey); // Choose a port for the HTTP server (default 3000) const port = 3000; diff --git a/setup/js/git_auth_env.cjs b/setup/js/git_auth_env.cjs new file mode 100644 index 00000000..65b2c6ec --- /dev/null +++ b/setup/js/git_auth_env.cjs @@ -0,0 +1,25 @@ +// @ts-check + +/** + * Build GIT_CONFIG_* environment variables that inject an Authorization header + * for git network operations without writing credentials to disk. + * + * This helper intentionally does not call core.setSecret so it is safe to use + * from MCP server processes. + * + * @param {string} authToken + * @param {(value: string) => void} [setSecret] + * @returns {Object} + */ +function buildGitAuthEnv(authToken, setSecret) { + const serverUrl = (process.env.GITHUB_SERVER_URL || "https://github.com").replace(/\/$/, ""); + const tokenBase64 = Buffer.from(`x-access-token:${authToken}`).toString("base64"); + setSecret?.(tokenBase64); + return { + GIT_CONFIG_COUNT: "1", + GIT_CONFIG_KEY_0: `http.${serverUrl}/.extraheader`, + GIT_CONFIG_VALUE_0: `Authorization: basic ${tokenBase64}`, + }; +} + +module.exports = { buildGitAuthEnv }; diff --git a/setup/js/git_auth_helpers.cjs b/setup/js/git_auth_helpers.cjs index 051aef16..57b2b3bc 100644 --- a/setup/js/git_auth_helpers.cjs +++ b/setup/js/git_auth_helpers.cjs @@ -4,10 +4,29 @@ // All callers must ensure these globals are set before invoking any helper. const { getErrorMessage } = require("./error_helpers.cjs"); +const { buildGitAuthEnv } = require("./git_auth_env.cjs"); +const { maskSecret } = require("./actions_secret_masking.cjs"); const fs = require("fs"); const os = require("os"); const path = require("path"); +/** + * Build git authentication environment variables and mask both credential + * representations in the surrounding GitHub Actions step. + * + * @param {string} [token] + * @returns {Object} + */ +function getGitAuthEnv(token) { + const authToken = token || process.env.GITHUB_TOKEN; + if (!authToken) { + core.debug("getGitAuthEnv: no token available, git network operations may fail if credentials were cleaned"); + return {}; + } + maskSecret(authToken); + return buildGitAuthEnv(authToken, maskSecret); +} + /** * Normalize a server URL by stripping any trailing slash so the git config key * matches exactly what actions/checkout writes (e.g. `http.https://github.com/.extraheader`). @@ -166,6 +185,29 @@ async function checkoutHasPersistedExtraheader(serverUrl) { } } +/** + * Run `git config` with `silent: true` (to prevent credential-bearing command + * lines from reaching stdout / uploaded safe-output artifacts) while still + * capturing stderr so that real git diagnostics surface on failure. + * + * @param {string[]} gitArgs - Arguments passed to `git` (e.g. `["config", "--local", ...]`) + * @param {string} [cwd] - Optional working directory + * @returns {Promise} + */ +async function gitExecSilent(gitArgs, cwd) { + let stderrBuf = ""; + const listeners = { + stderr: (/** @type {Buffer} */ data) => { + stderrBuf += data.toString(); + }, + }; + try { + await exec.exec("git", gitArgs, { silent: true, listeners, ...(cwd ? { cwd } : {}) }); + } catch (err) { + throw new Error(`git-config-credential failed: ${stderrBuf.trim() || getErrorMessage(err)}`); + } +} + /** * Replace any existing extraheader values with a single token-based Authorization * header and return the previous values for restoration. @@ -186,7 +228,9 @@ async function overridePersistedExtraheader(serverUrl, token, cwd) { previousValues = []; } core.info(`git_auth_helpers: overriding http.${normalizedUrl}/.extraheader with CI trigger token`); + maskSecret(token); const tokenBase64 = Buffer.from(`x-access-token:${token.trim()}`).toString("base64"); + maskSecret(tokenBase64); const authHeader = `Authorization: basic ${tokenBase64}`; // Clear from ALL writable scopes before writing our token to prevent duplicate @@ -195,11 +239,7 @@ async function overridePersistedExtraheader(serverUrl, token, cwd) { // global value in place and causing duplicate-header HTTP 400 errors. await unsetExtraheaderAllScopes(`http.${normalizedUrl}/.extraheader`, cwd); - if (cwd) { - await exec.exec("git", ["config", "--local", "--replace-all", `http.${normalizedUrl}/.extraheader`, authHeader], { cwd }); - } else { - await exec.exec("git", ["config", "--local", "--replace-all", `http.${normalizedUrl}/.extraheader`, authHeader]); - } + await gitExecSilent(["config", "--local", "--replace-all", `http.${normalizedUrl}/.extraheader`, authHeader], cwd); core.info(`git_auth_helpers: extraheader override applied`); return previousValues; } @@ -236,16 +276,9 @@ async function restorePersistedExtraheader(serverUrl, previousValues, cwd) { // best-effort cleanup, then re-throw so the caller is aware that restoration // failed. try { - if (cwd) { - await exec.exec("git", ["config", "--local", "--replace-all", key, previousValues[0]], { cwd }); - for (const value of previousValues.slice(1)) { - await exec.exec("git", ["config", "--local", "--add", key, value], { cwd }); - } - } else { - await exec.exec("git", ["config", "--local", "--replace-all", key, previousValues[0]]); - for (const value of previousValues.slice(1)) { - await exec.exec("git", ["config", "--local", "--add", key, value]); - } + await gitExecSilent(["config", "--local", "--replace-all", key, previousValues[0]], cwd); + for (const value of previousValues.slice(1)) { + await gitExecSilent(["config", "--local", "--add", key, value], cwd); } } catch (err) { core.warning(`git_auth_helpers: partial extraheader restore for ${key} — attempting cleanup: ${getErrorMessage(err)}`); @@ -294,6 +327,8 @@ async function withGitHubHostToken(token, callback, cwd) { module.exports = { checkoutHasPersistedExtraheader, findIncludedExtraheaderConfigFiles, + getGitAuthEnv, + gitExecSilent, overridePersistedExtraheader, restorePersistedExtraheader, unsetExtraheaderAllScopes, diff --git a/setup/js/git_helpers.cjs b/setup/js/git_helpers.cjs index 0a7985d2..39200f14 100644 --- a/setup/js/git_helpers.cjs +++ b/setup/js/git_helpers.cjs @@ -5,6 +5,7 @@ const { spawnSync } = require("child_process"); const { ERR_SYSTEM } = require("./error_codes.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { isTransientError } = require("./error_recovery.cjs"); +const { buildGitAuthEnv } = require("./git_auth_env.cjs"); /** * Build GIT_CONFIG_* environment variables that inject an Authorization header @@ -29,13 +30,7 @@ function getGitAuthEnv(token) { core.debug("getGitAuthEnv: no token available, git network operations may fail if credentials were cleaned"); return {}; } - const serverUrl = (process.env.GITHUB_SERVER_URL || "https://github.com").replace(/\/$/, ""); - const tokenBase64 = Buffer.from(`x-access-token:${authToken}`).toString("base64"); - return { - GIT_CONFIG_COUNT: "1", - GIT_CONFIG_KEY_0: `http.${serverUrl}/.extraheader`, - GIT_CONFIG_VALUE_0: `Authorization: basic ${tokenBase64}`, - }; + return buildGitAuthEnv(authToken); } /** diff --git a/setup/js/process_safe_outputs.cjs b/setup/js/process_safe_outputs.cjs index a386ef22..63a936fa 100644 --- a/setup/js/process_safe_outputs.cjs +++ b/setup/js/process_safe_outputs.cjs @@ -1,78 +1,17 @@ // @ts-check /// -const fs = require("fs"); -const path = require("path"); -const { getErrorMessage } = require("./error_helpers.cjs"); const safeOutputHandlerManager = require("./safe_output_handler_manager.cjs"); -const LOGS_DIR = "/tmp/gh-aw"; -const STDOUT_LOG = "process-safe-outputs.stdout.log"; -const STDERR_LOG = "process-safe-outputs.stderr.log"; - -/** - * End a writable stream, rejecting if an error occurs during close. - * @param {import("fs").WriteStream} stream - * @returns {Promise} - */ -function endStream(stream) { - return new Promise((resolve, reject) => { - stream.once("error", reject); - stream.end(() => resolve()); - }); -} - /** - * Capture process output while safe-output handlers execute. + * Run safe-output handlers. + * + * Process stdout/stderr logs are intentionally NOT captured to disk. + * They could contain sensitive information from handler execution and + * must never be packaged into artifacts. */ async function main() { - try { - fs.mkdirSync(LOGS_DIR, { recursive: true }); - } catch (err) { - throw new Error(`Failed to create logs directory: ${getErrorMessage(err)}`, { cause: err }); - } - - const stdoutStream = fs.createWriteStream(path.join(LOGS_DIR, STDOUT_LOG), { flags: "w" }); - const stderrStream = fs.createWriteStream(path.join(LOGS_DIR, STDERR_LOG), { flags: "w" }); - stdoutStream.on("error", err => core.warning(`stdout log write error: ${getErrorMessage(err)}`)); - stderrStream.on("error", err => core.warning(`stderr log write error: ${getErrorMessage(err)}`)); - - const originalStdoutWrite = process.stdout.write.bind(process.stdout); - const originalStderrWrite = process.stderr.write.bind(process.stderr); - - // TypeScript cannot verify that a single arrow function satisfies a multi-overload - // interface. Cast via any so the assignment passes the type checker; the runtime - // behaviour is correct (both 2-arg and 3-arg Node.js write() forms are forwarded). - /** @type {any} */ - const hookedStdoutWrite = (/** @type {string | Uint8Array} */ chunk, /** @type {BufferEncoding | undefined} */ encoding, /** @type {((err?: Error | null) => void) | undefined} */ callback) => { - if (typeof encoding === "string") { - stdoutStream.write(chunk, encoding); - } else { - stdoutStream.write(chunk); - } - return originalStdoutWrite(chunk, encoding, callback); - }; - - /** @type {any} */ - const hookedStderrWrite = (/** @type {string | Uint8Array} */ chunk, /** @type {BufferEncoding | undefined} */ encoding, /** @type {((err?: Error | null) => void) | undefined} */ callback) => { - if (typeof encoding === "string") { - stderrStream.write(chunk, encoding); - } else { - stderrStream.write(chunk); - } - return originalStderrWrite(chunk, encoding, callback); - }; - - process.stdout.write = hookedStdoutWrite; - process.stderr.write = hookedStderrWrite; - - try { - await safeOutputHandlerManager.main(); - } finally { - process.stdout.write = originalStdoutWrite; - process.stderr.write = originalStderrWrite; - await Promise.all([endStream(stdoutStream), endStream(stderrStream)]); - } + await safeOutputHandlerManager.main(); } module.exports = { main }; diff --git a/setup/js/push_experiment_state.cjs b/setup/js/push_experiment_state.cjs index 564bcbe9..654bd8e9 100644 --- a/setup/js/push_experiment_state.cjs +++ b/setup/js/push_experiment_state.cjs @@ -28,7 +28,8 @@ const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); -const { execGitSync, getGitAuthEnv, withGitRetry } = require("./git_helpers.cjs"); +const { getGitAuthEnv } = require("./git_auth_helpers.cjs"); +const { execGitSync, withGitRetry } = require("./git_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); function isPlainObject(value) { diff --git a/setup/js/push_repo_memory.cjs b/setup/js/push_repo_memory.cjs index 9e783d30..01e48ba7 100644 --- a/setup/js/push_repo_memory.cjs +++ b/setup/js/push_repo_memory.cjs @@ -6,7 +6,8 @@ const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); -const { execGitSync, getGitAuthEnv } = require("./git_helpers.cjs"); +const { getGitAuthEnv } = require("./git_auth_helpers.cjs"); +const { execGitSync } = require("./git_helpers.cjs"); const { getStagedPatchDiffSizeBytes } = require("./git_patch_utils.cjs"); const { parseAllowedRepos, validateRepo } = require("./repo_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); diff --git a/setup/js/replace_label.cjs b/setup/js/replace_label.cjs index b4cb6fb4..dcfbade2 100644 --- a/setup/js/replace_label.cjs +++ b/setup/js/replace_label.cjs @@ -221,6 +221,17 @@ const main = createCountGatedHandler({ const updatedLabelNames = (updatedLabels || []).map((/** @param {any} l */ l) => l.name || "").filter(Boolean); + if (!updatedLabelNames.includes(labelToAdd)) { + const error = `replace_label: label_to_add ${JSON.stringify(labelToAdd)} not found in POST-setLabels response`; + core.error(error); + return { success: false, error }; + } + if (labelToRemoveIsPresent && labelToRemove !== labelToAdd && updatedLabelNames.includes(labelToRemove)) { + const error = `replace_label: label_to_remove ${JSON.stringify(labelToRemove)} still present after setLabels call`; + core.error(error); + return { success: false, error }; + } + core.info(`Successfully replaced label "${labelToRemove}" → "${labelToAdd}" on ${contextType} #${itemNumber} in ${itemRepo}`); core.info(`Updated labels: ${JSON.stringify(updatedLabelNames)}`); diff --git a/setup/js/reply_to_pr_review_comment.cjs b/setup/js/reply_to_pr_review_comment.cjs index 1cf4f4f5..0d964eb5 100644 --- a/setup/js/reply_to_pr_review_comment.cjs +++ b/setup/js/reply_to_pr_review_comment.cjs @@ -183,7 +183,7 @@ async function main(config = {}) { // Append footer with workflow information when enabled if (includeFooter) { const footer = generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, undefined, triggeringPRNumber, undefined, undefined, { skipDetectionCaution: true }); - finalBody = finalBody.trimEnd() + footer; + finalBody = finalBody.trimEnd() + "\n\n" + footer; } core.info(`Replying to review comment ${numericCommentId} on PR #${targetPRNumber} (${owner}/${repo})`); diff --git a/setup/js/run_operation_update_upgrade.cjs b/setup/js/run_operation_update_upgrade.cjs index bd422abe..8f2f4efd 100644 --- a/setup/js/run_operation_update_upgrade.cjs +++ b/setup/js/run_operation_update_upgrade.cjs @@ -159,6 +159,7 @@ async function main() { if (!token) { throw new Error(`${ERR_CONFIG}: Missing GitHub token: set GH_TOKEN or GITHUB_TOKEN to push changes and create a pull request for agentic workflow update/upgrade operations.`); } + core.setSecret(token); const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; let githubHost; try { @@ -173,7 +174,7 @@ async function main() { } catch { // Remote doesn't exist yet - that's fine } - await exec.exec("git", ["remote", "add", "aw-push", remoteUrl]); + await exec.exec("git", ["remote", "add", "aw-push", remoteUrl], { silent: true }); try { await exec.exec("git", ["push", "aw-push", branchName]); diff --git a/setup/js/shim.cjs b/setup/js/shim.cjs index f903fd94..3d0c4328 100644 --- a/setup/js/shim.cjs +++ b/setup/js/shim.cjs @@ -12,6 +12,11 @@ * `github-script`) the respective block is a no-op. */ +const setSecret = /** @param {string} _value */ _value => { + throw new Error("core.setSecret is unavailable outside the github-script runtime"); +}; +Object.defineProperty(setSecret, "__ghAwUnavailable", { value: true }); + if (!global.core) { /** * Write shim log lines to stderr so MCP servers that speak JSON-RPC on stdout @@ -40,7 +45,10 @@ if (!global.core) { setOutput: /** @param {string} name @param {unknown} value */ (name, value) => { writeShimLog("output", `${name}=${value}`); }, + setSecret, }; +} else if (typeof global.core.setSecret !== "function") { + global.core.setSecret = setSecret; } if (!global.context) { diff --git a/setup/setup.sh b/setup/setup.sh index 2576943d..3460859b 100755 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -243,7 +243,6 @@ MCP_SCRIPTS_FILES=( "mcp_handler_javascript.cjs" "mcp_handler_process.cjs" "read_buffer.cjs" - "generate_mcp_scripts_config.cjs" "setup_globals.cjs" "runtime_features.cjs" "github_rate_limit_logger.cjs" @@ -337,6 +336,7 @@ SAFE_OUTPUTS_FILES=( "error_codes.cjs" "constants.cjs" "git_helpers.cjs" + "git_auth_env.cjs" "error_recovery.cjs" "checkout_manifest.cjs" "github_api_helpers.cjs" diff --git a/setup/sh/download_docker_images.sh b/setup/sh/download_docker_images.sh index 48213a0c..5d06e692 100755 --- a/setup/sh/download_docker_images.sh +++ b/setup/sh/download_docker_images.sh @@ -15,6 +15,13 @@ set +o histexpand # When images include a digest pin (e.g. image:tag@sha256:abc), the script # ensures the tag alias (image:tag) is created after pulling so that tools # referencing images by tag (such as AWF with --skip-pull) can find them. +# +# The script also aliases the image as "image:latest" so that any downstream +# tool that references the image via the mutable ":latest" tag (regardless of +# which versioned tag was actually pulled) can still resolve it locally under +# --pull-never/--skip-pull semantics. This guards against tag mismatches +# between the version-pinned tag written here and a ":latest" reference used +# elsewhere (see gh-aw#50681). set -euo pipefail @@ -30,14 +37,37 @@ docker_pull_with_retry() { if timeout 5m docker pull --quiet "$image" 2>&1; then echo "Successfully pulled $image" - # When pulling with a digest pin (image:tag@sha256:...), Docker may not - # create the tag alias automatically. Ensure the tag exists so that - # downstream tools (e.g. AWF --skip-pull) can find the image by tag. + # When pulling with a digest pin, Docker may not create a digest-free + # alias automatically. Preserve the original base reference and add back + # its implicit ":latest" or explicit tag alias so downstream tools can + # resolve the image locally under --pull-never/--skip-pull semantics. + local tag_ref="$image" + local base_ref="$image" if [[ "$image" == *"@sha256:"* ]]; then - local tag_ref="${image%%@sha256:*}" - if [[ "$tag_ref" == *":"* ]]; then - echo "Tagging digest-pinned image as $tag_ref" - docker tag "$image" "$tag_ref" + base_ref="${image%%@sha256:*}" + tag_ref="$base_ref" + if [[ "$base_ref" == *":"* ]]; then + echo "Tagging digest-pinned image as $base_ref" + docker tag "$image" "$base_ref" + else + local latest_ref="${base_ref}:latest" + echo "Tagging digest-pinned image as $latest_ref" + docker tag "$image" "$latest_ref" + tag_ref="$latest_ref" + fi + fi + + # Only AWF images need a mutable ":latest" alias for local compose + # stacks, and only when the requested reference was version-pinned. This + # avoids races when unrelated repositories are pulled concurrently with + # multiple distinct tags. + if [[ "$base_ref" == ghcr.io/github/gh-aw-* && "$tag_ref" == *":"* ]]; then + local repo_ref="${tag_ref%%:*}" + local tag_part="${tag_ref##*:}" + if [[ "$tag_part" != "latest" ]]; then + local latest_ref="${repo_ref}:latest" + echo "Aliasing $tag_ref as $latest_ref" + docker tag "$tag_ref" "$latest_ref" fi fi diff --git a/setup/sh/install_copilot_cli.sh b/setup/sh/install_copilot_cli.sh index cb795653..db9a9520 100755 --- a/setup/sh/install_copilot_cli.sh +++ b/setup/sh/install_copilot_cli.sh @@ -33,7 +33,7 @@ COPILOT_TOOLCACHE_MAX_DEPTH=4 # argument nor a GH_AW_COMPILED_VERSION-backed compat.json lookup is available. # It is the last resort (priority 3) after engine.version (priority 1) and # compat.json toolcache lookup (priority 2). -DEFAULT_COPILOT_VERSION="1.0.77" +DEFAULT_COPILOT_VERSION="1.0.78" COMPAT_URL="${COPILOT_COMPAT_URL:-https://raw.githubusercontent.com/github/gh-aw-actions/main/.github/aw/compat.json}" COMPILED_GH_AW_VERSION="${GH_AW_COMPILED_VERSION:-}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"