Skip to content

Mask secrets read by the JavaScript runtime - #50737

Closed
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/add-secret-helper-function
Closed

Mask secrets read by the JavaScript runtime#50737
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/add-secret-helper-function

Conversation

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

JavaScript runtime components could read authentication values from environment variables without registering them for GitHub Actions log masking.

Changes

  • Secret access

    • Add readSecretEnv() to mask every non-empty value through core.setSecret().
    • Route token, API key, credential, and secret reads through the helper.
  • Runtime compatibility

    • Add escaped ::add-mask:: support to shim.cjs for standalone Node.js and MCP processes.
    • Preserve native core.setSecret() behavior in github-script.
  • Packaging

    • Include the helper in setup, MCP scripts, and safe-outputs runtime bundles.
const token = readSecretEnv("GITHUB_TOKEN");

Copilot AI and others added 2 commits August 6, 2026 01:20
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Mask JavaScript runtime secrets read from environment Mask secrets read by the JavaScript runtime Aug 6, 2026
Copilot AI requested a review from pelikhan August 6, 2026 01:40
@pelikhan
pelikhan marked this pull request as ready for review August 6, 2026 01:40
Copilot AI balanced review requested due to automatic review settings August 6, 2026 01:40
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

No ADR enforcement needed: PR #50737 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens JavaScript runtimes against credential leakage by registering environment-based secrets with GitHub Actions masking.

Changes:

  • Adds readSecretEnv() and standalone core.setSecret() support.
  • Routes credential reads through the masking helper.
  • Packages and tests the helper across runtime bundles.
Show a summary per file
File Description
actions/setup/setup.sh Bundles the secret helper.
actions/setup/index.js Masks the OTLP input token.
actions/setup/js/action_setup_otlp.cjs Masks OTLP credentials.
actions/setup/js/apply_samples.cjs Masks repository tokens.
actions/setup/js/artifact_client.cjs Masks artifact runtime tokens.
actions/setup/js/assign_to_agent.cjs Masks assignment tokens.
actions/setup/js/build_checkout_manifest.cjs Masks checkout tokens.
actions/setup/js/check_daily_aic_workflow_guardrail.cjs Masks guardrail tokens.
actions/setup/js/check_workflow_recompile_needed.cjs Masks maintenance tokens.
actions/setup/js/codex_harness.cjs Masks API keys.
actions/setup/js/copilot_sdk_driver.cjs Masks SDK credentials.
actions/setup/js/create_agent_session.cjs Masks session tokens.
actions/setup/js/create_issue.cjs Masks agent-assignment tokens.
actions/setup/js/create_pull_request.cjs Masks PR and checkout tokens.
actions/setup/js/extra_empty_commit.cjs Masks CI trigger tokens.
actions/setup/js/git_helpers.cjs Masks fallback Git credentials.
actions/setup/js/mount_mcp_as_cli.cjs Masks gateway API keys.
actions/setup/js/pi_agent_core_driver.cjs Masks provider credentials.
actions/setup/js/pi_provider.cjs Masks configured provider tokens.
actions/setup/js/push_experiment_state.cjs Masks push credentials.
actions/setup/js/push_repo_memory.cjs Masks repository push tokens.
actions/setup/js/push_signed_commits.test.cjs Updates the core test mock.
actions/setup/js/read_secret_env.cjs Adds the masking helper.
actions/setup/js/read_secret_env.test.cjs Tests helper behavior.
actions/setup/js/redact_evals_results.cjs Masks evaluation secrets.
actions/setup/js/redact_secrets.cjs Masks redaction inputs.
actions/setup/js/run_operation_update_upgrade.cjs Masks update push tokens.
actions/setup/js/safe_output_handler_manager.cjs Masks project credentials.
actions/setup/js/safe_outputs_config.cjs Masks secret placeholders.
actions/setup/js/shim.cjs Adds standalone masking commands.
actions/setup/js/shim.test.cjs Tests command escaping.
actions/setup/js/start_mcp_gateway.cjs Masks gateway credentials.
actions/setup/js/test-live-github-api.cjs Masks live-test tokens.
actions/setup/js/update_project.cjs Masks project token checks.
actions/setup/js/validate_lockdown_requirements.cjs Masks validation token reads.
actions/setup/js/validate_secrets.cjs Masks validated credentials.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 36/36 changed files
  • Comments generated: 2
  • Review effort level: Balanced

}
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];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: ${WRITE_PROJECT_PAT} now matches the shared secret-env-name heuristic (PAT as an env segment), so safe-output placeholders with PAT values route through readSecretEnv() and are covered by a masking assertion.

Comment thread actions/setup/js/shim.cjs

/** @param {string} secret */
const setSecret = secret => {
process.stderr.write(`::add-mask::${escapeCommandData(secret)}\n`);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: start_mcp_gateway.cjs now registers likely secret environment values for masking in the parent Actions step before launching the detached gateway; the shim fallback remains for standalone child processes.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

✅ Test Quality Sentinel Report

PR: #50737 — "Mask secrets read by the JavaScript runtime"
Score: 90/100 ⚠️ Acceptable
Status: ✅ APPROVED

Summary

This PR adds comprehensive tests for secret masking functionality in JavaScript actions. The test suite covers happy-path and edge-case scenarios with strong integration test coverage using subprocess spawning.

Coverage Analysis

New Test Functions: 6
Design Tests: 6/6 (100%) — all tests verify user-visible behavior and design invariants
Edge Case Coverage: 6/6 (100%) — tests cover missing, empty, and special-character secrets
Implementation Tests: 0/6 (0%) ✅
Assertion Density: Strong — all tests verify observable behavior (return values, mock calls, stdout/stderr)

Test Files & Metrics

File Lines Added Lines (Source) Ratio Status
read_secret_env.test.cjs 59 21 2.8:1 ⚠️ Inflation
shim.test.cjs 29 26 1.1:1 ✅ Good
push_signed_commits.test.cjs +1 Infrastructure
Test Classification
Test Name Contract Value Type Notes
readSecretEnv: returns and masks Secret masking on read high design Mocks core.setSecret, verifies call
readSecretEnv: missing secret Skip masking if undefined high design Edge case: missing env var
readSecretEnv: empty secret Skip masking if empty high design Edge case: empty string
readSecretEnv: standalone subprocess Runtime compatibility high design Subprocess integration; verifies stderr
core shim: escape add-mask URL-encode special chars high design Tests %25, %0A, %0D escaping
core shim: partial core object Initialize if missing high design Merges into existing core object

Quality Signals

Strengths:

  • Integration tests with subprocess spawning validate real runtime behavior
  • Comprehensive edge-case coverage (missing, empty, special characters)
  • Strong output validation (return values, mock calls, stderr inspection)
  • Proper setup/teardown with beforeEach/afterEach
  • No mock libraries (gomock/testify/mock) — only vitest mocks for I/O

⚠️ Observation:

  • read_secret_env.test.cjs has test-to-source ratio of 2.8:1 (above 2:1 threshold); justified by integration-heavy approach with subprocess spawning and output inspection

Guidelines Check

✅ No Go tests (repo uses JavaScript for this feature)
✅ No missing build tags
✅ No forbidden mock libraries
✅ All assertions have observable value


Next Steps: No action required. Tests are ready for merge.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 20.1 AIC · ⊞ 7.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on a correctness issue and two quality gaps.

📋 Key Themes & Highlights

Key Themes

  • Implicit global reference in readSecretEnv (correctness): after calling ensureCoreSetSecret(), the code calls the bare core.setSecret(value) which relies on global.core being accessible — if it is cleared between the two lines the function throws. Use the returned object instead.
  • Whitespace-only secret edge case (test gap): a value like " " is truthy and gets masked, but callers downstream skip it after trimming; the asymmetry should be covered by a test.
  • Shim patching duplication (maintainability): the full shim construction block and the partial-else if patch block diverge; future method additions require touching both.

Positive Highlights

  • ✅ Excellent breadth of coverage — all known token/key reads across 36 files consistently migrated.
  • ✅ Standalone subprocess tests in shim.test.cjs and read_secret_env.test.cjs are exactly the right technique for verifying ::add-mask:: output.
  • escapeCommandData correctly mirrors @actions/core's percent/CR/LF encoding.
  • ✅ Graceful no-op when the secret is empty or undefined.
> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31063425327)* · sonnet46 · 41.4 AIC · ⊞ 7.1K > Comment /matt to run again

Comment thread actions/setup/js/read_secret_env.cjs Outdated
const value = process.env[name];
if (value) {
const { ensureCoreSetSecret } = require("./shim.cjs");
ensureCoreSetSecret();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] readSecretEnv calls ensureCoreSetSecret() (which sets global.core.setSecret) and then immediately calls the bare core.setSecret(value) on line 16 — relying on the implicit global. If global.core is ever cleared between those two lines (e.g. in test isolation), this will throw ReferenceError: core is not defined.

💡 Suggested fix

Use the returned core object directly:

const c = ensureCoreSetSecret();
c.setSecret(value);

This removes the implicit global dependency and makes the masking path safe under any global mutation.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: readSecretEnv() now captures the object returned by ensureCoreSetSecret() and calls coreShim.setSecret(value) instead of relying on the implicit global.

const __dirname = dirname(fileURLToPath(import.meta.url));
const originalCore = global.core;
const setSecret = vi.fn();
global.core = { setSecret };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] global.core is replaced at module load time (line 11), before readSecretEnv is required. This means tests that rely on shim fallback behaviour cannot be exercised within the same module — they need a subprocess (spawnSync) as the standalone test at the bottom does. The comment at line 6-12 should document this constraint so future maintainers understand why the mock must be set before the require call.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: added a test comment documenting that the core mock must be installed before requiring read_secret_env.cjs, with shim fallback coverage kept in the subprocess test.

expect(readSecretEnv("TEST_SECRET")).toBeUndefined();
expect(setSecret).not.toHaveBeenCalled();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Missing edge case: a secret value that is whitespace-only (e.g. " ") will pass the if (value) truthy check in readSecretEnv and get masked, but callers like redact_evals_results.cjs later trim-and-skip it with value.trim() !== "". Consider adding a test for the whitespace-only case so the masking-vs-usage asymmetry is explicit and intentional.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: added a whitespace-only secret test that asserts the value is returned and registered for masking.

Comment thread actions/setup/js/shim.cjs
@@ -40,7 +61,10 @@ if (!global.core) {
setOutput: /** @param {string} name @param {unknown} value */ (name, value) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The existing if (!global.core) block now constructs a full shim object including setSecret, while the new else if branch only patches in setSecret. This means the two code paths have different shapes — a future reader adding a new core method must remember to add it in both the initial construction block and any patching branches. Consider extracting a makeShimCore() factory so both paths share a single source of truth.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: refactored shim core construction through makeShimCore() and applyMissingShimCoreMethods() so full and partial-core paths share one source of truth.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 90/100. All 6 new test functions are design tests (100%). 0 implementation tests. Integration tests with subprocess spawning validate real runtime behavior. No violations detected.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Review: Mask secrets read by the JavaScript runtime

Good, well-scoped PR. The approach — centralizing secret reads through readSecretEnv() and wiring ::add-mask:: via the shim — is clean and the coverage across 36 files appears thorough. Tests for both shim.cjs and read_secret_env.cjs are a welcome addition.

One non-blocking suggestion (see inline): read_secret_env.cjs references bare core inside readSecretEnv() relying on global.core being implicitly in scope. The code works because CJS resolves bare identifiers against global, but ensureCoreSetSecret() already returns global.core — capturing that return value would make the dependency explicit and easier to reason about.> 🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 91.1 AIC · ⊞ 5.3K

*/
function readSecretEnv(name) {
const value = process.env[name];
if (value) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core identifier is referenced here without an explicit declaration. ensureCoreSetSecret() sets global.core and Node.js resolves bare identifiers against global in CJS — so this works — but it is implicit and fragile.

Consider capturing the return value of ensureCoreSetSecret() which already returns global.core:

if (value) {
  const { ensureCoreSetSecret } = require('./shim.cjs');
  const coreShim = ensureCoreSetSecret();
  coreShim.setSecret(value);
}

This makes the contract explicit and avoids silent reliance on global resolution. @copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 6449784f07: readSecretEnv() now uses the returned core shim directly before calling setSecret().

@pelikhan

pelikhan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan pelikhan closed this Aug 6, 2026
Copilot stopped work on behalf of pelikhan due to an error August 6, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants