From 54378075bee6d92243cf4ddca07611f9c41c03fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:20:17 +0000 Subject: [PATCH 1/3] Mask secrets read from JavaScript environment Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/index.js | 3 +- actions/setup/js/action_setup_otlp.cjs | 3 +- actions/setup/js/apply_samples.cjs | 5 ++- actions/setup/js/artifact_client.cjs | 5 ++- actions/setup/js/assign_to_agent.cjs | 3 +- actions/setup/js/build_checkout_manifest.cjs | 3 +- .../js/check_daily_aic_workflow_guardrail.cjs | 3 +- .../js/check_workflow_recompile_needed.cjs | 3 +- actions/setup/js/codex_harness.cjs | 5 ++- actions/setup/js/copilot_sdk_driver.cjs | 5 ++- actions/setup/js/create_agent_session.cjs | 3 +- actions/setup/js/create_issue.cjs | 3 +- actions/setup/js/create_pull_request.cjs | 5 ++- .../js/exchange_otlp_workload_identity.cjs | 5 +-- actions/setup/js/extra_empty_commit.cjs | 3 +- actions/setup/js/git_helpers.cjs | 3 +- actions/setup/js/mount_mcp_as_cli.cjs | 3 +- actions/setup/js/pi_agent_core_driver.cjs | 9 ++-- actions/setup/js/pi_provider.cjs | 10 +++-- actions/setup/js/push_experiment_state.cjs | 3 +- actions/setup/js/push_repo_memory.cjs | 3 +- actions/setup/js/read_secret_env.cjs | 21 +++++++++ actions/setup/js/read_secret_env.test.cjs | 43 +++++++++++++++++++ actions/setup/js/redact_evals_results.cjs | 3 +- actions/setup/js/redact_secrets.cjs | 3 +- .../setup/js/run_operation_update_upgrade.cjs | 3 +- .../setup/js/safe_output_handler_manager.cjs | 8 +++- actions/setup/js/safe_outputs_config.cjs | 6 ++- actions/setup/js/shim.cjs | 14 ++++++ actions/setup/js/shim.test.cjs | 29 +++++++++++++ actions/setup/js/start_mcp_gateway.cjs | 3 +- actions/setup/js/test-live-github-api.cjs | 3 +- actions/setup/js/update_project.cjs | 3 +- .../js/validate_lockdown_requirements.cjs | 7 +-- actions/setup/js/validate_secrets.cjs | 17 ++++---- actions/setup/setup.sh | 2 + 36 files changed, 199 insertions(+), 54 deletions(-) create mode 100644 actions/setup/js/read_secret_env.cjs create mode 100644 actions/setup/js/read_secret_env.test.cjs create mode 100644 actions/setup/js/shim.test.cjs diff --git a/actions/setup/index.js b/actions/setup/index.js index 678ae89b9ac..0cb812db259 100644 --- a/actions/setup/index.js +++ b/actions/setup/index.js @@ -4,6 +4,7 @@ const { spawnSync } = require("child_process"); const path = require("path"); const { getActionInput } = require("./js/action_input_utils.cjs"); +const { readSecretEnv } = require("./js/read_secret_env.cjs"); // Record start time for the OTLP span before any setup work begins. const setupStartMs = Date.now(); @@ -15,7 +16,7 @@ const safeOutputCustomTokens = getActionInput("SAFE_OUTPUT_CUSTOM_TOKENS") || "f const inputTraceId = getActionInput("TRACE_ID"); const inputParentSpanId = getActionInput("PARENT_SPAN_ID"); const inputJobName = getActionInput("JOB_NAME"); -const inputOTLPOIDCToken = getActionInput("OTLP_OIDC_TOKEN"); +const inputOTLPOIDCToken = (readSecretEnv("INPUT_OTLP_OIDC_TOKEN") || readSecretEnv("INPUT_OTLP-OIDC-TOKEN") || "").trim(); const result = spawnSync(path.join(__dirname, "setup.sh"), [], { stdio: "inherit", diff --git a/actions/setup/js/action_setup_otlp.cjs b/actions/setup/js/action_setup_otlp.cjs index 6681a1de20e..b8bb7185974 100644 --- a/actions/setup/js/action_setup_otlp.cjs +++ b/actions/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 { readSecretEnv } = require("./read_secret_env.cjs"); /** * Append a key=value line to a GitHub Actions file (GITHUB_OUTPUT or GITHUB_ENV) @@ -132,7 +133,7 @@ async function run() { process.env.INPUT_PARENT_SPAN_ID = inputParentSpanId; } - const inputOTLPOIDCToken = getActionInput("OTLP_OIDC_TOKEN"); + const inputOTLPOIDCToken = (readSecretEnv("INPUT_OTLP_OIDC_TOKEN") || readSecretEnv("INPUT_OTLP-OIDC-TOKEN") || "").trim(); if (inputOTLPOIDCToken) { const existingHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS || ""; const mergedHeaders = mergeAuthorizationHeader(existingHeaders, inputOTLPOIDCToken); diff --git a/actions/setup/js/apply_samples.cjs b/actions/setup/js/apply_samples.cjs index c102c58796a..fe9766d7bba 100644 --- a/actions/setup/js/apply_samples.cjs +++ b/actions/setup/js/apply_samples.cjs @@ -38,6 +38,7 @@ const os = require("os"); const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_VALIDATION, ERR_PARSE, ERR_SYSTEM, ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); const { findRepoCheckout } = require("./find_repo_checkout.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const DEFAULT_BASE_BRANCH = process.env.GH_AW_CUSTOM_BASE_BRANCH || process.env.GITHUB_BASE_REF || process.env.GITHUB_REF_NAME || "main"; const PATCH_SIDECAR_TOOLS = new Set(["create_pull_request", "push_to_pull_request_branch"]); @@ -154,7 +155,7 @@ function readEventPayload() { */ function selectTokenForRepo(owner, repo) { const slug = `${owner}/${repo}`; - const raw = process.env.GH_AW_REPO_TOKENS; + const raw = readSecretEnv("GH_AW_REPO_TOKENS"); if (raw && raw.trim()) { try { const map = JSON.parse(raw); @@ -165,7 +166,7 @@ function selectTokenForRepo(owner, repo) { core.warning(`apply_samples: GH_AW_REPO_TOKENS is not valid JSON, ignoring: ${getErrorMessage(err)}`); } } - return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || undefined; + return readSecretEnv("GITHUB_TOKEN") || readSecretEnv("GH_TOKEN"); } /** diff --git a/actions/setup/js/artifact_client.cjs b/actions/setup/js/artifact_client.cjs index 8ac97f1aeb5..ce9fb4205d0 100644 --- a/actions/setup/js/artifact_client.cjs +++ b/actions/setup/js/artifact_client.cjs @@ -16,6 +16,7 @@ const { pipeline } = require("stream/promises"); const { spawnSync } = require("child_process"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const DEFAULT_RETRY_ATTEMPTS = 5; const RETRY_DELAY_MS = 5000; @@ -55,7 +56,7 @@ function decodeJWTPayload(token) { } function getBackendIdsFromRuntimeToken() { - const token = process.env.ACTIONS_RUNTIME_TOKEN || ""; + const token = readSecretEnv("ACTIONS_RUNTIME_TOKEN") || ""; if (!token) { throw new Error("ACTIONS_RUNTIME_TOKEN is required for artifact upload"); } @@ -84,7 +85,7 @@ function getResultsServiceOrigin() { } async function twirpRequest(method, body) { - const runtimeToken = process.env.ACTIONS_RUNTIME_TOKEN || ""; + const runtimeToken = readSecretEnv("ACTIONS_RUNTIME_TOKEN") || ""; if (!runtimeToken) { throw new Error("ACTIONS_RUNTIME_TOKEN is required for artifact upload"); } diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 21868685ce5..87c47ad7235 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -9,6 +9,7 @@ const { isTemporaryId, normalizeTemporaryId, resolveRepoIssueTarget } = require( const { sleep } = require("./error_recovery.cjs"); const { parseAllowedRepos, validateRepo, resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { resolvePullRequestRepo } = require("./pr_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const { sanitizeContent } = require("./sanitize_content.cjs"); const { normalizeIssueIntentMetadata } = require("./issue_intents.cjs"); @@ -33,7 +34,7 @@ let _allResults = []; * @returns {Promise} Authenticated GitHub client */ async function createAssignToAgentGitHubClient(config) { - const token = config["github-token"] || process.env.GH_AW_ASSIGN_TO_AGENT_TOKEN; + const token = config["github-token"] || readSecretEnv("GH_AW_ASSIGN_TO_AGENT_TOKEN"); if (!token) { core.debug("No dedicated agent token configured — using step-level github client for assign-to-agent operations"); return github; diff --git a/actions/setup/js/build_checkout_manifest.cjs b/actions/setup/js/build_checkout_manifest.cjs index 53913543904..9892700075b 100644 --- a/actions/setup/js/build_checkout_manifest.cjs +++ b/actions/setup/js/build_checkout_manifest.cjs @@ -8,6 +8,7 @@ const path = require("path"); const { execFileSync } = require("child_process"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); function parseManifestEntries(entriesJSON = process.env.GH_AW_CHECKOUT_MANIFEST_ENTRIES || "[]") { let parsed; @@ -33,7 +34,7 @@ function readManifestEntriesFromEnv() { entries.push({ repository: process.env[`GH_AW_CHECKOUT_REPO_${i}`] || "", path: process.env[`GH_AW_CHECKOUT_PATH_${i}`] || "", - token: process.env[`GH_AW_CHECKOUT_TOKEN_${i}`] || "", + token: readSecretEnv(`GH_AW_CHECKOUT_TOKEN_${i}`) || "", }); } return entries; diff --git a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs index 007b820f211..da55d56043f 100644 --- a/actions/setup/js/check_daily_aic_workflow_guardrail.cjs +++ b/actions/setup/js/check_daily_aic_workflow_guardrail.cjs @@ -10,6 +10,7 @@ const { calculateDailyAICStats, findJSONLFiles, formatAICCredits, sumAICFromUsag const { AIC_USAGE_CACHE_FILE_PATH, CACHE_RETENTION_MS, pruneStaleJSONLCacheLines } = require("./daily_aic_cache_helpers.cjs"); const { parsePositiveCompactNumber } = require("./numeric_limits.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const { createRateLimitAwareGithub, fetchAndLogRateLimit } = require("./github_rate_limit_logger.cjs"); const PRIMARY_GUARDRAIL_ARTIFACT_NAMES = ["usage"]; @@ -576,7 +577,7 @@ async function main() { return; } - const token = process.env.GH_AW_GITHUB_TOKEN || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || ""; + const token = readSecretEnv("GH_AW_GITHUB_TOKEN") || readSecretEnv("GITHUB_TOKEN") || readSecretEnv("GH_TOKEN") || ""; if (!token) { core.setOutput("daily_ai_credits_guardrail_status", "skipped"); core.warning("Skipping daily workflow AI Credits guardrail because no GitHub token was available for artifact lookup."); diff --git a/actions/setup/js/check_workflow_recompile_needed.cjs b/actions/setup/js/check_workflow_recompile_needed.cjs index 9653b482658..8d1233829a2 100644 --- a/actions/setup/js/check_workflow_recompile_needed.cjs +++ b/actions/setup/js/check_workflow_recompile_needed.cjs @@ -8,6 +8,7 @@ const { getGitAuthEnv } = require("./git_helpers.cjs"); const { resolvePullRequestRepo } = require("./pr_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const RECOMPILE_ISSUE_TITLE = "[aw] agentic workflows out of sync"; const RECOMPILE_PR_TITLE = "[aw] recompile agentic workflows"; @@ -23,7 +24,7 @@ async function getEffectiveBaseBranch(owner, repo) { } function getRecompileToken() { - return process.env.GH_AW_MAINTENANCE_GITHUB_TOKEN || ""; + return readSecretEnv("GH_AW_MAINTENANCE_GITHUB_TOKEN") || ""; } function logConfiguration(createPullRequest) { diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index bfe086c9450..05fd1170705 100644 --- a/actions/setup/js/codex_harness.cjs +++ b/actions/setup/js/codex_harness.cjs @@ -53,6 +53,7 @@ const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractD const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError } = require("./harness_retry_guard.cjs"); const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs"); const { resolveRetryConfig } = require("./harness_retry_config.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const { applyModelFallback, injectModelFlagAfterExec } = require("./model_fallback.cjs"); const { parseMaxAICreditsExceededFromAuditLog } = require("./ai_credits_context.cjs"); @@ -494,8 +495,8 @@ async function main() { } // Diagnose API key presence so CI failures can be triaged without exposing secret values. - const codexApiKey = process.env.CODEX_API_KEY; - const openaiApiKey = process.env.OPENAI_API_KEY; + const codexApiKey = readSecretEnv("CODEX_API_KEY"); + const openaiApiKey = readSecretEnv("OPENAI_API_KEY"); const codexChildEnv = buildCodexChildEnv(process.env, codexApiKey, openaiApiKey); log(`secrets: CODEX_API_KEY=${codexApiKey ? `set (length=${codexApiKey.length})` : "not set"}` + ` OPENAI_API_KEY=${openaiApiKey ? `set (length=${openaiApiKey.length})` : "not set"}`); diff --git a/actions/setup/js/copilot_sdk_driver.cjs b/actions/setup/js/copilot_sdk_driver.cjs index 9716aaf3ffb..6b031c20d30 100644 --- a/actions/setup/js/copilot_sdk_driver.cjs +++ b/actions/setup/js/copilot_sdk_driver.cjs @@ -31,6 +31,7 @@ const { runWithCopilotSDK, extractPromptFromArgs } = require("./copilot_sdk_sess const { parsePermissionConfigFromServerArgs } = require("./copilot_sdk_permissions.cjs"); const { parseMultiProviderJson } = require("./copilot_sdk_multi_provider.cjs"); const { applyModelFallback } = require("./model_fallback.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); // Re-export the session and permission helpers so that existing callers that // require("./copilot_sdk_driver.cjs") (e.g. copilot_harness.cjs) continue to work. @@ -72,7 +73,7 @@ async function main() { process.exit(1); } - const connectionToken = process.env.COPILOT_CONNECTION_TOKEN; + const connectionToken = readSecretEnv("COPILOT_CONNECTION_TOKEN"); if (!connectionToken) { process.stderr.write("[copilot-sdk-driver] error: COPILOT_CONNECTION_TOKEN is required. This token is generated by copilot_harness.cjs and must be passed to the driver environment\n"); process.exit(1); @@ -94,7 +95,7 @@ async function main() { // The harness injects GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON before launching // this driver. Multi-provider BYOK is the only supported mode. - const multiProviderConfig = parseMultiProviderJson(process.env.GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON); + const multiProviderConfig = parseMultiProviderJson(readSecretEnv("GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON")); if (!multiProviderConfig) { process.stderr.write("[copilot-sdk-driver] error: GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON is not set or invalid — " + "ensure the harness resolved multi-provider config from awf-reflect data\n"); process.exit(1); diff --git a/actions/setup/js/create_agent_session.cjs b/actions/setup/js/create_agent_session.cjs index daee324f47f..02f276c3bc0 100644 --- a/actions/setup/js/create_agent_session.cjs +++ b/actions/setup/js/create_agent_session.cjs @@ -6,6 +6,7 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help const { getBaseBranch } = require("./get_base_branch.cjs"); const { isStagedMode } = require("./safe_output_helpers.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Module-level state — populated by handleMessage(), read by the exported getters below. @@ -28,7 +29,7 @@ let _allResults = []; * @returns {Promise} Authenticated GitHub client */ async function createAgentSessionGitHubClient(config) { - const token = config["github-token"] || process.env.GH_AW_AGENT_SESSION_TOKEN; + const token = config["github-token"] || readSecretEnv("GH_AW_AGENT_SESSION_TOKEN"); if (!token) { core.debug("No dedicated agent token configured — using step-level github client for create-agent-session operations"); return github; diff --git a/actions/setup/js/create_issue.cjs b/actions/setup/js/create_issue.cjs index 4943252ffd0..75d9e985d74 100644 --- a/actions/setup/js/create_issue.cjs +++ b/actions/setup/js/create_issue.cjs @@ -30,6 +30,7 @@ const { MAX_LABELS, MAX_ASSIGNEES } = require("./constants.cjs"); const { findAgent, getIssueDetails, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { parseDeduplicateByTitle, normalizeTitleForDedup, findDuplicateByTitle } = require("./issue_title_dedup.cjs"); const { resolveAllowedMentionsFromPayload } = require("./resolve_mentions_from_payload.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const MS_PER_DAY = 24 * 60 * 60 * 1000; const ISSUE_FIELD_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/; const RECENTLY_CLOSED_DEDUP_DAYS = 30; @@ -49,7 +50,7 @@ const TITLE_DEDUP_MIN_SEARCH_RATE_LIMIT_FRACTION = 0.2; * @returns {Promise} Authenticated GitHub client */ async function createCopilotAssignmentClient(config) { - const token = config["github-token"] || process.env.GH_AW_ASSIGN_TO_AGENT_TOKEN; + const token = config["github-token"] || readSecretEnv("GH_AW_ASSIGN_TO_AGENT_TOKEN"); if (!token) { core.debug("No dedicated agent token configured — using step-level github client for copilot assignment"); return github; diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 74c3336d67c..41c463eb273 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -35,6 +35,7 @@ const { COPILOT_REVIEWER_BOT, FAQ_CREATE_PR_PERMISSIONS_URL } = require("./const const { isStagedMode } = require("./safe_output_helpers.cjs"); const { normalizeCommitSHA } = require("./commit_sha_helpers.cjs"); const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const { findAgent, getIssueDetails, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits, getBundlePrerequisites, isShallowOrSparseCheckout, linearizeRangeAsCommit } = require("./git_helpers.cjs"); const { parseDiffGitHeader: parseDiffGitHeaderPaths, extractDiffGitHeaderEntries } = require("./patch_path_helpers.cjs"); @@ -78,7 +79,7 @@ const { * @returns {Promise} Authenticated GitHub client */ async function createCopilotAssignmentClient(config) { - const token = config["github-token"] || process.env.GH_AW_ASSIGN_TO_AGENT_TOKEN; + const token = config["github-token"] || readSecretEnv("GH_AW_ASSIGN_TO_AGENT_TOKEN"); if (!token) { core.debug("No dedicated agent token configured — using step-level github client for copilot assignment"); return github; @@ -935,7 +936,7 @@ async function main(config = {}) { // Create checkout manager for multi-repo support (fallback when no checkout_mapping) // Token is available via GITHUB_TOKEN environment variable (set by the workflow job) - const checkoutToken = process.env.GITHUB_TOKEN; + const checkoutToken = readSecretEnv("GITHUB_TOKEN"); const checkoutManager = checkoutToken ? createCheckoutManager(checkoutToken, { defaultBaseBranch: configBaseBranch }) : null; // Log multi-repo support status diff --git a/actions/setup/js/exchange_otlp_workload_identity.cjs b/actions/setup/js/exchange_otlp_workload_identity.cjs index b52b09f0629..fad4ff99e2a 100644 --- a/actions/setup/js/exchange_otlp_workload_identity.cjs +++ b/actions/setup/js/exchange_otlp_workload_identity.cjs @@ -12,14 +12,13 @@ */ const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; +const { readSecretEnv } = require("./read_secret_env.cjs"); async function main() { - const oidcToken = process.env.GH_AW_OTLP_OIDC_TOKEN; + const oidcToken = readSecretEnv("GH_AW_OTLP_OIDC_TOKEN"); if (!oidcToken) { throw new Error("Missing GitHub OIDC token for Google workload identity token exchange"); } - core.setSecret(oidcToken); - const response = await fetch("https://sts.googleapis.com/v1/token", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, diff --git a/actions/setup/js/extra_empty_commit.cjs b/actions/setup/js/extra_empty_commit.cjs index f297fe49f6f..8dad217855c 100644 --- a/actions/setup/js/extra_empty_commit.cjs +++ b/actions/setup/js/extra_empty_commit.cjs @@ -4,6 +4,7 @@ const { validateTargetRepo, parseAllowedRepos, getDefaultTargetRepo } = require("./repo_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { overridePersistedExtraheader, restorePersistedExtraheader } = require("./git_auth_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * @fileoverview Extra Empty Commit Helper @@ -55,7 +56,7 @@ function isCrossRepoTarget(repoOwner, repoName) { * @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>} */ async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage, newCommitCount, allowedRepos: allowedReposInput }) { - const token = process.env.GH_AW_CI_TRIGGER_TOKEN; + const token = readSecretEnv("GH_AW_CI_TRIGGER_TOKEN"); if (!token || !token.trim()) { core.info("No extra empty commit token configured - skipping"); diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index 0a7985d22a9..86e7349742b 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/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 { readSecretEnv } = require("./read_secret_env.cjs"); /** * Build GIT_CONFIG_* environment variables that inject an Authorization header @@ -24,7 +25,7 @@ const { isTransientError } = require("./error_recovery.cjs"); * Returns an empty object when no token is available. */ function getGitAuthEnv(token) { - const authToken = token || process.env.GITHUB_TOKEN; + const authToken = token || readSecretEnv("GITHUB_TOKEN"); if (!authToken) { core.debug("getGitAuthEnv: no token available, git network operations may fail if credentials were cleaned"); return {}; diff --git a/actions/setup/js/mount_mcp_as_cli.cjs b/actions/setup/js/mount_mcp_as_cli.cjs index 0aabc9bd832..16ae3d6d985 100644 --- a/actions/setup/js/mount_mcp_as_cli.cjs +++ b/actions/setup/js/mount_mcp_as_cli.cjs @@ -27,6 +27,7 @@ const http = require("http"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); const { renderSafeOutputsPromptDocs } = require("./mcp_cli_schema_docs.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const MANIFEST_FILE = path.join(process.env.RUNNER_TEMP || "/home/runner/work/_temp", "gh-aw/mcp-cli/manifest.json"); // Use RUNNER_TEMP so the bin and tools directories are inside the AWF sandbox mount @@ -555,7 +556,7 @@ async function main() { core.info(`Bridge script: ${bridgeScript}`); } - const apiKey = process.env.MCP_GATEWAY_API_KEY || ""; + const apiKey = readSecretEnv("MCP_GATEWAY_API_KEY") || ""; if (!apiKey) { core.warning("MCP_GATEWAY_API_KEY is not set; generated CLI wrappers will not be able to authenticate with the gateway"); } diff --git a/actions/setup/js/pi_agent_core_driver.cjs b/actions/setup/js/pi_agent_core_driver.cjs index 955bdb8c4ad..5ef7d16693f 100644 --- a/actions/setup/js/pi_agent_core_driver.cjs +++ b/actions/setup/js/pi_agent_core_driver.cjs @@ -40,6 +40,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); +const { readSecretEnv } = require("./read_secret_env.cjs"); // --------------------------------------------------------------------------- // Logging helpers @@ -140,7 +141,7 @@ function buildGetApiKey(gatewayConfig) { // holds the secret (Pi CLI's resolveConfigValue() semantics). const envVarName = gatewayConfig.apiKey; if (envVarName) { - const value = process.env[envVarName]; + const value = readSecretEnv(envVarName); if (value) return value; } } @@ -149,12 +150,12 @@ function buildGetApiKey(gatewayConfig) { switch (provider) { case "github-copilot": case "copilot": - return process.env.COPILOT_GITHUB_TOKEN || process.env.GITHUB_TOKEN; + return readSecretEnv("COPILOT_GITHUB_TOKEN") || readSecretEnv("GITHUB_TOKEN"); case "anthropic": - return process.env.ANTHROPIC_API_KEY; + return readSecretEnv("ANTHROPIC_API_KEY"); case "openai": case "codex": - return process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY; + return readSecretEnv("CODEX_API_KEY") || readSecretEnv("OPENAI_API_KEY"); default: return undefined; } diff --git a/actions/setup/js/pi_provider.cjs b/actions/setup/js/pi_provider.cjs index cf302aa3254..a53791e6427 100644 --- a/actions/setup/js/pi_provider.cjs +++ b/actions/setup/js/pi_provider.cjs @@ -31,6 +31,7 @@ const { fetchAWFReflect, AWF_API_PROXY_REFLECT_URL, AWF_REFLECT_OUTPUT_PATH, AWF const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); // Default logger: prefixed with "[gh-aw/pi-provider]" for easy grepping. // prettier-ignore @@ -241,7 +242,7 @@ function registerProviderAliases(pi, names, config, logger) { function registerConfiguredProviders(pi, logger) { let registeredCount = 0; - const copilotToken = process.env.COPILOT_GITHUB_TOKEN || process.env.GITHUB_TOKEN; + const copilotToken = readSecretEnv("COPILOT_GITHUB_TOKEN") || readSecretEnv("GITHUB_TOKEN"); if (copilotToken) { registerProviderAliases( pi, @@ -256,12 +257,13 @@ function registerConfiguredProviders(pi, logger) { registeredCount += 2; } - if (process.env.ANTHROPIC_API_KEY) { + const anthropicApiKey = readSecretEnv("ANTHROPIC_API_KEY"); + if (anthropicApiKey) { registerProviderAliases( pi, ["anthropic"], { - apiKey: process.env.ANTHROPIC_API_KEY, + apiKey: anthropicApiKey, api: "anthropic", ...(process.env.ANTHROPIC_BASE_URL ? { baseUrl: process.env.ANTHROPIC_BASE_URL } : {}), }, @@ -270,7 +272,7 @@ function registerConfiguredProviders(pi, logger) { registeredCount += 1; } - const openAIKey = process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY; + const openAIKey = readSecretEnv("CODEX_API_KEY") || readSecretEnv("OPENAI_API_KEY"); if (openAIKey) { registerProviderAliases( pi, diff --git a/actions/setup/js/push_experiment_state.cjs b/actions/setup/js/push_experiment_state.cjs index 564bcbe9fa4..d9cbd713235 100644 --- a/actions/setup/js/push_experiment_state.cjs +++ b/actions/setup/js/push_experiment_state.cjs @@ -30,6 +30,7 @@ const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); const { execGitSync, getGitAuthEnv, withGitRetry } = require("./git_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); @@ -340,7 +341,7 @@ async function main() { .split(",") .map(name => name.trim()) .filter(Boolean); - const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || ""; + const ghToken = readSecretEnv("GH_TOKEN") || readSecretEnv("GITHUB_TOKEN") || ""; const githubRunId = process.env.GITHUB_RUN_ID || "unknown"; const githubServerUrl = (process.env.GITHUB_SERVER_URL || "https://github.com").replace(/\/$/, ""); const serverHost = githubServerUrl.replace(/^https?:\/\//, ""); diff --git a/actions/setup/js/push_repo_memory.cjs b/actions/setup/js/push_repo_memory.cjs index 9e783d301e5..8dffd02ff25 100644 --- a/actions/setup/js/push_repo_memory.cjs +++ b/actions/setup/js/push_repo_memory.cjs @@ -10,6 +10,7 @@ const { execGitSync, getGitAuthEnv } = 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"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Push repo-memory changes to git branch @@ -62,7 +63,7 @@ async function main() { } } - const ghToken = process.env.GH_TOKEN; + const ghToken = readSecretEnv("GH_TOKEN"); const githubRunId = process.env.GITHUB_RUN_ID || "unknown"; const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; const serverHost = githubServerUrl.replace(/^https?:\/\//, ""); diff --git a/actions/setup/js/read_secret_env.cjs b/actions/setup/js/read_secret_env.cjs new file mode 100644 index 00000000000..2e8cc4d9d71 --- /dev/null +++ b/actions/setup/js/read_secret_env.cjs @@ -0,0 +1,21 @@ +// @ts-check +"use strict"; + +require("./shim.cjs"); + +/** + * Read a secret from the environment and register its value for masking in + * GitHub Actions logs. + * + * @param {string} name + * @returns {string|undefined} + */ +function readSecretEnv(name) { + const value = process.env[name]; + if (value) { + core.setSecret(value); + } + return value; +} + +module.exports = { readSecretEnv }; diff --git a/actions/setup/js/read_secret_env.test.cjs b/actions/setup/js/read_secret_env.test.cjs new file mode 100644 index 00000000000..3bf5c9c4279 --- /dev/null +++ b/actions/setup/js/read_secret_env.test.cjs @@ -0,0 +1,43 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createRequire } from "module"; + +const req = createRequire(import.meta.url); +const originalCore = global.core; +const setSecret = vi.fn(); +global.core = { setSecret }; +const { readSecretEnv } = req("./read_secret_env.cjs"); + +describe("readSecretEnv", () => { + afterAll(() => { + global.core = originalCore; + }); + + beforeEach(() => { + setSecret.mockClear(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("returns and masks a configured secret", () => { + vi.stubEnv("TEST_SECRET", "secret-value"); + + expect(readSecretEnv("TEST_SECRET")).toBe("secret-value"); + expect(setSecret).toHaveBeenCalledWith("secret-value"); + }); + + it("does not mask a missing secret", () => { + vi.stubEnv("TEST_SECRET", undefined); + + expect(readSecretEnv("TEST_SECRET")).toBeUndefined(); + expect(setSecret).not.toHaveBeenCalled(); + }); + + it("does not mask an empty secret", () => { + vi.stubEnv("TEST_SECRET", ""); + + expect(readSecretEnv("TEST_SECRET")).toBe(""); + expect(setSecret).not.toHaveBeenCalled(); + }); +}); diff --git a/actions/setup/js/redact_evals_results.cjs b/actions/setup/js/redact_evals_results.cjs index 06b8caf829e..1ad9927e92c 100644 --- a/actions/setup/js/redact_evals_results.cjs +++ b/actions/setup/js/redact_evals_results.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const { EVALS_OUTPUT_PATH } = require("./evals_constants.cjs"); const { main: redactWorkspaceSecrets, redactSecrets, redactBuiltInPatterns, extractMCPGatewayTokens, MCP_GATEWAY_CONFIG_PATHS } = require("./redact_secrets.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); function getSecretValues() { const secretNames = (process.env.GH_AW_SECRET_NAMES || "") @@ -14,7 +15,7 @@ function getSecretValues() { /** @type {string[]} */ const secretValues = []; for (const secretName of secretNames) { - const value = process.env[`SECRET_${secretName}`]; + const value = readSecretEnv(`SECRET_${secretName}`); if (typeof value === "string" && value.trim() !== "") { secretValues.push(value.trim()); } diff --git a/actions/setup/js/redact_secrets.cjs b/actions/setup/js/redact_secrets.cjs index 98080c2616c..36928d339ca 100644 --- a/actions/setup/js/redact_secrets.cjs +++ b/actions/setup/js/redact_secrets.cjs @@ -10,6 +10,7 @@ const fs = require("fs"); const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_VALIDATION } = require("./error_codes.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Recursively finds all files matching the specified extensions * @param {string} dir - Directory to search @@ -231,7 +232,7 @@ async function main() { const secretNameList = secretNames.split(",").filter(name => name.trim()); for (const secretName of secretNameList) { const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; + const secretValue = readSecretEnv(envVarName); // Skip empty or undefined secrets if (!secretValue || secretValue.trim() === "") { continue; diff --git a/actions/setup/js/run_operation_update_upgrade.cjs b/actions/setup/js/run_operation_update_upgrade.cjs index bd422abeb8b..015b75f67a3 100644 --- a/actions/setup/js/run_operation_update_upgrade.cjs +++ b/actions/setup/js/run_operation_update_upgrade.cjs @@ -4,6 +4,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_CONFIG, ERR_SYSTEM } = require("./error_codes.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Files that the 'update' command can modify outside of .github/workflows/. @@ -155,7 +156,7 @@ async function main() { // Push to the new branch using a token-authenticated remote const owner = context.repo.owner; const repo = context.repo.repo; - const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; + const token = readSecretEnv("GH_TOKEN") || readSecretEnv("GITHUB_TOKEN"); 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.`); } diff --git a/actions/setup/js/safe_output_handler_manager.cjs b/actions/setup/js/safe_output_handler_manager.cjs index ca152ba02da..d3ad7e95f35 100644 --- a/actions/setup/js/safe_output_handler_manager.cjs +++ b/actions/setup/js/safe_output_handler_manager.cjs @@ -17,6 +17,7 @@ const { hasUnresolvedTemporaryIds, replaceTemporaryIdReferences, replaceArtifact const { generateMissingInfoSections } = require("./missing_info_formatter.cjs"); const { setCollectedMissings } = require("./missing_messages_helper.cjs"); const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const { getAssignToAgentAssigned, getAssignToAgentErrors, getAssignToAgentErrorCount, writeAssignToAgentSummary } = require("./assign_to_agent.cjs"); const { getCreateAgentSessionNumber, getCreateAgentSessionUrl, writeCreateAgentSessionSummary } = require("./create_agent_session.cjs"); const { createPrReviewBufferRegistry } = require("./pr_review_buffer.cjs"); @@ -342,8 +343,11 @@ async function loadHandlers(config, prReviewBufferRegistry, resolvedAllowedMenti // Call the factory function with config to get the message handler const handlerConfig = { ...(config[type] || {}) }; - if (PROJECT_HANDLER_TYPES.has(type) && !handlerConfig[GITHUB_TOKEN_CONFIG_KEY] && process.env.GH_AW_PROJECT_GITHUB_TOKEN) { - handlerConfig[GITHUB_TOKEN_CONFIG_KEY] = process.env.GH_AW_PROJECT_GITHUB_TOKEN; + if (PROJECT_HANDLER_TYPES.has(type) && !handlerConfig[GITHUB_TOKEN_CONFIG_KEY]) { + const projectToken = readSecretEnv("GH_AW_PROJECT_GITHUB_TOKEN"); + if (projectToken) { + handlerConfig[GITHUB_TOKEN_CONFIG_KEY] = projectToken; + } } // Pass top-level mentions policy through so handlers can preserve diff --git a/actions/setup/js/safe_outputs_config.cjs b/actions/setup/js/safe_outputs_config.cjs index cac513816f6..21498b73d0b 100644 --- a/actions/setup/js/safe_outputs_config.cjs +++ b/actions/setup/js/safe_outputs_config.cjs @@ -3,6 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { redactSensitiveConfig } = require("./safe_outputs_config_redact.cjs"); const { ERR_SYSTEM } = require("./error_codes.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); const fs = require("fs"); const path = require("path"); @@ -37,7 +38,10 @@ function resolveEnvPlaceholders(value) { if (typeof value !== "string") { return value; } - return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => process.env[envName] ?? match); + return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => { + const envValue = /(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|AUTH)/.test(envName) ? readSecretEnv(envName) : process.env[envName]; + return envValue ?? match; + }); } /** diff --git a/actions/setup/js/shim.cjs b/actions/setup/js/shim.cjs index f903fd94003..bfdad0e19ee 100644 --- a/actions/setup/js/shim.cjs +++ b/actions/setup/js/shim.cjs @@ -12,6 +12,17 @@ * `github-script`) the respective block is a no-op. */ +/** + * Escape workflow command data using the same encoding as @actions/core. + * @param {string} value + */ +const escapeCommandData = value => value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A"); + +/** @param {string} secret */ +const setSecret = secret => { + process.stderr.write(`::add-mask::${escapeCommandData(secret)}\n`); +}; + if (!global.core) { /** * Write shim log lines to stderr so MCP servers that speak JSON-RPC on stdout @@ -40,7 +51,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/actions/setup/js/shim.test.cjs b/actions/setup/js/shim.test.cjs new file mode 100644 index 00000000000..06cb8ed913e --- /dev/null +++ b/actions/setup/js/shim.test.cjs @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "child_process"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +describe("core shim", () => { + it("emits escaped add-mask workflow commands for secrets", () => { + const result = spawnSync(process.execPath, ["-e", 'require("./shim.cjs"); core.setSecret("a%b\\nc\\r");'], { + cwd: __dirname, + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(""); + expect(result.stderr).toBe("::add-mask::a%25b%0Ac%0D\n"); + }); + + it("adds setSecret to an existing partial core object", () => { + const result = spawnSync(process.execPath, ["-e", 'global.core = {}; require("./shim.cjs"); core.setSecret("secret");'], { + cwd: __dirname, + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stderr).toBe("::add-mask::secret\n"); + }); +}); diff --git a/actions/setup/js/start_mcp_gateway.cjs b/actions/setup/js/start_mcp_gateway.cjs index 1a902d8e1d8..435e584a1fe 100644 --- a/actions/setup/js/start_mcp_gateway.cjs +++ b/actions/setup/js/start_mcp_gateway.cjs @@ -36,6 +36,7 @@ const path = require("path"); const { withRetry } = require("./error_recovery.cjs"); const { lstatGuard } = require("./symlink_guard.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** @type {number | null} */ let activeGatewayPid = null; @@ -336,7 +337,7 @@ async function main() { process.umask(0o077); const dockerCommand = process.env.MCP_GATEWAY_DOCKER_COMMAND; - const apiKey = process.env.MCP_GATEWAY_API_KEY; + const apiKey = readSecretEnv("MCP_GATEWAY_API_KEY"); const gatewayPort = process.env.MCP_GATEWAY_PORT; const gatewayDomain = process.env.MCP_GATEWAY_DOMAIN; const runnerTemp = process.env.RUNNER_TEMP; diff --git a/actions/setup/js/test-live-github-api.cjs b/actions/setup/js/test-live-github-api.cjs index 853acf34714..a9a63ff2a83 100755 --- a/actions/setup/js/test-live-github-api.cjs +++ b/actions/setup/js/test-live-github-api.cjs @@ -1,6 +1,7 @@ #!/usr/bin/env node // @ts-check require("./shim.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Standalone script to test frontmatter hash computation with live GitHub API @@ -18,7 +19,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); async function testLiveGitHubAPI() { // Check for GitHub token - const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; + const token = readSecretEnv("GITHUB_TOKEN") || readSecretEnv("GH_TOKEN"); if (!token) { core.setFailed( "❌ Error: No GitHub token found\n" + diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index d4f50a1ac4e..07393655b46 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -9,6 +9,7 @@ const { isStagedMode } = require("./safe_output_helpers.cjs"); const { ERR_API, ERR_CONFIG, ERR_NOT_FOUND, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs"); const { parseRepoSlug, resolveTargetRepoConfig, isRepoAllowed } = require("./repo_helpers.cjs"); const { logGraphQLError } = require("./github_api_helpers.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** @type {import('./github_api_helpers.cjs').GraphQLErrorHints} */ const PROJECT_GRAPHQL_HINTS = { @@ -1266,7 +1267,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = } } catch (error) { if (getErrorMessage(error) && getErrorMessage(error).includes("does not have permission to create projects")) { - const usingCustomToken = !!process.env.GH_AW_PROJECT_GITHUB_TOKEN; + const usingCustomToken = !!readSecretEnv("GH_AW_PROJECT_GITHUB_TOKEN"); core.error( `Failed to manage project: ${getErrorMessage(error)}\n\nTroubleshooting:\n • Create the project manually at https://github.com/orgs/${owner}/projects/new.\n • Or supply a PAT (classic with project + repo scopes, or fine-grained with Projects: Read+Write) via GH_AW_PROJECT_GITHUB_TOKEN.\n • Or use a GitHub App with Projects: Read+Write permission.\n • Ensure the workflow grants projects: write.\n\n` + (usingCustomToken ? "GH_AW_PROJECT_GITHUB_TOKEN is set but lacks access." : "Using default GITHUB_TOKEN - this cannot access Projects v2 API. You must configure GH_AW_PROJECT_GITHUB_TOKEN.") diff --git a/actions/setup/js/validate_lockdown_requirements.cjs b/actions/setup/js/validate_lockdown_requirements.cjs index d69d45f0263..4bc6430ef4a 100644 --- a/actions/setup/js/validate_lockdown_requirements.cjs +++ b/actions/setup/js/validate_lockdown_requirements.cjs @@ -1,6 +1,7 @@ // @ts-check const { renderLockdownTokenErrorMessage, renderPublicStrictModeErrorMessage, renderPullRequestTargetErrorMessage } = require("./validate_lockdown_requirements_templates.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Validates that lockdown mode requirements are met at runtime. @@ -47,9 +48,9 @@ function validateLockdownRequirements(core) { // Check if any custom GitHub token is configured // This matches the token selection logic used by the MCP gateway: // GH_AW_GITHUB_MCP_SERVER_TOKEN || GH_AW_GITHUB_TOKEN || custom github-token - const hasGhAwToken = !!process.env.GH_AW_GITHUB_TOKEN; - const hasGhAwMcpToken = !!process.env.GH_AW_GITHUB_MCP_SERVER_TOKEN; - const hasCustomToken = !!process.env.CUSTOM_GITHUB_TOKEN; + const hasGhAwToken = !!readSecretEnv("GH_AW_GITHUB_TOKEN"); + const hasGhAwMcpToken = !!readSecretEnv("GH_AW_GITHUB_MCP_SERVER_TOKEN"); + const hasCustomToken = !!readSecretEnv("CUSTOM_GITHUB_TOKEN"); const hasAnyCustomToken = hasGhAwToken || hasGhAwMcpToken || hasCustomToken; core.info(`GH_AW_GITHUB_TOKEN configured: ${hasGhAwToken}`); diff --git a/actions/setup/js/validate_secrets.cjs b/actions/setup/js/validate_secrets.cjs index 1b2d64f87d0..fb69f4922a4 100644 --- a/actions/setup/js/validate_secrets.cjs +++ b/actions/setup/js/validate_secrets.cjs @@ -19,6 +19,7 @@ const { exec } = require("child_process"); const execAsync = promisify(exec); const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_VALIDATION } = require("./error_codes.cjs"); +const { readSecretEnv } = require("./read_secret_env.cjs"); /** * Test result status @@ -616,7 +617,7 @@ async function main() { // Test GH_AW_GITHUB_TOKEN core.info("Testing GH_AW_GITHUB_TOKEN..."); - const ghAwToken = process.env.GH_AW_GITHUB_TOKEN; + const ghAwToken = readSecretEnv("GH_AW_GITHUB_TOKEN"); const restResult = await testGitHubRESTAPI(ghAwToken, owner, repo); results.push({ secret: "GH_AW_GITHUB_TOKEN", @@ -635,7 +636,7 @@ async function main() { // Test GH_AW_GITHUB_MCP_SERVER_TOKEN core.info("Testing GH_AW_GITHUB_MCP_SERVER_TOKEN..."); - const mcpToken = process.env.GH_AW_GITHUB_MCP_SERVER_TOKEN; + const mcpToken = readSecretEnv("GH_AW_GITHUB_MCP_SERVER_TOKEN"); const mcpRestResult = await testGitHubRESTAPI(mcpToken, owner, repo); results.push({ secret: "GH_AW_GITHUB_MCP_SERVER_TOKEN", @@ -646,7 +647,7 @@ async function main() { // Test GH_AW_PROJECT_GITHUB_TOKEN core.info("Testing GH_AW_PROJECT_GITHUB_TOKEN..."); - const projectToken = process.env.GH_AW_PROJECT_GITHUB_TOKEN; + const projectToken = readSecretEnv("GH_AW_PROJECT_GITHUB_TOKEN"); const projectRestResult = await testGitHubRESTAPI(projectToken, owner, repo); results.push({ secret: "GH_AW_PROJECT_GITHUB_TOKEN", @@ -657,7 +658,7 @@ async function main() { // Test GH_AW_COPILOT_TOKEN core.info("Testing GH_AW_COPILOT_TOKEN..."); - const copilotToken = process.env.GH_AW_COPILOT_TOKEN; + const copilotToken = readSecretEnv("GH_AW_COPILOT_TOKEN"); const copilotOrgBilling = process.env.GH_AW_COPILOT_ORG_BILLING === "true"; const copilotResult = await testCopilotToken(copilotToken, copilotOrgBilling); results.push({ @@ -669,7 +670,7 @@ async function main() { // Test ANTHROPIC_API_KEY core.info("Testing ANTHROPIC_API_KEY..."); - const anthropicKey = process.env.ANTHROPIC_API_KEY; + const anthropicKey = readSecretEnv("ANTHROPIC_API_KEY"); const anthropicResult = await testAnthropicAPI(anthropicKey); results.push({ secret: "ANTHROPIC_API_KEY", @@ -680,7 +681,7 @@ async function main() { // Test OPENAI_API_KEY core.info("Testing OPENAI_API_KEY..."); - const openaiKey = process.env.OPENAI_API_KEY; + const openaiKey = readSecretEnv("OPENAI_API_KEY"); const openaiResult = await testOpenAIAPI(openaiKey); results.push({ secret: "OPENAI_API_KEY", @@ -691,7 +692,7 @@ async function main() { // Test BRAVE_API_KEY core.info("Testing BRAVE_API_KEY..."); - const braveKey = process.env.BRAVE_API_KEY; + const braveKey = readSecretEnv("BRAVE_API_KEY"); const braveResult = await testBraveSearchAPI(braveKey); results.push({ secret: "BRAVE_API_KEY", @@ -702,7 +703,7 @@ async function main() { // Test NOTION_API_TOKEN core.info("Testing NOTION_API_TOKEN..."); - const notionToken = process.env.NOTION_API_TOKEN; + const notionToken = readSecretEnv("NOTION_API_TOKEN"); const notionResult = await testNotionAPI(notionToken); results.push({ secret: "NOTION_API_TOKEN", diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 2576943dc53..ad987764ac6 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -250,6 +250,7 @@ MCP_SCRIPTS_FILES=( "error_helpers.cjs" "error_codes.cjs" "constants.cjs" + "read_secret_env.cjs" "mcp_enhanced_errors.cjs" "shim.cjs" "mcp-scripts-runner.cjs" @@ -336,6 +337,7 @@ SAFE_OUTPUTS_FILES=( "error_helpers.cjs" "error_codes.cjs" "constants.cjs" + "read_secret_env.cjs" "git_helpers.cjs" "error_recovery.cjs" "checkout_manifest.cjs" From ef1d15e0029e7e8045208d51ffebc23308f56af7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:38:23 +0000 Subject: [PATCH 2/3] Harden secret masking runtime compatibility Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/exchange_otlp_workload_identity.cjs | 5 +++-- actions/setup/js/push_signed_commits.test.cjs | 1 + actions/setup/js/read_secret_env.cjs | 4 ++-- actions/setup/js/read_secret_env.test.cjs | 16 ++++++++++++++++ actions/setup/js/shim.cjs | 12 ++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/exchange_otlp_workload_identity.cjs b/actions/setup/js/exchange_otlp_workload_identity.cjs index fad4ff99e2a..b52b09f0629 100644 --- a/actions/setup/js/exchange_otlp_workload_identity.cjs +++ b/actions/setup/js/exchange_otlp_workload_identity.cjs @@ -12,13 +12,14 @@ */ const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; -const { readSecretEnv } = require("./read_secret_env.cjs"); async function main() { - const oidcToken = readSecretEnv("GH_AW_OTLP_OIDC_TOKEN"); + 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); + const response = await fetch("https://sts.googleapis.com/v1/token", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, diff --git a/actions/setup/js/push_signed_commits.test.cjs b/actions/setup/js/push_signed_commits.test.cjs index cfea7eb4518..c3aebf80ec4 100644 --- a/actions/setup/js/push_signed_commits.test.cjs +++ b/actions/setup/js/push_signed_commits.test.cjs @@ -255,6 +255,7 @@ describe("push_signed_commits integration tests", () => { warning: vi.fn(), error: vi.fn(), debug: vi.fn(), + setSecret: vi.fn(), }; global.core = mockCore; diff --git a/actions/setup/js/read_secret_env.cjs b/actions/setup/js/read_secret_env.cjs index 2e8cc4d9d71..1775d9c4845 100644 --- a/actions/setup/js/read_secret_env.cjs +++ b/actions/setup/js/read_secret_env.cjs @@ -1,8 +1,6 @@ // @ts-check "use strict"; -require("./shim.cjs"); - /** * Read a secret from the environment and register its value for masking in * GitHub Actions logs. @@ -13,6 +11,8 @@ require("./shim.cjs"); function readSecretEnv(name) { const value = process.env[name]; if (value) { + const { ensureCoreSetSecret } = require("./shim.cjs"); + ensureCoreSetSecret(); core.setSecret(value); } return value; diff --git a/actions/setup/js/read_secret_env.test.cjs b/actions/setup/js/read_secret_env.test.cjs index 3bf5c9c4279..a05465fe94a 100644 --- a/actions/setup/js/read_secret_env.test.cjs +++ b/actions/setup/js/read_secret_env.test.cjs @@ -1,7 +1,11 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createRequire } from "module"; +import { spawnSync } from "child_process"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; const req = createRequire(import.meta.url); +const __dirname = dirname(fileURLToPath(import.meta.url)); const originalCore = global.core; const setSecret = vi.fn(); global.core = { setSecret }; @@ -40,4 +44,16 @@ describe("readSecretEnv", () => { expect(readSecretEnv("TEST_SECRET")).toBe(""); expect(setSecret).not.toHaveBeenCalled(); }); + + it("uses the core shim when loaded by a standalone Node.js process", () => { + const result = spawnSync(process.execPath, ["-e", 'const { readSecretEnv } = require("./read_secret_env.cjs"); process.stdout.write(readSecretEnv("TEST_SECRET"));'], { + cwd: __dirname, + encoding: "utf8", + env: { ...process.env, TEST_SECRET: "standalone-secret" }, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("standalone-secret"); + expect(result.stderr).toBe("::add-mask::standalone-secret\n"); + }); }); diff --git a/actions/setup/js/shim.cjs b/actions/setup/js/shim.cjs index bfdad0e19ee..828a2d37350 100644 --- a/actions/setup/js/shim.cjs +++ b/actions/setup/js/shim.cjs @@ -23,6 +23,16 @@ const setSecret = secret => { process.stderr.write(`::add-mask::${escapeCommandData(secret)}\n`); }; +function ensureCoreSetSecret() { + if (!global.core) { + global.core = {}; + } + if (typeof global.core.setSecret !== "function") { + global.core.setSecret = setSecret; + } + return global.core; +} + if (!global.core) { /** * Write shim log lines to stderr so MCP servers that speak JSON-RPC on stdout @@ -97,3 +107,5 @@ if (!global.context) { repo: { owner, repo }, }; } + +module.exports = { ensureCoreSetSecret, setSecret }; From 6449784f07d50c1d3403fa52bac92bd57de776f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:11:28 +0000 Subject: [PATCH 3/3] Address secret masking review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/read_secret_env.cjs | 23 +++++++- actions/setup/js/read_secret_env.test.cjs | 10 ++++ actions/setup/js/safe_outputs_config.cjs | 4 +- actions/setup/js/safe_outputs_config.test.cjs | 40 ++++++++----- actions/setup/js/shim.cjs | 58 +++++++++++-------- actions/setup/js/start_mcp_gateway.cjs | 3 +- 6 files changed, 93 insertions(+), 45 deletions(-) diff --git a/actions/setup/js/read_secret_env.cjs b/actions/setup/js/read_secret_env.cjs index 1775d9c4845..b24c8e8a643 100644 --- a/actions/setup/js/read_secret_env.cjs +++ b/actions/setup/js/read_secret_env.cjs @@ -12,10 +12,27 @@ function readSecretEnv(name) { const value = process.env[name]; if (value) { const { ensureCoreSetSecret } = require("./shim.cjs"); - ensureCoreSetSecret(); - core.setSecret(value); + const coreShim = ensureCoreSetSecret(); + coreShim.setSecret(value); } return value; } -module.exports = { readSecretEnv }; +function isSecretEnvName(name) { + return /(?:^|_)(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|AUTH|PAT)(?:_|$)/.test(name); +} + +function maskSecretEnvValues(env = process.env) { + const { ensureCoreSetSecret } = require("./shim.cjs"); + const coreShim = ensureCoreSetSecret(); + let masked = 0; + for (const [name, value] of Object.entries(env)) { + if (value && isSecretEnvName(name)) { + coreShim.setSecret(value); + masked++; + } + } + return masked; +} + +module.exports = { isSecretEnvName, maskSecretEnvValues, readSecretEnv }; diff --git a/actions/setup/js/read_secret_env.test.cjs b/actions/setup/js/read_secret_env.test.cjs index a05465fe94a..559f2d5447d 100644 --- a/actions/setup/js/read_secret_env.test.cjs +++ b/actions/setup/js/read_secret_env.test.cjs @@ -8,6 +8,9 @@ const req = createRequire(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url)); const originalCore = global.core; const setSecret = vi.fn(); +// Install the core mock before requiring read_secret_env.cjs. The standalone +// shim fallback is covered in a subprocess below so this module can keep using +// the mocked core captured at require time. global.core = { setSecret }; const { readSecretEnv } = req("./read_secret_env.cjs"); @@ -45,6 +48,13 @@ describe("readSecretEnv", () => { expect(setSecret).not.toHaveBeenCalled(); }); + it("masks a whitespace-only secret", () => { + vi.stubEnv("TEST_SECRET", " "); + + expect(readSecretEnv("TEST_SECRET")).toBe(" "); + expect(setSecret).toHaveBeenCalledWith(" "); + }); + it("uses the core shim when loaded by a standalone Node.js process", () => { const result = spawnSync(process.execPath, ["-e", 'const { readSecretEnv } = require("./read_secret_env.cjs"); process.stdout.write(readSecretEnv("TEST_SECRET"));'], { cwd: __dirname, diff --git a/actions/setup/js/safe_outputs_config.cjs b/actions/setup/js/safe_outputs_config.cjs index 21498b73d0b..5337dbce0e8 100644 --- a/actions/setup/js/safe_outputs_config.cjs +++ b/actions/setup/js/safe_outputs_config.cjs @@ -3,7 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { redactSensitiveConfig } = require("./safe_outputs_config_redact.cjs"); const { ERR_SYSTEM } = require("./error_codes.cjs"); -const { readSecretEnv } = require("./read_secret_env.cjs"); +const { isSecretEnvName, readSecretEnv } = require("./read_secret_env.cjs"); const fs = require("fs"); const path = require("path"); @@ -39,7 +39,7 @@ function resolveEnvPlaceholders(value) { return value; } return value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (match, envName) => { - const envValue = /(?:TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|AUTH)/.test(envName) ? readSecretEnv(envName) : process.env[envName]; + const envValue = isSecretEnvName(envName) ? readSecretEnv(envName) : process.env[envName]; return envValue ?? match; }); } diff --git a/actions/setup/js/safe_outputs_config.test.cjs b/actions/setup/js/safe_outputs_config.test.cjs index c8908fdc7c7..cf0bcbe8ed0 100644 --- a/actions/setup/js/safe_outputs_config.test.cjs +++ b/actions/setup/js/safe_outputs_config.test.cjs @@ -194,28 +194,36 @@ describe("safe_outputs_config", () => { it("should resolve env placeholders in memory without logging token values", () => { const configDir = path.dirname(testConfigPath); fs.mkdirSync(configDir, { recursive: true }); + const originalCore = global.core; + const setSecret = vi.fn(); + global.core = { setSecret }; process.env.WRITE_PROJECT_PAT = "runtime-project-token"; process.env.GH_AW_INPUT_TARGET_REPO = "github/docs"; - fs.writeFileSync( - testConfigPath, - JSON.stringify({ - "update-project": { - "github-token": "${WRITE_PROJECT_PAT}", - "target-repo": "${GH_AW_INPUT_TARGET_REPO}", - }, - }) - ); + try { + fs.writeFileSync( + testConfigPath, + JSON.stringify({ + "update-project": { + "github-token": "${WRITE_PROJECT_PAT}", + "target-repo": "${GH_AW_INPUT_TARGET_REPO}", + }, + }) + ); - /** @type {import("./safe_outputs_config.cjs").LoadConfigResult} */ - const result = loadConfig(mockServer); + /** @type {import("./safe_outputs_config.cjs").LoadConfigResult} */ + const result = loadConfig(mockServer); - expect(result.config.update_project["github-token"]).toBe("runtime-project-token"); - expect(result.config.update_project["target-repo"]).toBe("github/docs"); + expect(result.config.update_project["github-token"]).toBe("runtime-project-token"); + expect(result.config.update_project["target-repo"]).toBe("github/docs"); + expect(setSecret).toHaveBeenCalledWith("runtime-project-token"); - const debugOutput = mockServer.debug.mock.calls.map(call => String(call[0])).join("\n"); - expect(debugOutput).toContain("***REDACTED***"); - expect(debugOutput).not.toContain("runtime-project-token"); + const debugOutput = mockServer.debug.mock.calls.map(call => String(call[0])).join("\n"); + expect(debugOutput).toContain("***REDACTED***"); + expect(debugOutput).not.toContain("runtime-project-token"); + } finally { + global.core = originalCore; + } }); it("should emit exactly one diagnostic when a GH_AW_INPUT_* placeholder is duplicated and unresolved", () => { diff --git a/actions/setup/js/shim.cjs b/actions/setup/js/shim.cjs index 828a2d37350..32c684814da 100644 --- a/actions/setup/js/shim.cjs +++ b/actions/setup/js/shim.cjs @@ -23,28 +23,18 @@ const setSecret = secret => { process.stderr.write(`::add-mask::${escapeCommandData(secret)}\n`); }; -function ensureCoreSetSecret() { - if (!global.core) { - global.core = {}; - } - if (typeof global.core.setSecret !== "function") { - global.core.setSecret = setSecret; - } - return global.core; -} - -if (!global.core) { - /** - * Write shim log lines to stderr so MCP servers that speak JSON-RPC on stdout - * never interleave protocol frames with diagnostic output. - * @param {string} level - * @param {string} message - */ - const writeShimLog = (level, message) => { - process.stderr.write(`[${level}] ${message}\n`); - }; +/** + * Write shim log lines to stderr so MCP servers that speak JSON-RPC on stdout + * never interleave protocol frames with diagnostic output. + * @param {string} level + * @param {string} message + */ +const writeShimLog = (level, message) => { + process.stderr.write(`[${level}] ${message}\n`); +}; - global.core = { +function makeShimCore() { + return { debug: /** @param {string} message */ message => writeShimLog("debug", message), info: /** @param {string} message */ message => writeShimLog("info", message), notice: /** @param {string} message */ message => writeShimLog("notice", message), @@ -63,8 +53,30 @@ if (!global.core) { }, setSecret, }; -} else if (typeof global.core.setSecret !== "function") { - global.core.setSecret = setSecret; +} + +function applyMissingShimCoreMethods(coreObj) { + const shimCore = makeShimCore(); + for (const [name, fn] of Object.entries(shimCore)) { + if (typeof coreObj[name] !== "function") { + coreObj[name] = fn; + } + } + return coreObj; +} + +function ensureCoreSetSecret() { + if (!global.core) { + global.core = makeShimCore(); + return global.core; + } + return applyMissingShimCoreMethods(global.core); +} + +if (!global.core) { + global.core = makeShimCore(); +} else { + applyMissingShimCoreMethods(global.core); } if (!global.context) { diff --git a/actions/setup/js/start_mcp_gateway.cjs b/actions/setup/js/start_mcp_gateway.cjs index 435e584a1fe..3a0d1d3c3ee 100644 --- a/actions/setup/js/start_mcp_gateway.cjs +++ b/actions/setup/js/start_mcp_gateway.cjs @@ -36,7 +36,7 @@ const path = require("path"); const { withRetry } = require("./error_recovery.cjs"); const { lstatGuard } = require("./symlink_guard.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); -const { readSecretEnv } = require("./read_secret_env.cjs"); +const { maskSecretEnvValues, readSecretEnv } = require("./read_secret_env.cjs"); /** @type {number | null} */ let activeGatewayPid = null; @@ -547,6 +547,7 @@ async function main() { const outputFd = fs.openSync(outputPath, "w", 0o600); const stderrFd = fs.openSync(stderrLogPath, "w", 0o600); + maskSecretEnvValues(process.env); const child = spawn(cmd, args, { stdio: ["pipe", outputFd, stderrFd],