Skip to content

Add GH_AW_RUNTIME_FEATURES support to built-in workflow jobs#40824

Merged
pelikhan merged 5 commits into
mainfrom
copilot/add-runtime-feature-flag-support
Jun 22, 2026
Merged

Add GH_AW_RUNTIME_FEATURES support to built-in workflow jobs#40824
pelikhan merged 5 commits into
mainfrom
copilot/add-runtime-feature-flag-support

Conversation

Copilot AI commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

This adds runtime feature flag propagation via GH_AW_RUNTIME_FEATURES using GitHub Actions vars, with support for newline-delimited flags and key=value entries. It also makes the activation job surface the effective value in the step summary for quick runtime inspection.

  • Built-in job env wiring

    • Inject GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} into built-in job-level env blocks.
    • Apply this consistently across framework-managed jobs such as activation, agent, detection, safe_outputs, conclusion, and related built-in follow-on jobs.
  • Activation visibility

    • Add an activation step that writes the raw runtime feature value to $GITHUB_STEP_SUMMARY.
    • Render unset state explicitly to make absence distinguishable from an empty value.
  • Runtime parsing helpers

    • Add a shared JS parser for newline-separated runtime features.
    • Support both flag-only entries and keyed values:
      • key
      • key2=value
    • Expose parsed helpers through setup_globals so runtime scripts can query feature presence and values without duplicating parsing logic.
  • Golden output updates

    • Refresh workflow golden fixtures to reflect the new built-in job-level env emission.

Example shape of the propagated variable:

env:
  GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}

Example runtime value:

key
key2=value

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add GH_AW_RUNTIME_FEATURES support to built-in jobs Add GH_AW_RUNTIME_FEATURES support to built-in workflow jobs Jun 22, 2026
Copilot AI requested a review from pelikhan June 22, 2026 17:28
@pelikhan pelikhan marked this pull request as ready for review June 22, 2026 17:29
Copilot AI review requested due to automatic review settings June 22, 2026 17:29
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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

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

This PR adds propagation of a runtime feature flag bundle via GH_AW_RUNTIME_FEATURES into framework-managed (built-in) jobs, plus shared JS helpers for parsing/querying the runtime feature set and a step-summary log in the activation job. It updates the workflow golden fixtures to reflect the new generated YAML.

Changes:

  • Inject GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} into job-level env for built-in jobs during render.
  • Add an activation job step that logs the runtime features value into $GITHUB_STEP_SUMMARY.
  • Add JS parsing helpers (parseRuntimeFeatures, hasRuntimeFeature, getRuntimeFeatureValue) and expose them via setupGlobals, with Vitest coverage.
Show a summary per file
File Description
pkg/workflow/jobs.go Injects GH_AW_RUNTIME_FEATURES into rendered env for a defined set of built-in jobs.
pkg/workflow/runtime_features_env_test.go Adds Go unit tests for the built-in job env injection and activation step presence.
pkg/workflow/compiler_activation_job.go Wires the runtime-features summary step into activation job construction.
pkg/workflow/compiler_activation_job_builder.go Implements the activation “Log runtime features” step YAML.
actions/setup/js/runtime_features.cjs Adds runtime feature parsing and lookup helpers for newline-delimited inputs.
actions/setup/js/runtime_features.test.cjs Adds Vitest coverage for the runtime feature parser/helpers.
actions/setup/js/setup_globals.cjs Exposes parsed runtime features + helper accessors on global.* for runtime scripts.
pkg/workflow/testdata/TestWasmGolden_CompileFixtures/with-imports.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_CompileFixtures/playwright-cli-mode.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_CompileFixtures/basic-copilot.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_AllEngines/pi.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_AllEngines/gemini.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_AllEngines/copilot.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_AllEngines/codex.golden Golden update to reflect built-in env injection + activation summary step.
pkg/workflow/testdata/TestWasmGolden_AllEngines/claude.golden Golden update to reflect built-in env injection + activation summary step.

Copilot's findings

Tip

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

  • Files reviewed: 16/16 changed files
  • Comments generated: 2

Comment on lines +409 to +425
func buildRuntimeFeaturesSummaryStep() []string {
return []string{
" - name: Log runtime features\n",
" run: |\n",
" {\n",
" echo \"## Runtime features\"\n",
" echo\n",
" if [[ -n \"${GH_AW_RUNTIME_FEATURES:-}\" ]]; then\n",
" echo '```text'\n",
" printf '%s\\n' \"$GH_AW_RUNTIME_FEATURES\"\n",
" echo '```'\n",
" else\n",
" echo \"_Not set_\"\n",
" fi\n",
" } >> \"$GITHUB_STEP_SUMMARY\"\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.

Fixed in 470f5bd. The activation summary step now uses a separate GH_AW_RUNTIME_FEATURES_IS_SET flag so it can render _Not set_ for an absent repo variable and _Empty string_ when the variable exists but is empty.

Comment on lines +3 to +6
import { describe, expect, it } from "vitest";

const { parseRuntimeFeatures, hasRuntimeFeature, getRuntimeFeatureValue } = require("./runtime_features.cjs");

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.

Fixed in 470f5bd. runtime_features.test.cjs now uses createRequire(import.meta.url) and keeps the .cjs import path aligned with the repo’s Vitest ESM pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (>100 new lines in pkg/ and actions/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/40824-propagate-runtime-features-via-actions-variable.md — review and complete it before merging.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff.
  2. Complete the missing sections — refine the decision rationale, confirm the allowlist trade-offs, and verify the alternatives reflect what you actually considered.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-40824: Propagate Runtime Feature Flags via a Single GH_AW_RUNTIME_FEATURES Actions Variable

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 40824-propagate-runtime-features-via-actions-variable.md for PR #40824).

🔒 This PR cannot merge until an ADR is linked in the PR body.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 98.2 AIC · ⌖ 10.6 AIC · ⊞ 11.7K ·

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 91/100 — Excellent

Analyzed 7 test(s) across 2 new files: 7 design tests, 0 implementation tests, 0 guideline violations.

📊 Metrics & Test Classification (7 tests analyzed)
Metric Value
New/modified tests analyzed 7
✅ Design tests (behavioral contracts) 7 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 5 (71%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
TestBuildRenderedJobEnv_AddsRuntimeFeaturesForBuiltInJobs pkg/workflow/runtime_features_env_test.go:13 ✅ Design
TestBuildRenderedJobEnv_DoesNotAddRuntimeFeaturesForCustomJobs pkg/workflow/runtime_features_env_test.go:22 ✅ Design
TestBuildRenderedJobEnv_PreservesExistingRuntimeFeaturesOverride pkg/workflow/runtime_features_env_test.go:31 ✅ Design
TestActivationJobIncludesRuntimeFeatureSummaryStep pkg/workflow/runtime_features_env_test.go:50 ✅ Design
parses newline-delimited flags and key value pairs actions/setup/js/runtime_features.test.cjs:8 ✅ Design
ignores blank lines and malformed empty keys actions/setup/js/runtime_features.test.cjs:18 ✅ Design
supports feature lookup helpers actions/setup/js/runtime_features.test.cjs:25 ✅ Design

Go: 4 (*_test.go); JavaScript: 3 (*.test.cjs). No other languages detected.

Score breakdown: Behavioral Coverage 40/40 + Edge/Error Coverage 21/30 + Low Duplication 20/20 + Proportional Growth 10/10 = 91/100

Inflation ratios: JS 35/66 = 0.53 ✅ · Go 71/66 = 1.08 ✅

💡 Minor Coverage Gaps (non-blocking)

These do not affect the score or verdict but are worth noting:

runtime_features.test.cjs:

  • No test for null/undefined input to parseRuntimeFeatures. An unset GitHub Actions env var may arrive as undefined, which could throw.
  • No \r\n (Windows CRLF) line-ending test.
  • hasRuntimeFeature behaviour for an empty-string value key (e.g. valid=) is ambiguous and untested — truthy or falsy?
  • getRuntimeFeatureValue for a key with "" value is unspecified (distinct from undefined).

runtime_features_env_test.go:

  • All four tests are flat single-case functions. A table-driven approach over runtimeFeaturesBuiltInJobNames would provide automatic coverage if new built-in job names are added in future.

Verdict

Check passed. 0% implementation tests (threshold: 30%). All 7 tests verify observable behavioral contracts. Build tag //go:build !integration is present, no mock libraries used, and all assertion calls include descriptive messages.

🧪 Test quality analysis by Test Quality Sentinel · 127.2 AIC · ⌖ 14.1 AIC · ⊞ 8.3K ·

@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: 91/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

@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.

Skills-Based Review 🧠

Applied /tdd, /grill-with-docs, and /zoom-out — commenting with test coverage gaps and a naming-consistency observation. No blocking issues found.

📋 Key Themes & Highlights

Key Themes

  • Allowlist maintenance: Three job names in runtimeFeaturesBuiltInJobNames use raw string literals rather than constants.*JobName values, creating a silent-drift risk on future rename.
  • Test coverage gaps: The Uses-job exclusion guard, null/undefined parser input, and multi-= value parsing are all implemented correctly but lack explicit tests.
  • Global re-parse on repeat calls: setupGlobals() overwrites global.runtimeFeatures on every invocation — harmless in production but worth documenting for test scaffolding.

Positive Highlights

  • buildRenderedJobEnv uses maps.Clone() — no aliasing bugs possible on the returned env map.
  • ✅ The !exists guard in buildRenderedJobEnv correctly preserves caller-supplied GH_AW_RUNTIME_FEATURES overrides, and that behaviour is tested.
  • ✅ Parser handles CRLF (\r?\n), whitespace-trimmed keys and values, and malformed entries — robust for real-world GitHub vars input.
  • ✅ Nine golden fixture files updated identically — a strong regression guard across all engine combinations.
  • ✅ Go and JS tests together cover the primary happy paths and key edge cases well.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 88.5 AIC · ⌖ 10.4 AIC · ⊞ 6.5K

Comment thread pkg/workflow/jobs.go Outdated
string(constants.UnlockJobName): {},
"push_experiments_state": {},
"push_repo_memory": {},
"update_cache_memory": {},

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.

[/grill-with-docs] Three entries in runtimeFeaturesBuiltInJobNames use raw string literals instead of constants.*JobName values, so a job rename will silently stop injecting GH_AW_RUNTIME_FEATURES for that job with no compile-time signal.

💡 Suggestion

"push_experiments_state", "push_repo_memory", and "update_cache_memory" are the only allowlist members not backed by a typed constant. Consider either adding constants.PushExperimentsStateJobName etc. (consistent with the nine entries above them), or adding a comment noting why they lack constants so a future refactor knows to revisit the allowlist.

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.

Fixed in 470f5bd. I introduced shared job-name constants and now use them in both the runtime-features allowlist and the corresponding job builders.

if len(env) != 0 {
t.Fatalf("expected no env vars for custom job, got %v", env)
}
}

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] The shouldInjectRuntimeFeaturesEnv guard that skips uses:-based reusable workflow jobs has no test — it is the only branch of that function not covered here.

💡 Suggested test
func TestBuildRenderedJobEnv_DoesNotAddRuntimeFeaturesForUsesJobs(t *testing.T) {
    job := &Job{
        Name: string(constants.AgentJobName),
        Uses: "some/repo/.github/workflows/action.yml@main",
    }
    env := buildRenderedJobEnv(job)
    if _, ok := env[runtimeFeaturesEnvVarName]; ok {
        t.Fatal("expected no runtime features env for uses: job")
    }
}

This pins the job.Uses != "" check against accidental removal.

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.

Fixed in 470f5bd. Added a focused test covering the Uses != "" reusable-workflow skip path in buildRenderedJobEnv.

expect(getRuntimeFeatureValue(features, "mode")).toBe("fast");
expect(getRuntimeFeatureValue(features, "missing")).toBeUndefined();
});
});

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] The JSDoc declares @param {string | undefined | null} but there are no tests for null or undefined input — the if (!raw) return {} branch is unreachable from the three existing tests.

💡 Suggested tests
it("returns empty map for null", () => {
  expect(parseRuntimeFeatures(null)).toEqual({});
});

it("returns empty map for undefined", () => {
  expect(parseRuntimeFeatures(undefined)).toEqual({});
});

These lock in the contract described by the annotation and prevent a future refactor from accidentally dropping the guard.

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.

Fixed in 470f5bd. Added nullish-input coverage so parseRuntimeFeatures(null) and parseRuntimeFeatures(undefined) are both pinned to {}.


describe("runtime_features", () => {
it("parses newline-delimited flags and key value pairs", () => {
const features = parseRuntimeFeatures("key\nkey2=value\nkey3 = spaced 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.

[/tdd] indexOf("=") correctly splits on the first =, so "key=a=b=c" yields { key: "a=b=c" } — but this is not tested. Feature values containing = (base64 payloads, URLs, etc.) are a realistic input.

💡 Suggested test
it("treats only the first = as the key/value separator", () => {
  expect(parseRuntimeFeatures("key=a=b=c")).toEqual({ key: "a=b=c" });
});

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.

Fixed in 470f5bd. Added coverage for values containing additional = characters so only the first separator is treated as structural.

Comment thread actions/setup/js/setup_globals.cjs Outdated
*/
function setupGlobals(coreModule, githubModule, contextModule, execModule, ioModule, getOctokitFn) {
global.core = coreModule;
const runtimeFeatures = parseRuntimeFeatures(process.env.GH_AW_RUNTIME_FEATURES);

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.

[/zoom-out] setupGlobals() unconditionally overwrites global.runtimeFeatures on every call — in test scaffolding that calls it multiple times to swap core/github mocks, features are re-derived from the process environment rather than from any test-controlled value.

💡 Options

In production this is called once, so there is no live bug. But for test isolation you could either:

  1. Parse only on first call:
if (!global.runtimeFeatures) {
  const rf = parseRuntimeFeatures(process.env.GH_AW_RUNTIME_FEATURES);
  global.runtimeFeatures = rf;
  global.hasRuntimeFeature = key => hasRuntimeFeature(rf, key);
  global.getRuntimeFeatureValue = key => getRuntimeFeatureValue(rf, key);
}
  1. Or add a JSDoc/comment noting that setupGlobals is call-once so test helpers know to reset process.env.GH_AW_RUNTIME_FEATURES before each call.

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.

No code change for the re-parse behavior in 470f5bd. I kept setupGlobals() re-evaluating process.env.GH_AW_RUNTIME_FEATURES on each call so repeated test setup can intentionally pick up the current environment, and paired that with an immutable Object.freeze(...) runtime-features map.

@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.

Overall: Non-blocking, 3 findings worth addressing

The core wiring is sound — buildRenderedJobEnv correctly clones before mutating, the inject-only-once guard prevents clobbering explicit overrides, and the activation-job summary step is a clean observability addition. Three concerns worth addressing before this gets heavy usage:

📋 Finding summary
  1. pkg/workflow/jobs.go lines 29–31 — Three job names (push_experiments_state, push_repo_memory, update_cache_memory) are raw string literals with no compile-time link to their builder Name: assignments. A silent rename in any builder file would drop the env injection for that job with no failing test or compiler error.

  2. actions/setup/js/runtime_features.cjs line 38 — A key= entry (empty value) produces "" (falsy), but hasRuntimeFeature still returns true. Callers who check if (getRuntimeFeatureValue(...)) for presence will get a silent false negative. Consider normalising empty values to true, or documenting the contract explicitly in JSDoc.

  3. pkg/workflow/runtime_features_env_test.go — The Uses != "" guard in shouldInjectRuntimeFeaturesEnv has no test. A regression there would silently inject env into reusable workflow calls.

Note: grumpy-coder sub-agent timed out; this review is based on direct analysis.

🔎 Code quality review by PR Code Quality Reviewer · 167 AIC · ⌖ 7.79 AIC · ⊞ 5.1K

Comment thread pkg/workflow/jobs.go Outdated
string(constants.UnlockJobName): {},
"push_experiments_state": {},
"push_repo_memory": {},
"update_cache_memory": {},

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.

Three job names are raw string literals with no compile-time safety: if any of these names change in their builder files, the runtime-features injection silently breaks.

💡 Details and suggested fix

"push_experiments_state", "push_repo_memory", and "update_cache_memory" are hardcoded here as raw string literals, while the same names are independently set as raw strings in their builder files:

  • compiler_experiments.go:664Name: "push_experiments_state"
  • repo_memory.go:817Name: "push_repo_memory"
  • cache.go:1067Name: "update_cache_memory"

There is no compile-time link between this allowlist entry and the job's actual Name field. A silent rename in any builder file would leave a stale entry here and silently drop the env injection for that job — no error, no warning, no test failure (since the golden fixtures don't cover those optional jobs).

Suggested fix: introduce package-level constants mirroring the constants.XxxJobName pattern:

// In an appropriate constants or jobs file:
const (
    pushExperimentsStateJobName = "push_experiments_state"
    pushRepoMemoryJobName       = "push_repo_memory"
    updateCacheMemoryJobName    = "update_cache_memory"
)

Then use those constants in both the allowlist here and the Name: assignments in the builder files.

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.

Fixed in 470f5bd by introducing shared constants for those three built-in job names and using them from both the allowlist and the builder Name: assignments.

if (!key) {
continue;
}
features[key] = trimmed.slice(equalsIndex + 1).trim();

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.

key= entries produce a falsy empty string "", but hasRuntimeFeature still returns true for them — the two helpers give contradictory truthiness signals for the same feature.

💡 Impact and suggested fix

A line like myfeature= parses to features["myfeature"] = "". This means:

hasRuntimeFeature(features, "myfeature")   // → true  (feature IS present)
getRuntimeFeatureValue(features, "myfeature") // → ""  (falsy!)

Any consumer script that guards on truthiness of the value:

if (getRuntimeFeatureValue(runtimeFeatures, "myfeature")) {
  // never reached for "myfeature="  — silent false negative
}

will silently skip the feature even though it was explicitly set. This is especially easy to hit since hasRuntimeFeature is the advertised "is it set?" API, but callers may reasonably collapse both checks into one.

Option A — Normalize empty values to true (treat key= same as key):

const raw_value = trimmed.slice(equalsIndex + 1).trim();
features[key] = raw_value === "" ? true : raw_value;

Option B — Keep the distinction but document it explicitly in the JSDoc and the hasRuntimeFeature JSDoc so consumers know to use hasRuntimeFeature for presence checks and never rely on truthiness of getRuntimeFeatureValue.

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.

Addressed in 470f5bd by documenting and testing the empty-string contract instead of changing parsing semantics. key= still preserves "", and callers should use hasRuntimeFeature(...) for presence checks when they need to distinguish empty from missing.

job := &Job{
Name: string(constants.AgentJobName),
Env: map[string]string{
runtimeFeaturesEnvVarName: `"explicit"`,

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.

No test covers the reusable-workflow skip path in shouldInjectRuntimeFeaturesEnv: a job with Uses != "" must not receive the env injection, but this is currently untested.

💡 Suggested test
func TestBuildRenderedJobEnv_SkipsReusableWorkflowJobs(t *testing.T) {
    // Even a built-in job name should be skipped when Uses is set
    job := &Job{
        Name: string(constants.AgentJobName),
        Uses: "./.github/workflows/reusable.yml",
    }

    env := buildRenderedJobEnv(job)

    if _, ok := env[runtimeFeaturesEnvVarName]; ok {
        t.Fatalf("expected reusable workflow job to not receive runtime features env, got %v", env)
    }
}

Without this test, the guard job.Uses != "" in shouldInjectRuntimeFeaturesEnv could be removed or broken with no immediate test failure.

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.

Fixed in 470f5bd. Added a reusable-workflow skip test so a built-in job name with Uses set does not receive the runtime-features env injection.

function setupGlobals(coreModule, githubModule, contextModule, execModule, ioModule, getOctokitFn) {
global.core = coreModule;
const runtimeFeatures = parseRuntimeFeatures(process.env.GH_AW_RUNTIME_FEATURES);
global.runtimeFeatures = runtimeFeatures;

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.

global.runtimeFeatures is a mutable object: any action script in the same Node.js process can modify it and corrupt feature state for all subsequent callers.

💡 Suggested fix

All runtime scripts run in the same long-lived Node.js process. Because global.runtimeFeatures is a plain object, any script can do:

global.runtimeFeatures['someFlag'] = true; // silently corrupts feature state

This would affect every subsequent call to hasRuntimeFeature / getRuntimeFeatureValue in that process, including calls made by unrelated scripts.

Freeze the object after assignment:

global.runtimeFeatures = Object.freeze(parseRuntimeFeatures(process.env.GH_AW_RUNTIME_FEATURES));

Note: Object.freeze is shallow, but since all values here are primitives (boolean | string), it fully protects the feature map.

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.

Fixed in 470f5bd. setupGlobals() now freezes the parsed runtime-features object before exposing it on global.runtimeFeatures.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits June 22, 2026 18:03
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Done in 470f5bd. I applied the review fixes, refreshed the generated lockfiles and wasm goldens, ran make agent-report-progress and make test, and checked the earlier CJS failure root cause locally by adding runtime_features.cjs to the setup file lists.

@pelikhan pelikhan merged commit 0033f98 into main Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants