Skip to content

Commit 706d57f

Browse files
authored
Honor workflow-level COPILOT_MODEL in Copilot BYOK smoke workflows (#4876)
* Initial plan * fix: honor workflow COPILOT_MODEL in BYOK smoke * docs: clarify BYOK model rewrite comment --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 2d567fc commit 706d57f

6 files changed

Lines changed: 76 additions & 43 deletions

.github/workflows/smoke-copilot-byok-aoai-apikey.lock.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-copilot-byok-aoai-entra.lock.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/smoke-copilot-byok.lock.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/ci/postprocess-smoke-workflows.test.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ const legacyApiProxyLogsDirRegex =
291291
const copySessionStateStepRegex =
292292
/^(\s+)- name: Copy Copilot session state files to logs\n\1 if: always\(\)\n\1 continue-on-error: true\n\1 run: bash "\$\{RUNNER_TEMP\}\/gh-aw\/actions\/copy_copilot_session_state\.sh"\n/m;
293293

294-
const copilotModelEmptyFallbackRegex =
295-
/(COPILOT_MODEL:\s*\$\{\{\s*vars\.GH_AW_MODEL_AGENT_COPILOT\s*\|\|\s*)(?:vars\.GH_AW_DEFAULT_MODEL_COPILOT\s*\|\|\s*)?'[^']*'(\s*\}\})/g;
294+
const copilotModelOverrideRegex =
295+
/^(\s*COPILOT_MODEL:\s*)\$\{\{\s*(?:vars\.GH_AW_MODEL_AGENT_COPILOT\s*\|\|\s*)?(?:vars\.GH_AW_DEFAULT_MODEL_COPILOT\s*\|\|\s*)?(?:env\.COPILOT_MODEL|''|'[^']*')\s*\}\}[ \t]*$/gm;
296296

297297
function buildCopySessionStateStep(indent: string): string {
298298
const i = indent;
@@ -429,52 +429,61 @@ describe('buildCopySessionStateStep', () => {
429429
});
430430
});
431431

432-
describe('copilotModelEmptyFallbackRegex', () => {
432+
describe('copilotModelOverrideRegex', () => {
433433
beforeEach(() => {
434-
copilotModelEmptyFallbackRegex.lastIndex = 0;
434+
copilotModelOverrideRegex.lastIndex = 0;
435435
});
436436

437-
it('should replace empty fallback with env.COPILOT_MODEL', () => {
437+
it('should replace empty fallback with workflow-level env.COPILOT_MODEL', () => {
438438
const input = " COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }}\n";
439439
const result = input.replace(
440-
copilotModelEmptyFallbackRegex,
441-
`$1env.COPILOT_MODEL$2`
440+
copilotModelOverrideRegex,
441+
'$1${{ env.COPILOT_MODEL }}'
442442
);
443443
expect(result).toBe(
444-
` COPILOT_MODEL: \${{ vars.GH_AW_MODEL_AGENT_COPILOT || env.COPILOT_MODEL }}\n`
444+
` COPILOT_MODEL: \${{ env.COPILOT_MODEL }}\n`
445445
);
446446
});
447447

448-
it('should replace hardcoded model fallback with env.COPILOT_MODEL', () => {
448+
it('should replace hardcoded model fallback with workflow-level env.COPILOT_MODEL', () => {
449449
const input =
450450
" COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-opus-4.8' }}\n";
451451
const result = input.replace(
452-
copilotModelEmptyFallbackRegex,
453-
`$1env.COPILOT_MODEL$2`
452+
copilotModelOverrideRegex,
453+
'$1${{ env.COPILOT_MODEL }}'
454454
);
455455
expect(result).toBe(
456-
` COPILOT_MODEL: \${{ vars.GH_AW_MODEL_AGENT_COPILOT || env.COPILOT_MODEL }}\n`
456+
` COPILOT_MODEL: \${{ env.COPILOT_MODEL }}\n`
457457
);
458458
});
459459

460460
it('should replace fallback chain with vars.GH_AW_DEFAULT_MODEL_COPILOT link', () => {
461461
const input =
462462
" COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }}\n";
463463
const result = input.replace(
464-
copilotModelEmptyFallbackRegex,
465-
`$1env.COPILOT_MODEL$2`
464+
copilotModelOverrideRegex,
465+
'$1${{ env.COPILOT_MODEL }}'
466466
);
467467
expect(result).toBe(
468-
` COPILOT_MODEL: \${{ vars.GH_AW_MODEL_AGENT_COPILOT || env.COPILOT_MODEL }}\n`
468+
` COPILOT_MODEL: \${{ env.COPILOT_MODEL }}\n`
469469
);
470470
});
471471

472-
it('should not modify already-rewritten env.COPILOT_MODEL fallback', () => {
472+
it('should replace repo-level override fallback with workflow-level env.COPILOT_MODEL', () => {
473473
const input =
474474
" COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || env.COPILOT_MODEL }}\n";
475475
const result = input.replace(
476-
copilotModelEmptyFallbackRegex,
477-
`$1env.COPILOT_MODEL$2`
476+
copilotModelOverrideRegex,
477+
'$1${{ env.COPILOT_MODEL }}'
478+
);
479+
expect(result).toBe(` COPILOT_MODEL: \${{ env.COPILOT_MODEL }}\n`);
480+
});
481+
482+
it('should be idempotent when already using workflow-level env.COPILOT_MODEL', () => {
483+
const input = " COPILOT_MODEL: ${{ env.COPILOT_MODEL }}\n";
484+
const result = input.replace(
485+
copilotModelOverrideRegex,
486+
'$1${{ env.COPILOT_MODEL }}'
478487
);
479488
expect(result).toBe(input);
480489
});

scripts/ci/postprocess-smoke-workflows.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,15 @@ const legacyApiProxyLogsDirRegex =
110110
// postinstall script downloads the platform-specific native binary. Without it,
111111
// `claude` fails with "native binary not installed".
112112

113-
// Work around gh-aw compiler bug (gh-aw#26565) where Copilot model fallback is
114-
// emitted at the step level and overrides the workflow-level COPILOT_MODEL env
115-
// when the repo variables are unset. Older compilers emitted an empty string
116-
// fallback (`|| ''`); newer compilers emit a hardcoded default model
117-
// (`|| 'claude-sonnet-4.6'`) and may add an extra `vars.GH_AW_DEFAULT_MODEL_COPILOT`
118-
// link in the fallback chain. In both cases the step-level value wins over the
119-
// workflow-level `env: COPILOT_MODEL: ...` we set on BYOK smoke workflows, which
120-
// breaks targeted BYOK testing (e.g. forcing `o4-mini-aw` against Azure OpenAI).
121-
// We replace the entire expression with `env.COPILOT_MODEL` so the step inherits
122-
// the workflow-level default verbatim.
123-
const copilotModelEmptyFallbackRegex =
124-
/(COPILOT_MODEL:\s*\$\{\{\s*vars\.GH_AW_MODEL_AGENT_COPILOT\s*\|\|\s*)(?:vars\.GH_AW_DEFAULT_MODEL_COPILOT\s*\|\|\s*)?'[^']*'(\s*\}\})/g;
113+
// Work around gh-aw compiler bug (gh-aw#26565) where Copilot model selection is
114+
// emitted at the step level for BYOK smoke workflows, overriding the
115+
// workflow-level `env: COPILOT_MODEL: ...` that intentionally pins a
116+
// low-cost/provider-specific model (for example `claude-haiku-4.5` or
117+
// `o4-mini-aw`). Normalize every compiled step-level COPILOT_MODEL expression
118+
// back to `${{ env.COPILOT_MODEL }}` so the workflow-level setting wins even if
119+
// repo-level default model variables are configured.
120+
const copilotModelOverrideRegex =
121+
/^(\s*COPILOT_MODEL:\s*)\$\{\{\s*(?:vars\.GH_AW_MODEL_AGENT_COPILOT\s*\|\|\s*)?(?:vars\.GH_AW_DEFAULT_MODEL_COPILOT\s*\|\|\s*)?(?:env\.COPILOT_MODEL|''|'[^']*')\s*\}\}[ \t]*$/gm;
125122

126123
// Sentinel used to detect whether the "Copy Copilot session state" step has
127124
// already been replaced with the AWF-aware inline script.
@@ -542,20 +539,21 @@ for (const workflowPath of workflowPaths) {
542539
}
543540
}
544541

545-
// For smoke-copilot-byok variants: replace empty model fallbacks with the
546-
// workflow-level COPILOT_MODEL env so the generated step inherits the shared
547-
// default without hardcoding a duplicate model string here.
542+
// For smoke-copilot-byok variants: replace compiled COPILOT_MODEL override
543+
// expressions with the workflow-level COPILOT_MODEL env so the generated
544+
// step inherits the intended BYOK model instead of any repo-level default.
548545
const isCopilotByokSmoke = /smoke-copilot-byok[^/]*\.lock\.yml$/.test(workflowPath);
549546
if (isCopilotByokSmoke) {
550-
const emptyFallbackMatches = content.match(copilotModelEmptyFallbackRegex);
551-
if (emptyFallbackMatches) {
552-
content = content.replace(
553-
copilotModelEmptyFallbackRegex,
554-
'$1env.COPILOT_MODEL$2'
555-
);
547+
const rewrittenContent = content.replace(
548+
copilotModelOverrideRegex,
549+
'$1${{ env.COPILOT_MODEL }}'
550+
);
551+
if (rewrittenContent !== content) {
552+
const rewrittenCount = (content.match(copilotModelOverrideRegex) || []).length;
553+
content = rewrittenContent;
556554
modified = true;
557555
console.log(
558-
` Replaced ${emptyFallbackMatches.length} empty COPILOT_MODEL fallback(s) for BYOK smoke`
556+
` Rewrote ${rewrittenCount} COPILOT_MODEL override(s) to env.COPILOT_MODEL for BYOK smoke`
559557
);
560558
}
561559
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
const workflowsDir = path.resolve(__dirname, '../../.github/workflows');
5+
6+
const byokSourcePath = path.join(workflowsDir, 'smoke-copilot-byok.md');
7+
const byokLockPaths = [
8+
path.join(workflowsDir, 'smoke-copilot-byok.lock.yml'),
9+
path.join(workflowsDir, 'smoke-copilot-byok-aoai-apikey.lock.yml'),
10+
path.join(workflowsDir, 'smoke-copilot-byok-aoai-entra.lock.yml'),
11+
];
12+
13+
describe('smoke copilot BYOK workflow model selection', () => {
14+
it('pins the direct BYOK source workflow to claude-haiku-4.5', () => {
15+
const source = fs.readFileSync(byokSourcePath, 'utf-8');
16+
17+
expect(source).toContain('COPILOT_MODEL: claude-haiku-4.5');
18+
});
19+
20+
it.each(byokLockPaths)('forces workflow-level COPILOT_MODEL in %s', (lockPath) => {
21+
const lock = fs.readFileSync(lockPath, 'utf-8');
22+
23+
expect(lock).toContain('COPILOT_MODEL: ${{ env.COPILOT_MODEL }}');
24+
expect(lock).not.toContain('COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || env.COPILOT_MODEL }}');
25+
});
26+
});

0 commit comments

Comments
 (0)