diff --git a/.gitattributes b/.gitattributes index 5fc58b06b87..529dd5125ac 100644 --- a/.gitattributes +++ b/.gitattributes @@ -18,4 +18,4 @@ slides/*.pdf filter=lfs diff=lfs merge=lfs -text slides/*.pptx filter=lfs diff=lfs merge=lfs -text docs/slides/** filter=lfs diff=lfs merge=lfs -text -.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file +.github/workflows/*.lock.yml linguist-generated=true \ No newline at end of file diff --git a/create.md b/create.md index 7cd6cd2e020..5aac3fb4eab 100644 --- a/create.md +++ b/create.md @@ -126,7 +126,7 @@ If creating a workflow, the actual files you created will be under `.github/work If creating a workflow, check the .gitattributes file and make sure it exists and contains at least the following line: ```text -.github/workflows/*.lock.yml linguist-generated=true merge=ours +.github/workflows/*.lock.yml linguist-generated=true ``` You do not need to run `gh aw init` as part of your workflow creation. However if you did run this you may also see: diff --git a/pkg/cli/add_gitattributes_test.go b/pkg/cli/add_gitattributes_test.go index a502e01e4d8..4a7001d3d12 100644 --- a/pkg/cli/add_gitattributes_test.go +++ b/pkg/cli/add_gitattributes_test.go @@ -109,8 +109,8 @@ This is a test workflow.` t.Fatalf("Failed to read .gitattributes: %v", err) } - if !strings.Contains(string(content), ".github/workflows/*.lock.yml linguist-generated=true merge=ours") { - t.Errorf("Expected .gitattributes to contain '.github/workflows/*.lock.yml linguist-generated=true merge=ours'") + if !strings.Contains(string(content), ".github/workflows/*.lock.yml linguist-generated=true") { + t.Errorf("Expected .gitattributes to contain '.github/workflows/*.lock.yml linguist-generated=true'") } }) diff --git a/pkg/cli/git.go b/pkg/cli/git.go index 8bb45af9acb..fc08c490135 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -375,9 +375,11 @@ func ensureGitAttributes() (bool, error) { found = true break } - // Check for old format entries that need updating - if strings.HasPrefix(trimmedLine, constants.WorkflowsLockYmlGlob) && required == lockYmlEntry { - gitLog.Print("Updating old .gitattributes entry format") + // Only clean up the exact legacy gh-aw entry (with the ineffective + // "merge=ours" attribute); never rewrite other repository-owned lines + // that happen to start with the lock-yml glob. + if trimmedLine == constants.WorkflowsLockYmlGitAttributesEntryLegacy && required == lockYmlEntry { + gitLog.Print("Updating legacy .gitattributes entry format") lines[i] = lockYmlEntry found = true modified = true diff --git a/pkg/cli/gitattributes_test.go b/pkg/cli/gitattributes_test.go index fc5b54f69d1..6c2425c5cce 100644 --- a/pkg/cli/gitattributes_test.go +++ b/pkg/cli/gitattributes_test.go @@ -47,31 +47,37 @@ func TestEnsureGitAttributes(t *testing.T) { { name: "creates new gitattributes file", existingContent: "", - expectedContent: ".github/workflows/*.lock.yml linguist-generated=true merge=ours", + expectedContent: ".github/workflows/*.lock.yml linguist-generated=true", expectUpdated: true, }, { name: "adds entry to existing file", existingContent: "*.generated linguist-generated=true\n", - expectedContent: "*.generated linguist-generated=true\n\n.github/workflows/*.lock.yml linguist-generated=true merge=ours", + expectedContent: "*.generated linguist-generated=true\n\n.github/workflows/*.lock.yml linguist-generated=true", expectUpdated: true, }, { name: "does not duplicate existing entry", - existingContent: ".github/workflows/*.lock.yml linguist-generated=true merge=ours\n", - expectedContent: ".github/workflows/*.lock.yml linguist-generated=true merge=ours", + existingContent: ".github/workflows/*.lock.yml linguist-generated=true\n", + expectedContent: ".github/workflows/*.lock.yml linguist-generated=true", expectUpdated: false, }, { name: "does not duplicate entry with different order", - existingContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true merge=ours\n*.txt text=auto\n", - expectedContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true merge=ours\n*.txt text=auto", + existingContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true\n*.txt text=auto\n", + expectedContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true\n*.txt text=auto", expectUpdated: false, }, { - name: "updates old format entry", - existingContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true\n*.txt text=auto\n", - expectedContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true merge=ours\n*.txt text=auto", + name: "migrates legacy merge=ours entry", + existingContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true merge=ours\n*.txt text=auto\n", + expectedContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true\n*.txt text=auto", + expectUpdated: true, + }, + { + name: "does not rewrite repository-owned lock-yml policy", + existingContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true merge=union\n*.txt text=auto\n", + expectedContent: "*.md linguist-documentation=true\n.github/workflows/*.lock.yml linguist-generated=true merge=union\n*.txt text=auto\n\n.github/workflows/*.lock.yml linguist-generated=true", expectUpdated: true, }, } @@ -119,8 +125,8 @@ func TestEnsureGitAttributes(t *testing.T) { } // Verify the entry is actually present - if !strings.Contains(string(content), ".github/workflows/*.lock.yml linguist-generated=true merge=ours") { - t.Errorf("Expected .gitattributes to contain '.github/workflows/*.lock.yml linguist-generated=true merge=ours'") + if !strings.Contains(string(content), ".github/workflows/*.lock.yml linguist-generated=true") { + t.Errorf("Expected .gitattributes to contain '.github/workflows/*.lock.yml linguist-generated=true'") } // Verify campaign.g.md entry is NOT present (it's now in .gitignore) if strings.Contains(string(content), ".github/workflows/*.campaign.g.md") { diff --git a/pkg/cli/init_command_test.go b/pkg/cli/init_command_test.go index 9cd2735aa58..706e813e420 100644 --- a/pkg/cli/init_command_test.go +++ b/pkg/cli/init_command_test.go @@ -357,7 +357,7 @@ func TestInitRepositoryBasic(t *testing.T) { t.Fatalf("Failed to read .gitattributes: %v", err) } - expectedEntry := ".github/workflows/*.lock.yml linguist-generated=true merge=ours" + expectedEntry := ".github/workflows/*.lock.yml linguist-generated=true" if !strings.Contains(string(content), expectedEntry) { t.Errorf("Expected .gitattributes to contain %q", expectedEntry) } @@ -797,7 +797,7 @@ func TestInitRepositoryIdempotent(t *testing.T) { t.Fatalf("Failed to read .gitattributes: %v", err) } - expectedEntry := ".github/workflows/*.lock.yml linguist-generated=true merge=ours" + expectedEntry := ".github/workflows/*.lock.yml linguist-generated=true" // Count occurrences - should only appear once count := strings.Count(string(content), expectedEntry) @@ -997,7 +997,7 @@ func TestInitRepositoryWithExistingFiles(t *testing.T) { t.Error("Expected existing content to be preserved") } - expectedEntry := ".github/workflows/*.lock.yml linguist-generated=true merge=ours" + expectedEntry := ".github/workflows/*.lock.yml linguist-generated=true" if !strings.Contains(contentStr, expectedEntry) { t.Error("Expected new entry to be added") } diff --git a/pkg/cli/init_integration_test.go b/pkg/cli/init_integration_test.go index 4b1e32a5d53..8698c9aeed0 100644 --- a/pkg/cli/init_integration_test.go +++ b/pkg/cli/init_integration_test.go @@ -83,7 +83,7 @@ func TestInitCommandIntegration(t *testing.T) { gitAttrPath := filepath.Join(setup.tempDir, ".gitattributes") content, err := os.ReadFile(gitAttrPath) require.NoError(t, err, ".gitattributes should be created") - assert.Contains(t, string(content), ".github/workflows/*.lock.yml linguist-generated=true merge=ours", + assert.Contains(t, string(content), ".github/workflows/*.lock.yml linguist-generated=true", ".gitattributes should mark lock.yml files as generated") // The dispatcher skill is created for every engine. diff --git a/pkg/cli/init_test.go b/pkg/cli/init_test.go index 13314c4c943..a9b891b77da 100644 --- a/pkg/cli/init_test.go +++ b/pkg/cli/init_test.go @@ -86,8 +86,8 @@ func TestInitRepository(t *testing.T) { if err != nil { t.Fatalf("Failed to read .gitattributes: %v", err) } - if !strings.Contains(string(content), ".github/workflows/*.lock.yml linguist-generated=true merge=ours") { - t.Errorf("Expected .gitattributes to contain '.github/workflows/*.lock.yml linguist-generated=true merge=ours'") + if !strings.Contains(string(content), ".github/workflows/*.lock.yml linguist-generated=true") { + t.Errorf("Expected .gitattributes to contain '.github/workflows/*.lock.yml linguist-generated=true'") } }) } diff --git a/pkg/constants/README.md b/pkg/constants/README.md index be63313bd4a..a48339dcc98 100644 --- a/pkg/constants/README.md +++ b/pkg/constants/README.md @@ -545,10 +545,10 @@ This appendix is generated from the current non-test Go source files in this pac | Category | Count | |----------|------:| | Types | 14 | -| Constants | 305 | +| Constants | 306 | | Variables | 23 | | Functions and methods | 14 | -| Additional symbols documented in this appendix | 99 | +| Additional symbols documented in this appendix | 100 | ### Additional constants and variables @@ -620,7 +620,8 @@ This appendix is generated from the current non-test Go source files in this pac | `constants.go` | `const` | `UsrLocalPrefix` | `const UsrLocalPrefix = "/usr/local"` | UsrLocalPrefix is the standard /usr/local installation prefix. | | `constants.go` | `const` | `WorkflowsDir` | `const WorkflowsDir = ".github/workflows"` | WorkflowsDir is the GitHub Actions workflow directory path (without trailing slash). | | `constants.go` | `const` | `WorkflowsDirSlash` | `const WorkflowsDirSlash = WorkflowsDir + "/"` | WorkflowsDirSlash is WorkflowsDir with a trailing slash. | -| `constants.go` | `const` | `WorkflowsLockYmlGitAttributesEntry` | `const WorkflowsLockYmlGitAttributesEntry = WorkflowsLockYmlGlob + " linguist-generated=true merge=ours"` | WorkflowsLockYmlGitAttributesEntry is the . | +| `constants.go` | `const` | `WorkflowsLockYmlGitAttributesEntry` | `const WorkflowsLockYmlGitAttributesEntry = WorkflowsLockYmlGlob + " linguist-generated=true"` | WorkflowsLockYmlGitAttributesEntry is the .gitattributes entry that marks lock YAML files as generated. | +| `constants.go` | `const` | `WorkflowsLockYmlGitAttributesEntryLegacy` | `const WorkflowsLockYmlGitAttributesEntryLegacy = WorkflowsLockYmlGlob + " linguist-generated=true merge=ours"` | WorkflowsLockYmlGitAttributesEntryLegacy is the previous .gitattributes entry format that included an ineffective "merge=ours" attribute. It is only used to detect and clean up entries that gh-aw itself previously wrote, so we do not overwrite repository-owned policy. | | `constants.go` | `const` | `WorkflowsLockYmlGlob` | `const WorkflowsLockYmlGlob = WorkflowsDirSlash + "*.lock.yml"` | WorkflowsLockYmlGlob is the glob pattern for compiled workflow lock YAML files. | | `engine_constants.go` | `const` | `AnthropicAPIKey` | `const AnthropicAPIKey = "ANTHROPIC_API_KEY"` | AnthropicAPIKey is the API key secret name required by the Claude engine. | | `engine_constants.go` | `const` | `CodexAPIKey` | `const CodexAPIKey = "CODEX_API_KEY"` | CodexAPIKey is the API key secret name used by the Codex engine. | diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 6826b343b05..5a86eddcacc 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -427,8 +427,13 @@ const AgentsDir = ".github/agents/" const WorkflowsLockYmlGlob = WorkflowsDirSlash + "*.lock.yml" // WorkflowsLockYmlGitAttributesEntry is the .gitattributes entry that marks lock YAML -// files as generated and sets the merge strategy. -const WorkflowsLockYmlGitAttributesEntry = WorkflowsLockYmlGlob + " linguist-generated=true merge=ours" +// files as generated. +const WorkflowsLockYmlGitAttributesEntry = WorkflowsLockYmlGlob + " linguist-generated=true" + +// WorkflowsLockYmlGitAttributesEntryLegacy is the previous .gitattributes entry format that +// included an ineffective "merge=ours" attribute. It is only used to detect and clean up +// entries that gh-aw itself previously wrote, so we do not overwrite repository-owned policy. +const WorkflowsLockYmlGitAttributesEntryLegacy = WorkflowsLockYmlGlob + " linguist-generated=true merge=ours" // Temporary runtime directory constants (/tmp/gh-aw tree) //