Skip to content

Commit 7bc15e2

Browse files
authored
Cover top-level app: migration and complete breaking-change release notes (#42794)
1 parent 375b4ea commit 7bc15e2

7 files changed

Lines changed: 314 additions & 11 deletions

.changeset/major-remove-imports-if.md

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.changeset/minor-rename-app-to-github-app.md

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,17 @@ Run `gh aw fix --write` to apply automatic updates across your repository.
438438

439439
### Breaking Changes
440440

441+
#### Remove `imports.if` from workflow frontmatter
442+
443+
`imports:` entries no longer accept an `if` condition. Conditional imports are
444+
now rejected so the compiled workflow structure stays explicit and stable at
445+
compile time.
446+
447+
**Migration:**
448+
- Remove `if:` from `imports:` entries and keep imports unconditional
449+
- For experiment-specific prompt variants, move the condition into the workflow
450+
body and use `{{#if ...}}` with `{{#runtime-import ...}}`
451+
441452
#### Terminology Change: "Agent Task" → "Agent Session"
442453

443454
The terminology for creating Copilot coding agent work items has been updated from "agent task" to "agent session" to better reflect their purpose and avoid confusion with other task concepts.
@@ -455,10 +466,11 @@ Run `gh aw fix` to automatically update your workflow files to use the new termi
455466
#### Replace removed `app:` with `github-app:`
456467

457468
The deprecated `app:` workflow frontmatter field was removed and replaced with
458-
`github-app:`. Workflows still using `app:` will now fail validation.
469+
`github-app:`. Workflows still using top-level `app:` or nested `app:` auth
470+
blocks will now fail validation.
459471

460472
**Migration:**
461-
- Replace `app:` with `github-app:`
473+
- Replace `app:` with `github-app:` everywhere in workflow frontmatter
462474
- Run `gh aw fix` to apply the codemod automatically
463475

464476
#### Replace `supportsLLMGateway` flag with `llmGatewayPort`
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ADR-42794: Extend `app-to-github-app` Codemod to Cover Top-Level `app:` Key
2+
3+
**Date**: 2026-07-01
4+
**Status**: Draft
5+
**Deciders**: pelikhan, copilot-swe-agent
6+
7+
---
8+
9+
### Context
10+
11+
The `gh aw fix` command provides automated codemods to help users migrate their workflow frontmatter across breaking changes. A previous breaking change renamed the `app:` frontmatter field to `github-app:`, and a codemod was implemented to automate that rename. However, the codemod only handled `app:` when it appeared nested under `tools.github`, `safe-outputs`, or `checkout` blocks — it did not handle the top-level `app:` key. Users whose workflows used the top-level `app:` auth configuration would run `gh aw fix` and see no changes applied, then encounter validation failures when the field was rejected by the tooling. This gap was surfaced through a daily breaking-change audit.
12+
13+
### Decision
14+
15+
We will extend the `hasDeprecatedAppField` detection function and the `renameAppToGitHubApp` rewrite function in `pkg/cli/codemod_github_app.go` to also detect and rename a top-level `app:` key in workflow YAML frontmatter. The top-level check uses the existing `isTopLevelKey` helper (shared frontmatter key detection logic) and is evaluated before the nested-section checks to avoid double-processing. This preserves the existing behavior for nested locations while closing the coverage gap.
16+
17+
### Alternatives Considered
18+
19+
#### Alternative 1: Document-only migration (no codemod for top-level)
20+
21+
Rather than extending the codemod, we could have documented the top-level `app:` rename in the migration guide and relied on users to make the change manually. This was rejected because the entire purpose of `gh aw fix` is to automate breaking-change migrations, and leaving one common configuration location uncovered would undermine user trust in the tool and create inconsistent migration outcomes.
22+
23+
#### Alternative 2: Generic "rename any `app:` key" approach
24+
25+
An alternative was to rename every occurrence of `app:` in any position within the frontmatter, without checking whether it is at the top level or inside a specific section. This was rejected because it would be too broad: it could incorrectly rename `app:` keys that appear inside unrelated nested structures, or inside the workflow body rather than the frontmatter, leading to false positives.
26+
27+
### Consequences
28+
29+
#### Positive
30+
- `gh aw fix` now covers all known locations of the deprecated `app:` field, giving users a fully automated migration path for this breaking change.
31+
- Regression tests (`codemod_github_app_test.go` and `fix_command_test.go`) were added for top-level rename coverage, increasing confidence that this behavior does not regress in future changes.
32+
33+
#### Negative
34+
- The `hasDeprecatedAppField` and `renameAppToGitHubApp` functions are now more complex, with an explicit ordering dependency: top-level checks run before nested-section checks.
35+
- Future contributors adding new `app:` rename locations must understand this ordering, which is not self-documenting.
36+
37+
#### Neutral
38+
- The top-level check reuses the existing `isTopLevelKey` shared helper, avoiding any new YAML parsing logic.
39+
- Changeset and CHANGELOG documentation was updated to reflect that both top-level and nested `app:` locations are affected by the breaking change.
40+
41+
---
42+
43+
*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*

pkg/cli/codemod_github_app.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import (
99
var githubAppCodemodLog = logger.New("cli:codemod_github_app")
1010

1111
// getGitHubAppCodemod creates a codemod for renaming 'app:' to 'github-app:' in workflow frontmatter.
12-
// The 'app:' field under tools.github, safe-outputs, and checkout is deprecated in favour of 'github-app:'.
12+
// The deprecated 'app:' field can appear at the top level and under tools.github,
13+
// safe-outputs, and checkout.
1314
func getGitHubAppCodemod() Codemod {
1415
return Codemod{
1516
ID: "app-to-github-app",
1617
Name: "Rename 'app' to 'github-app'",
17-
Description: "Renames the deprecated 'app:' field to 'github-app:' in tools.github, safe-outputs, and checkout configurations.",
18+
Description: "Renames the deprecated 'app:' field to 'github-app:' at the top level and in tools.github, safe-outputs, and checkout configurations.",
1819
IntroducedIn: "0.15.0",
1920
Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
2021
if !hasDeprecatedAppField(frontmatter) {
@@ -29,8 +30,15 @@ func getGitHubAppCodemod() Codemod {
2930
}
3031
}
3132

32-
// hasDeprecatedAppField returns true if any of the target sections contain a deprecated 'app:' field.
33+
// hasDeprecatedAppField returns true if the deprecated 'app:' field is present at the
34+
// top level or in one of the supported nested sections.
3335
func hasDeprecatedAppField(frontmatter map[string]any) bool {
36+
// Check top-level app
37+
if _, hasApp := frontmatter["app"]; hasApp {
38+
githubAppCodemodLog.Print("Deprecated 'app' field found at top level")
39+
return true
40+
}
41+
3442
// Check tools.github.app
3543
if toolsAny, hasTools := frontmatter["tools"]; hasTools {
3644
if toolsMap, ok := toolsAny.(map[string]any); ok {
@@ -78,7 +86,8 @@ func hasDeprecatedAppField(frontmatter map[string]any) bool {
7886
return false
7987
}
8088

81-
// renameAppToGitHubApp renames 'app:' to 'github-app:' within tools.github, safe-outputs, and checkout blocks.
89+
// renameAppToGitHubApp renames top-level 'app:' keys and nested 'app:' keys within
90+
// tools.github, safe-outputs, and checkout blocks.
8291
func renameAppToGitHubApp(lines []string) ([]string, bool) {
8392
var result []string
8493
modified := false
@@ -143,7 +152,18 @@ func renameAppToGitHubApp(lines []string) ([]string, bool) {
143152
continue
144153
}
145154

146-
// Rename 'app:' to 'github-app:' when inside a target block
155+
// Rename a top-level 'app:' key.
156+
if strings.HasPrefix(trimmed, "app:") && isTopLevelKey(line) {
157+
newLine, replaced := findAndReplaceInLine(line, "app", "github-app")
158+
if replaced {
159+
result = append(result, newLine)
160+
modified = true
161+
githubAppCodemodLog.Printf("Renamed top-level 'app' to 'github-app' on line %d", i+1)
162+
continue
163+
}
164+
}
165+
166+
// Rename nested 'app:' keys when inside a target block
147167
if strings.HasPrefix(trimmed, "app:") {
148168
lineIndent := getIndentation(line)
149169
shouldRename := false

pkg/cli/codemod_github_app_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,31 @@ checkout:
116116
assert.False(t, hasDeprecatedAppFieldInContent(result), "Should not contain old app field")
117117
})
118118

119+
t.Run("renames top-level app to github-app", func(t *testing.T) {
120+
content := `---
121+
engine: copilot
122+
app:
123+
app-id: ${{ vars.APP_ID }}
124+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
125+
---
126+
127+
# Test Workflow
128+
`
129+
frontmatter := map[string]any{
130+
"engine": "copilot",
131+
"app": map[string]any{
132+
"app-id": "${{ vars.APP_ID }}",
133+
"private-key": "${{ secrets.APP_PRIVATE_KEY }}",
134+
},
135+
}
136+
137+
result, modified, err := codemod.Apply(content, frontmatter)
138+
require.NoError(t, err, "Should not error when applying codemod")
139+
assert.True(t, modified, "Should modify content")
140+
assert.Contains(t, result, "github-app:", "Should contain github-app field")
141+
assert.False(t, hasDeprecatedAppFieldInContent(result), "Should not contain old app field")
142+
})
143+
119144
t.Run("does not modify workflows without app field", func(t *testing.T) {
120145
content := `---
121146
engine: copilot
@@ -310,4 +335,55 @@ tools:
310335
assert.Contains(t, result, "# GitHub App for token minting", "Should preserve comment")
311336
assert.Contains(t, result, "github-app: # Use a GitHub App", "Should preserve inline comment")
312337
})
338+
339+
t.Run("renames top-level app and nested app in the same document", func(t *testing.T) {
340+
content := `---
341+
engine: copilot
342+
app:
343+
app-id: ${{ vars.APP_ID }}
344+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
345+
tools:
346+
github:
347+
mode: remote
348+
app:
349+
app-id: ${{ vars.APP_ID }}
350+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
351+
safe-outputs:
352+
app:
353+
app-id: ${{ vars.APP_ID }}
354+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
355+
---
356+
357+
# Test Workflow
358+
`
359+
frontmatter := map[string]any{
360+
"engine": "copilot",
361+
"app": map[string]any{
362+
"app-id": "${{ vars.APP_ID }}",
363+
"private-key": "${{ secrets.APP_PRIVATE_KEY }}",
364+
},
365+
"tools": map[string]any{
366+
"github": map[string]any{
367+
"mode": "remote",
368+
"app": map[string]any{
369+
"app-id": "${{ vars.APP_ID }}",
370+
"private-key": "${{ secrets.APP_PRIVATE_KEY }}",
371+
},
372+
},
373+
},
374+
"safe-outputs": map[string]any{
375+
"app": map[string]any{
376+
"app-id": "${{ vars.APP_ID }}",
377+
"private-key": "${{ secrets.APP_PRIVATE_KEY }}",
378+
},
379+
},
380+
}
381+
382+
result, modified, err := codemod.Apply(content, frontmatter)
383+
require.NoError(t, err, "Should not error when applying codemod")
384+
assert.True(t, modified, "Should modify content")
385+
assert.False(t, hasDeprecatedAppFieldInContent(result), "Should not contain any old app fields")
386+
// Expect all three app: occurrences replaced with github-app:
387+
assert.Equal(t, 3, strings.Count(result, "github-app:"), "Should have three github-app: occurrences")
388+
})
313389
}

0 commit comments

Comments
 (0)