From 3fffeb92702603deda1f6c7731d8719d41066ff0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:20:32 +0000 Subject: [PATCH 1/5] plan: engine.version support for copilot/claude/codex/gemini Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot-auto.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke-copilot-auto.lock.yml b/.github/workflows/smoke-copilot-auto.lock.yml index 80f7930c2f1..5c316f6e8b8 100644 --- a/.github/workflows/smoke-copilot-auto.lock.yml +++ b/.github/workflows/smoke-copilot-auto.lock.yml @@ -149,7 +149,7 @@ jobs: GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🌸" GH_AW_COMPILED_STRICT: "true" - GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"8.5e-07","output":"1.55e-06"}}}}}}' + GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"0","output":"0"}}}}}}' GH_AW_INFO_FEATURES: '{"gh-aw-detection":false}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: From 42c5d3e80828d71ea7a9b33a28238ce27b5c6636 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:27:21 +0000 Subject: [PATCH 2/5] feat: honor engine.version for copilot; add version tests for gemini Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot-auto.lock.yml | 1 - pkg/workflow/copilot_engine_installation.go | 23 ++-- pkg/workflow/copilot_installer_test.go | 100 ++++++++++++------ pkg/workflow/gemini_engine_test.go | 63 +++++++++++ 4 files changed, 144 insertions(+), 43 deletions(-) diff --git a/.github/workflows/smoke-copilot-auto.lock.yml b/.github/workflows/smoke-copilot-auto.lock.yml index 5c316f6e8b8..9cac699d23d 100644 --- a/.github/workflows/smoke-copilot-auto.lock.yml +++ b/.github/workflows/smoke-copilot-auto.lock.yml @@ -149,7 +149,6 @@ jobs: GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🌸" GH_AW_COMPILED_STRICT: "true" - GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"0","output":"0"}}}}}}' GH_AW_INFO_FEATURES: '{"gh-aw-detection":false}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: diff --git a/pkg/workflow/copilot_engine_installation.go b/pkg/workflow/copilot_engine_installation.go index aa8a9eb3077..ca4135865b2 100644 --- a/pkg/workflow/copilot_engine_installation.go +++ b/pkg/workflow/copilot_engine_installation.go @@ -142,21 +142,22 @@ func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHu return appendCopilotLSPInstallSteps(steps, workflowData) } - // Copilot CLI is pinned to the default version constant. + // Use engine.version if provided, otherwise fall back to the default pinned version. + // When no explicit version is set, normalize the engine config so downstream + // consumers observe the effective installed value. copilotVersion := string(constants.DefaultCopilotVersion) if workflowData.EngineConfig != nil { if workflowData.EngineConfig.Version != "" { - copilotInstallLog.Printf("Ignoring pinned engine.version (%s): Copilot CLI install version is pinned to %s", workflowData.EngineConfig.Version, copilotVersion) + copilotVersion = workflowData.EngineConfig.Version + copilotInstallLog.Printf("Using engine.version for Copilot CLI installation: %s", copilotVersion) + } else { + // Normalize engine config version to the effective installed version so + // downstream checks that consult EngineConfig.Version stay consistent. + // This mutates workflowData by design because subsequent generation steps + // in the same compile flow should observe the effective installed version. + workflowData.EngineConfig.Version = copilotVersion + copilotInstallLog.Printf("No engine.version specified, using default Copilot CLI version: %s", copilotVersion) } - // Normalize engine config version to effective installed version so - // downstream checks that consult EngineConfig.Version stay consistent. - // This applies even when the original version was empty (unset), so all - // downstream consumers observe the effective installed value. - // This mutates workflowData by design because subsequent generation steps - // in the same compile flow should observe the effective installed version. - // Callers that reuse the same WorkflowData instance should expect this - // field to be rewritten after installation-step generation. - workflowData.EngineConfig.Version = copilotVersion } // Use the installer script for global installation diff --git a/pkg/workflow/copilot_installer_test.go b/pkg/workflow/copilot_installer_test.go index 72072020b41..4592205324e 100644 --- a/pkg/workflow/copilot_installer_test.go +++ b/pkg/workflow/copilot_installer_test.go @@ -108,8 +108,9 @@ func TestGenerateCopilotInstallerSteps(t *testing.T) { } } -func TestCopilotInstallerCustomVersion(t *testing.T) { - // Test that pinned version from engine config is ignored in favor of default pinned version +func TestCopilotEngineWithVersion(t *testing.T) { + // engine.version must be honored: when an explicit version is set it should be + // passed to the installer and compat.json resolution must be skipped. engine := NewCopilotEngine() customVersion := "1.0.0" @@ -119,14 +120,12 @@ func TestCopilotInstallerCustomVersion(t *testing.T) { Version: customVersion, }, } - if workflowData.EngineConfig.Version != customVersion { - t.Fatalf("Expected initial engine config version %q, got: %q", customVersion, workflowData.EngineConfig.Version) - } steps := engine.GetInstallationSteps(workflowData) - if workflowData.EngineConfig.Version != string(constants.DefaultCopilotVersion) { - t.Fatalf("Expected engine config version to be normalized to default Copilot version %q, got: %q", constants.DefaultCopilotVersion, workflowData.EngineConfig.Version) + // EngineConfig.Version must remain as the user-specified value. + if workflowData.EngineConfig.Version != customVersion { + t.Fatalf("Expected engine config version to remain %q, got: %q", customVersion, workflowData.EngineConfig.Version) } // Find the install step @@ -143,23 +142,58 @@ func TestCopilotInstallerCustomVersion(t *testing.T) { t.Fatal("Could not find install step with install_copilot_cli.sh") } - // Should contain default pinned version - if !strings.Contains(installStep, `install_copilot_cli.sh" `+string(constants.DefaultCopilotVersion)) { - t.Errorf("Expected default Copilot version in install step, got:\n%s", installStep) + // Should pass the user-specified version to the installer (compat.json skipped). + if !strings.Contains(installStep, `install_copilot_cli.sh" `+customVersion) { + t.Errorf("Expected user-specified version %q in install step, got:\n%s", customVersion, installStep) } - if strings.Contains(installStep, `install_copilot_cli.sh" `+customVersion) { - t.Errorf("Expected pinned version to be ignored, got:\n%s", installStep) + if strings.Contains(installStep, `install_copilot_cli.sh" `+string(constants.DefaultCopilotVersion)) { + t.Errorf("Expected user-specified version, not default version, in install step:\n%s", installStep) } // Must pin GH_HOST: github.com to prevent workflow-level GHES overrides from - // leaking into the Copilot CLI install step. Without this pin, a workflow with - // env.GH_HOST set to a GHES host would cause the install/auth path to target - // the wrong host. + // leaking into the Copilot CLI install step. if !strings.Contains(installStep, "GH_HOST: github.com") { t.Errorf("Install step should pin GH_HOST: github.com to prevent GHES workflow-level overrides, got:\n%s", installStep) } } +func TestCopilotEngineWithoutVersion(t *testing.T) { + // When engine.version is not set, the default pinned version must be used and + // EngineConfig.Version must be normalized to the effective installed value. + engine := NewCopilotEngine() + + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{}, + } + + steps := engine.GetInstallationSteps(workflowData) + + // EngineConfig.Version must be normalized to the default version. + if workflowData.EngineConfig.Version != string(constants.DefaultCopilotVersion) { + t.Fatalf("Expected engine config version to be normalized to default Copilot version %q, got: %q", constants.DefaultCopilotVersion, workflowData.EngineConfig.Version) + } + + // Find the install step + var installStep string + for _, step := range steps { + stepContent := strings.Join(step, "\n") + if strings.Contains(stepContent, "install_copilot_cli.sh") { + installStep = stepContent + break + } + } + + if installStep == "" { + t.Fatal("Could not find install step with install_copilot_cli.sh") + } + + // Should use the default pinned version. + if !strings.Contains(installStep, `install_copilot_cli.sh" `+string(constants.DefaultCopilotVersion)) { + t.Errorf("Expected default Copilot version in install step, got:\n%s", installStep) + } +} + func TestGenerateCopilotInstallerSteps_ExpressionVersion(t *testing.T) { tests := []struct { name string @@ -248,8 +282,9 @@ func TestGenerateCopilotInstallerSteps_RootlessWithExpression(t *testing.T) { } } -func TestCopilotInstallerExpressionVersion_ViaEngineConfig(t *testing.T) { - // Test that expression version from engine config is ignored in favor of default pinned version +func TestCopilotEngineWithExpressionVersion(t *testing.T) { + // expression engine.version must be honored: the value must flow through env-var + // injection (not embedded directly in the shell command) and compat.json must be skipped. engine := NewCopilotEngine() expressionVersion := "${{ inputs.engine-version }}" @@ -262,8 +297,9 @@ func TestCopilotInstallerExpressionVersion_ViaEngineConfig(t *testing.T) { steps := engine.GetInstallationSteps(workflowData) - if workflowData.EngineConfig.Version != string(constants.DefaultCopilotVersion) { - t.Fatalf("Expected engine config version to be normalized to default Copilot version %q, got: %q", constants.DefaultCopilotVersion, workflowData.EngineConfig.Version) + // EngineConfig.Version must remain as the expression value. + if workflowData.EngineConfig.Version != expressionVersion { + t.Fatalf("Expected engine config version to remain %q, got: %q", expressionVersion, workflowData.EngineConfig.Version) } // Find the install step @@ -280,18 +316,20 @@ func TestCopilotInstallerExpressionVersion_ViaEngineConfig(t *testing.T) { t.Fatal("Could not find install step with install_copilot_cli.sh") } - // Should not use expression wiring - if strings.Contains(installStep, "ENGINE_VERSION: ${{ inputs.engine-version }}") { - t.Errorf("Expected expression version to be ignored, got:\n%s", installStep) + // Should use env var injection (not embed expression directly in shell command). + if !strings.Contains(installStep, "ENGINE_VERSION: "+expressionVersion) { + t.Errorf("Expected ENGINE_VERSION env var with expression, got:\n%s", installStep) } - - // Should install default pinned version - if !strings.Contains(installStep, `install_copilot_cli.sh" `+string(constants.DefaultCopilotVersion)) { - t.Errorf("Expected default Copilot version in install step, got:\n%s", installStep) + if !strings.Contains(installStep, `"${ENGINE_VERSION}"`) { + t.Errorf(`Expected step to reference "$ENGINE_VERSION" in run command, got:\n%s`, installStep) + } + if strings.Contains(installStep, "install_copilot_cli.sh "+expressionVersion) { + t.Errorf("Expression version should NOT be embedded directly in shell command, got:\n%s", installStep) } } -func TestCopilotInstallerByokFeatureStillUsesDefaultPinnedVersion(t *testing.T) { +func TestCopilotEngineWithVersionAndByokFeature(t *testing.T) { + // engine.version must be honored even when the BYOK feature flag is enabled. engine := NewCopilotEngine() workflowData := &WorkflowData{ Name: "test-workflow", @@ -318,10 +356,10 @@ func TestCopilotInstallerByokFeatureStillUsesDefaultPinnedVersion(t *testing.T) t.Fatal("Could not find install step with install_copilot_cli.sh") } - if !strings.Contains(installStep, `install_copilot_cli.sh" `+string(constants.DefaultCopilotVersion)) { - t.Errorf("Expected default pinned Copilot CLI install, got:\n%s", installStep) + if !strings.Contains(installStep, `install_copilot_cli.sh" 1.0.0`) { + t.Errorf("Expected user-specified version in install step, got:\n%s", installStep) } - if strings.Contains(installStep, `install_copilot_cli.sh" 1.0.0`) { - t.Errorf("Expected pinned version to be ignored, got:\n%s", installStep) + if strings.Contains(installStep, `install_copilot_cli.sh" `+string(constants.DefaultCopilotVersion)) { + t.Errorf("Expected user-specified version, not default version, in install step:\n%s", installStep) } } diff --git a/pkg/workflow/gemini_engine_test.go b/pkg/workflow/gemini_engine_test.go index a476afeb142..9efaa6af4cd 100644 --- a/pkg/workflow/gemini_engine_test.go +++ b/pkg/workflow/gemini_engine_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/github/gh-aw/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -779,3 +780,65 @@ func TestGeminiEngineWithExpressionVersion(t *testing.T) { t.Errorf("Expression should NOT be embedded directly in npm install command, got:\n%s", installStep) } } + +func TestGeminiEngineWithVersion(t *testing.T) { + engine := NewGeminiEngine() + + customVersion := "0.99.0" + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "gemini", + Version: customVersion, + }, + } + + installSteps := engine.GetInstallationSteps(workflowData) + + var installStep string + for _, step := range installSteps { + stepContent := strings.Join([]string(step), "\n") + if strings.Contains(stepContent, "npm install") { + installStep = stepContent + break + } + } + + if installStep == "" { + t.Fatal("Could not find npm install step") + } + + if !strings.Contains(installStep, "@google/gemini-cli@"+customVersion) { + t.Errorf("Expected custom version %q in install step, got:\n%s", customVersion, installStep) + } + if strings.Contains(installStep, "@google/gemini-cli@"+string(constants.DefaultGeminiVersion)) { + t.Errorf("Expected user-specified version, not default, in install step:\n%s", installStep) + } +} + +func TestGeminiEngineWithoutVersion(t *testing.T) { + engine := NewGeminiEngine() + + workflowData := &WorkflowData{ + Name: "test-workflow", + } + + installSteps := engine.GetInstallationSteps(workflowData) + + var installStep string + for _, step := range installSteps { + stepContent := strings.Join([]string(step), "\n") + if strings.Contains(stepContent, "npm install") { + installStep = stepContent + break + } + } + + if installStep == "" { + t.Fatal("Could not find npm install step") + } + + if !strings.Contains(installStep, "@google/gemini-cli@"+string(constants.DefaultGeminiVersion)) { + t.Errorf("Expected default version %q in install step when no engine.version set, got:\n%s", constants.DefaultGeminiVersion, installStep) + } +} From 8b2a5fd20bd97d1f7f2b4d5c2b0a94743d7f3f7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 06:39:53 +0000 Subject: [PATCH 3/5] docs(adr): draft ADR-48519 for honoring engine.version in Copilot engine --- .../48519-honor-engine-version-for-copilot.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/adr/48519-honor-engine-version-for-copilot.md diff --git a/docs/adr/48519-honor-engine-version-for-copilot.md b/docs/adr/48519-honor-engine-version-for-copilot.md new file mode 100644 index 00000000000..9498c030260 --- /dev/null +++ b/docs/adr/48519-honor-engine-version-for-copilot.md @@ -0,0 +1,44 @@ +# ADR-48519: Honor engine.version for Copilot CLI Installation + +**Date**: 2026-07-28 +**Status**: Draft +**Deciders**: pelikhan, copilot-swe-agent + +--- + +### Context + +The `engine.version` field in workflow configuration allows users to pin the version of a coding engine (Copilot, Claude, codex, gemini) installed during workflow execution. Claude, codex, and gemini engines all honored this field correctly, using the user-specified value when present and falling back to a default pinned constant otherwise. However, the Copilot engine's `GetInstallationSteps` silently ignored `engine.version` and always installed `DefaultCopilotVersion`, logging only a message that it was ignoring the user-set value. This inconsistency made `engine.version` a no-op for Copilot, violating the principle of least surprise and breaking user expectations established by the other three engine implementations. + +### Decision + +We will make the Copilot engine honor `engine.version` exactly as Claude, codex, and gemini do: when `EngineConfig.Version` is set, use it as the installation target; when it is absent, fall back to `DefaultCopilotVersion` and normalize `EngineConfig.Version` to that default so all downstream consumers in the same compile flow observe the effective installed value. The install script already skips `compat.json` resolution when an explicit version is provided, so no additional bypass logic is required. + +### Alternatives Considered + +#### Alternative 1: Keep Copilot always pinned to DefaultCopilotVersion + +The existing behavior guaranteed a stable, tested Copilot CLI version regardless of user configuration, which reduced risk of users accidentally installing untested or incompatible versions. This was rejected because it made `engine.version` silently ineffective for Copilot while working for all other engines, creating inconsistent and confusing behavior. The field's documented contract implies it controls the installed version across all engines. + +#### Alternative 2: Introduce a Copilot-specific version override field + +A dedicated field (e.g., `engine.copilot_version`) could allow Copilot-specific version control without changing the behavior of `engine.version`. This was rejected because it adds unnecessary API surface, conflicts with the established cross-engine convention of `engine.version`, and would require documentation and migration for any users who expect the unified field to work. + +### Consequences + +#### Positive +- `engine.version` now behaves consistently across all four supported engines (Copilot, Claude, codex, gemini), fulfilling user expectations. +- Users can pin or override the Copilot CLI version in their workflow definitions, enabling version-locked reproducible builds or controlled rollouts of new CLI versions. +- Test coverage explicitly asserts the honored-version contract, preventing silent regression back to the ignore-version behavior. + +#### Negative +- `compat.json` compatibility-matrix resolution is skipped when an explicit `engine.version` is provided, meaning users who specify an incompatible or unsupported version will receive no automated correction and may encounter runtime failures. +- Any workflow that previously set `engine.version` for Copilot expecting it to be ignored (e.g., to document an intent without changing behavior) will now have that version actually installed — a silent behavioral change at the call site. + +#### Neutral +- `EngineConfig.Version` is still normalized to the effective installed version when no explicit version is set, preserving the existing contract for downstream consumers in the compile flow. +- The change is contained to `copilot_engine_installation.go`; the install script and `compat.json` integration path are unchanged. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From f303c1b8975ace7c40e61a8af96df33fab1304ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:52:56 +0000 Subject: [PATCH 4/5] plan: address review thread issues from pr-finisher run Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot-auto.lock.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-copilot-auto.lock.yml b/.github/workflows/smoke-copilot-auto.lock.yml index 9cac699d23d..5c316f6e8b8 100644 --- a/.github/workflows/smoke-copilot-auto.lock.yml +++ b/.github/workflows/smoke-copilot-auto.lock.yml @@ -149,6 +149,7 @@ jobs: GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🌸" GH_AW_COMPILED_STRICT: "true" + GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"0","output":"0"}}}}}}' GH_AW_INFO_FEATURES: '{"gh-aw-detection":false}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: From 68f2556d4bc6ae247be35f1db5fa9ce8f6b05c3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:13:25 +0000 Subject: [PATCH 5/5] fix: handle expression versions in copilotSupportsNoAskUser; add Gemini version to setup step; update golden and lock files - copilot_engine_execution.go: treat expression versions (e.g. ${{ inputs.engine-version }}) as supporting --no-ask-user, since the resolved version is unknown at compile time and the installer already handles expression versions correctly via ENGINE_VERSION env-var injection - copilot_engine_test.go: add two test cases for expression versions in TestCopilotSupportsNoAskUser - compiler_yaml_lookups.go: add GeminiEngine to getVersionForSetup switch so GH_AW_INFO_VERSION is consistently emitted in all setup steps (not just those after GetInstallationSteps runs) - gemini_engine.go: normalize EngineConfig.Version to DefaultGeminiVersion when absent, matching Copilot behavior for downstream version-aware logic - gemini_engine_test.go: use explicit &EngineConfig{} in TestGeminiEngineWithoutVersion and assert normalization - testdata/gemini.golden: update golden to reflect GH_AW_INFO_VERSION now emitted - smoke-copilot-auto.lock.yml: recompile removes zero-pricing overlay (models.dev unavailable locally; real pricing needs human recompile with network access) - smoke-gemini.lock.yml: recompile adds GH_AW_INFO_VERSION and GH_AW_ENGINE_VERSION now that Gemini version is tracked Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot-auto.lock.yml | 1 - .github/workflows/smoke-gemini.lock.yml | 9 +++++++++ pkg/workflow/compiler_yaml_lookups.go | 2 ++ pkg/workflow/copilot_engine_execution.go | 9 +++++++++ pkg/workflow/copilot_engine_test.go | 10 ++++++++++ pkg/workflow/gemini_engine.go | 7 +++++++ pkg/workflow/gemini_engine_test.go | 8 +++++++- .../testdata/TestWasmGolden_AllEngines/gemini.golden | 3 +++ 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-copilot-auto.lock.yml b/.github/workflows/smoke-copilot-auto.lock.yml index 5c316f6e8b8..9cac699d23d 100644 --- a/.github/workflows/smoke-copilot-auto.lock.yml +++ b/.github/workflows/smoke-copilot-auto.lock.yml @@ -149,7 +149,6 @@ jobs: GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🌸" GH_AW_COMPILED_STRICT: "true" - GH_AW_INFO_MODEL_COSTS: '{"providers":{"github-copilot":{"models":{"auto":{"cost":{"input":"0","output":"0"}}}}}}' GH_AW_INFO_FEATURES: '{"gh-aw-detection":false}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml index 1149084cbd2..4d67f9ba637 100644 --- a/.github/workflows/smoke-gemini.lock.yml +++ b/.github/workflows/smoke-gemini.lock.yml @@ -151,6 +151,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Mask OTLP telemetry headers run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" @@ -569,6 +570,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Set runtime paths id: set-runtime-paths @@ -1312,6 +1314,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Download agent output artifact id: download-agent-output @@ -1589,6 +1592,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Download agent output artifact id: download-agent-output @@ -1838,6 +1842,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Check team membership for command workflow id: check_membership @@ -1891,6 +1896,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -1959,6 +1965,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "gemini" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "0.39.1" GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ✨ *[{workflow_name}]({run_url}) — Powered by Gemini*{ai_credits_suffix}{history_link}\",\"runStarted\":\"✨ Gemini awakens... [{workflow_name}]({run_url}) begins its journey on this {event_type}...\",\"runSuccess\":\"🚀 [{workflow_name}]({run_url}) **MISSION COMPLETE!** Gemini has spoken. ✨\",\"runFailure\":\"⚠️ [{workflow_name}]({run_url}) {status}. Gemini encountered unexpected challenges...\"}" @@ -1998,6 +2005,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Mask OTLP telemetry headers run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" @@ -2086,6 +2094,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Smoke Gemini" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-gemini.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Download cache-memory artifact (default) id: download_cache_default diff --git a/pkg/workflow/compiler_yaml_lookups.go b/pkg/workflow/compiler_yaml_lookups.go index f40bfb0facd..a245afd04b2 100644 --- a/pkg/workflow/compiler_yaml_lookups.go +++ b/pkg/workflow/compiler_yaml_lookups.go @@ -39,6 +39,8 @@ func getVersionForSetup(data *WorkflowData) string { return string(constants.DefaultClaudeCodeVersion) case string(constants.CodexEngine): return string(constants.DefaultCodexVersion) + case string(constants.GeminiEngine): + return string(constants.DefaultGeminiVersion) case string(constants.OpenCodeEngine): return string(constants.DefaultOpenCodeVersion) case string(constants.PiEngine): diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 4aaab348c09..17eb9c4b4b6 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -708,6 +708,9 @@ func (e *CopilotEngine) buildCopilotExecutionStep(workflowData *WorkflowData, co // DefaultCopilotVersion. This preserves existing behavior while avoiding drift if // DefaultCopilotVersion is ever lowered below CopilotNoAskUserMinVersion. // - "latest": always returns true (latest is always a new release). +// - Expression (e.g. "${{ inputs.engine-version }}"): returns true. The version resolves +// at runtime so we cannot gate at compile time; the installer already handles expression +// versions via ENGINE_VERSION env-var injection, ensuring the right binary is installed. // - Any semver string ≥ CopilotNoAskUserMinVersion: returns true. // - Any semver string < CopilotNoAskUserMinVersion: returns false. // - Non-semver string (e.g. a branch name): returns false (conservative). @@ -716,6 +719,12 @@ func copilotSupportsNoAskUser(engineConfig *EngineConfig) bool { if engineConfig != nil && engineConfig.Version != "" { versionStr = engineConfig.Version } + // Expression versions resolve at runtime; treat as supported so the generated execution + // flags match the installed binary for any version >= CopilotNoAskUserMinVersion. + if containsExpression(versionStr) { + copilotExecLog.Printf("copilotSupportsNoAskUser: expression version %q treated as supported", versionStr) + return true + } return versionAtLeast( versionStr, string(constants.DefaultCopilotVersion), diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 2ace2457b02..0d73dccbd86 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -2978,6 +2978,16 @@ func TestCopilotSupportsNoAskUser(t *testing.T) { engineConfig: &EngineConfig{Version: "main"}, expected: false, }, + { + name: "expression version is treated as supported", + engineConfig: &EngineConfig{Version: "${{ inputs.engine-version }}"}, + expected: true, + }, + { + name: "github event input expression is treated as supported", + engineConfig: &EngineConfig{Version: "${{ github.event.inputs.engine-version }}"}, + expected: true, + }, } for _, tt := range tests { diff --git a/pkg/workflow/gemini_engine.go b/pkg/workflow/gemini_engine.go index 27c6c0be82a..6205be4575b 100644 --- a/pkg/workflow/gemini_engine.go +++ b/pkg/workflow/gemini_engine.go @@ -123,6 +123,13 @@ func (e *GeminiEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHub return []GitHubActionStep{} } + // Normalize engine config version when not explicitly set, so downstream consumers + // (e.g. execution steps) observe the effective installed version. + if workflowData.EngineConfig != nil && workflowData.EngineConfig.Version == "" { + workflowData.EngineConfig.Version = string(constants.DefaultGeminiVersion) + geminiLog.Printf("No engine.version specified, using default Gemini CLI version: %s", workflowData.EngineConfig.Version) + } + npmSteps := BuildStandardNpmEngineInstallStepsNoCooldown( "@google/gemini-cli", string(constants.DefaultGeminiVersion), diff --git a/pkg/workflow/gemini_engine_test.go b/pkg/workflow/gemini_engine_test.go index 9efaa6af4cd..b3c2e2a07f5 100644 --- a/pkg/workflow/gemini_engine_test.go +++ b/pkg/workflow/gemini_engine_test.go @@ -820,11 +820,17 @@ func TestGeminiEngineWithoutVersion(t *testing.T) { engine := NewGeminiEngine() workflowData := &WorkflowData{ - Name: "test-workflow", + Name: "test-workflow", + EngineConfig: &EngineConfig{}, } installSteps := engine.GetInstallationSteps(workflowData) + // EngineConfig.Version must be normalized to the default version. + if workflowData.EngineConfig.Version != string(constants.DefaultGeminiVersion) { + t.Fatalf("Expected engine config version to be normalized to default Gemini version %q, got: %q", constants.DefaultGeminiVersion, workflowData.EngineConfig.Version) + } + var installStep string for _, step := range installSteps { stepContent := strings.Join([]string(step), "\n") diff --git a/pkg/workflow/testdata/TestWasmGolden_AllEngines/gemini.golden b/pkg/workflow/testdata/TestWasmGolden_AllEngines/gemini.golden index 4b7dbf1cebe..d1d87a3564d 100644 --- a/pkg/workflow/testdata/TestWasmGolden_AllEngines/gemini.golden +++ b/pkg/workflow/testdata/TestWasmGolden_AllEngines/gemini.golden @@ -62,6 +62,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "engine-gemini-test" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/workflow.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Generate agentic run info id: generate_aw_info @@ -356,6 +357,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "engine-gemini-test" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/workflow.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -688,6 +690,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "engine-gemini-test" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/workflow.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "0.39.1" GH_AW_INFO_ENGINE_ID: "gemini" - name: Check team membership for workflow id: check_membership