From 4dc896cf4a0b3f39c209c2f677bed6e0f45fbe7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:18:07 +0000 Subject: [PATCH 1/4] Initial plan From fd4da04c8417a73be0d91bd8d09598fca759d1e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:35:14 +0000 Subject: [PATCH 2/4] fix: tighten repo-memory push concurrency key to actual branch targets The push_repo_memory job previously used a single repository-wide concurrency group (push-repo-memory-${{ github.repository }}), causing all workflows that use repo-memory to serialise or cancel each other even when they push to completely different memory/* branches. This change introduces buildPushRepoMemoryConcurrencyGroup(), which builds the concurrency group key from the sorted set of (target-repo, branch) pairs that the push job actually writes to. Different workflows pushing to different branches now run in parallel; workflows pushing to the same branch still serialise correctly. All 178 lock files have been recompiled with their per-branch keys. Fixes #" Agent-Logs-Url: https://github.com/github/gh-aw/sessions/bfc3a2b6-31fa-4406-9f29-97b0018892ae Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../agent-performance-analyzer.lock.yml | 2 +- .github/workflows/audit-workflows.lock.yml | 2 +- .../workflows/code-scanning-fixer.lock.yml | 2 +- .../workflows/copilot-agent-analysis.lock.yml | 2 +- .../copilot-cli-deep-research.lock.yml | 2 +- .../copilot-pr-nlp-analysis.lock.yml | 2 +- .../copilot-pr-prompt-analysis.lock.yml | 2 +- .../copilot-session-insights.lock.yml | 2 +- .../workflows/daily-cli-performance.lock.yml | 2 +- .github/workflows/daily-code-metrics.lock.yml | 2 +- .../daily-community-attribution.lock.yml | 2 +- .../daily-copilot-token-report.lock.yml | 2 +- .github/workflows/daily-news.lock.yml | 2 +- .../daily-testify-uber-super-expert.lock.yml | 2 +- .github/workflows/deep-report.lock.yml | 2 +- .github/workflows/delight.lock.yml | 2 +- .../developer-docs-consolidator.lock.yml | 2 +- .../workflows/discussion-task-miner.lock.yml | 2 +- .github/workflows/firewall-escape.lock.yml | 2 +- .../workflows/glossary-maintainer.lock.yml | 2 +- .github/workflows/metrics-collector.lock.yml | 2 +- .github/workflows/pr-triage-agent.lock.yml | 2 +- .../workflows/security-compliance.lock.yml | 2 +- .../workflows/technical-doc-writer.lock.yml | 2 +- .../weekly-blog-post-writer.lock.yml | 2 +- .../workflow-health-manager.lock.yml | 2 +- pkg/workflow/repo_memory.go | 34 ++++++++- pkg/workflow/repo_memory_test.go | 71 +++++++++++++++++++ 28 files changed, 128 insertions(+), 29 deletions(-) diff --git a/.github/workflows/agent-performance-analyzer.lock.yml b/.github/workflows/agent-performance-analyzer.lock.yml index 8ce2e9fdb61..20e1cce59ff 100644 --- a/.github/workflows/agent-performance-analyzer.lock.yml +++ b/.github/workflows/agent-performance-analyzer.lock.yml @@ -1223,7 +1223,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/meta-orchestrators" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index b052f9939d0..c35a6d260d4 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -1302,7 +1302,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/audit-workflows" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 5a537927cb8..188c7aaf0d4 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -1173,7 +1173,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/campaigns" cancel-in-progress: false outputs: patch_size_exceeded_campaigns: ${{ steps.push_repo_memory_campaigns.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index 16c85ac0267..b0331e35495 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -1185,7 +1185,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/copilot-agent-analysis" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-cli-deep-research.lock.yml b/.github/workflows/copilot-cli-deep-research.lock.yml index 0a67f69a325..8a64631f4d0 100644 --- a/.github/workflows/copilot-cli-deep-research.lock.yml +++ b/.github/workflows/copilot-cli-deep-research.lock.yml @@ -1083,7 +1083,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/copilot-cli-research" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index 8e8f66900ec..4d3eb50bd99 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -1180,7 +1180,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/nlp-analysis" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index 7843c85cbf7..c4eb46b2215 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -1116,7 +1116,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/prompt-analysis" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index b21b5479dbd..15ca47f11a4 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -1248,7 +1248,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/session-insights" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-cli-performance.lock.yml b/.github/workflows/daily-cli-performance.lock.yml index d2cb761e3c1..7b76ad03f51 100644 --- a/.github/workflows/daily-cli-performance.lock.yml +++ b/.github/workflows/daily-cli-performance.lock.yml @@ -1329,7 +1329,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/cli-performance" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index f72a397dd83..b13fb81e61f 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -1223,7 +1223,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-daily/daily-code-metrics" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-community-attribution.lock.yml b/.github/workflows/daily-community-attribution.lock.yml index 72556476950..ba882016392 100644 --- a/.github/workflows/daily-community-attribution.lock.yml +++ b/.github/workflows/daily-community-attribution.lock.yml @@ -1129,7 +1129,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml index 7a3f19c2444..89c63cbf18a 100644 --- a/.github/workflows/daily-copilot-token-report.lock.yml +++ b/.github/workflows/daily-copilot-token-report.lock.yml @@ -1185,7 +1185,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/token-metrics" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index cfbc20bfabf..d6003827d6f 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -1256,7 +1256,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/daily-news" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-testify-uber-super-expert.lock.yml b/.github/workflows/daily-testify-uber-super-expert.lock.yml index 0b96052c6ba..5701a892c42 100644 --- a/.github/workflows/daily-testify-uber-super-expert.lock.yml +++ b/.github/workflows/daily-testify-uber-super-expert.lock.yml @@ -1212,7 +1212,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/testify-expert" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 4cd12565ac4..348152d8a57 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -1284,7 +1284,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/deep-report" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/delight.lock.yml b/.github/workflows/delight.lock.yml index cf81c8f20fb..ed3a50cad59 100644 --- a/.github/workflows/delight.lock.yml +++ b/.github/workflows/delight.lock.yml @@ -1128,7 +1128,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/delight" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 91cc4aade9f..1b99241b2a6 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -1453,7 +1453,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/discussion-task-miner.lock.yml b/.github/workflows/discussion-task-miner.lock.yml index cf28d21f938..bd54e11600e 100644 --- a/.github/workflows/discussion-task-miner.lock.yml +++ b/.github/workflows/discussion-task-miner.lock.yml @@ -1111,7 +1111,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/discussion-task-miner" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/firewall-escape.lock.yml b/.github/workflows/firewall-escape.lock.yml index 067c12788f0..19c2b7f676f 100644 --- a/.github/workflows/firewall-escape.lock.yml +++ b/.github/workflows/firewall-escape.lock.yml @@ -1186,7 +1186,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/firewall-escape" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 9a3d5c6c55e..ed70c79fdaf 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1348,7 +1348,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/metrics-collector.lock.yml b/.github/workflows/metrics-collector.lock.yml index 06ac3960bc9..a18f0f51933 100644 --- a/.github/workflows/metrics-collector.lock.yml +++ b/.github/workflows/metrics-collector.lock.yml @@ -685,7 +685,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/meta-orchestrators" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/pr-triage-agent.lock.yml b/.github/workflows/pr-triage-agent.lock.yml index ceab9e306a9..0339dfc5618 100644 --- a/.github/workflows/pr-triage-agent.lock.yml +++ b/.github/workflows/pr-triage-agent.lock.yml @@ -1097,7 +1097,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/pr-triage" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/security-compliance.lock.yml b/.github/workflows/security-compliance.lock.yml index 18bb482e28e..8050ce147d3 100644 --- a/.github/workflows/security-compliance.lock.yml +++ b/.github/workflows/security-compliance.lock.yml @@ -1076,7 +1076,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/campaigns" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 2404d471100..7d5b8e23609 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -1352,7 +1352,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/weekly-blog-post-writer.lock.yml b/.github/workflows/weekly-blog-post-writer.lock.yml index 3d41d62bf1d..1d81196ec49 100644 --- a/.github/workflows/weekly-blog-post-writer.lock.yml +++ b/.github/workflows/weekly-blog-post-writer.lock.yml @@ -1314,7 +1314,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml index f3cc3cf7079..55867a5f85d 100644 --- a/.github/workflows/workflow-health-manager.lock.yml +++ b/.github/workflows/workflow-health-manager.lock.yml @@ -1178,7 +1178,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}" + group: "push-repo-memory-${{ github.repository }}-memory/meta-orchestrators" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/pkg/workflow/repo_memory.go b/pkg/workflow/repo_memory.go index 29bd7d78ec2..76909729167 100644 --- a/pkg/workflow/repo_memory.go +++ b/pkg/workflow/repo_memory.go @@ -13,6 +13,7 @@ import ( "encoding/json" "fmt" "regexp" + "sort" "strings" "github.com/github/gh-aw/pkg/constants" @@ -547,6 +548,30 @@ func generateRepoMemorySteps(builder *strings.Builder, data *WorkflowData) { } } +// buildPushRepoMemoryConcurrencyGroup builds a concurrency group key that is scoped to the +// specific (target-repo, branch) pairs being written by this push job. Using the actual +// write targets—rather than a single repo-wide key—ensures that workflows pushing to +// different memory branches do not unnecessarily serialise or cancel each other. +// +// Key format: "push-repo-memory-${{ github.repository }}-[-…]" +// +// For memories that target a non-default repository, the target repo is prepended to the +// branch name (e.g., "other-owner/other-repo:memory/branch") so that distinct targets +// produce distinct concurrency groups. The branches are sorted for a deterministic key +// regardless of the order memories are declared in the frontmatter. +func buildPushRepoMemoryConcurrencyGroup(memories []RepoMemoryEntry) string { + branchKeys := make([]string, 0, len(memories)) + for _, m := range memories { + key := m.BranchName + if m.TargetRepo != "" { + key = m.TargetRepo + ":" + key + } + branchKeys = append(branchKeys, key) + } + sort.Strings(branchKeys) + return "push-repo-memory-${{ github.repository }}-" + strings.Join(branchKeys, "-") +} + // buildPushRepoMemoryJob creates a job that downloads repo-memory artifacts and pushes them to git branches // This job runs after the agent job completes (even if it fails) and requires contents: write permission // If threat detection is enabled, only runs if no threats were detected @@ -729,9 +754,12 @@ func (c *Compiler) buildPushRepoMemoryJob(data *WorkflowData, threatDetectionEna outputs["patch_size_exceeded_"+memory.ID] = fmt.Sprintf("${{ steps.%s.outputs.patch_size_exceeded }}", stepID) } - // Serialize all push_repo_memory jobs per repository to prevent concurrent git pushes - // cancel-in-progress is false so that updates from concurrent agents are queued, not dropped - concurrency := c.indentYAMLLines("concurrency:\n group: \"push-repo-memory-${{ github.repository }}\"\n cancel-in-progress: false", " ") + // Build a concurrency key scoped to the actual branches being written. + // This prevents false serialisation between workflows that push to different memory + // branches while still serialising concurrent pushes to the *same* branch. + // cancel-in-progress is false so queued pushes are not dropped. + concurrencyGroup := buildPushRepoMemoryConcurrencyGroup(data.RepoMemoryConfig.Memories) + concurrency := c.indentYAMLLines(fmt.Sprintf("concurrency:\n group: %q\n cancel-in-progress: false", concurrencyGroup), " ") job := &Job{ Name: "push_repo_memory", diff --git a/pkg/workflow/repo_memory_test.go b/pkg/workflow/repo_memory_test.go index 3f6ee3ae865..99b0ca7a856 100644 --- a/pkg/workflow/repo_memory_test.go +++ b/pkg/workflow/repo_memory_test.go @@ -1191,3 +1191,74 @@ func TestRepoMemoryNonWikiPushNoAllowedRepos(t *testing.T) { assert.NotContains(t, pushJobOutput, "REPO_MEMORY_ALLOWED_REPOS", "Non-wiki push step should not set REPO_MEMORY_ALLOWED_REPOS") } + +// TestBuildPushRepoMemoryConcurrencyGroup verifies that the concurrency group key is scoped +// to the actual branch targets, so workflows pushing to different memory branches do not +// contend with each other. +func TestBuildPushRepoMemoryConcurrencyGroup(t *testing.T) { + tests := []struct { + name string + memories []RepoMemoryEntry + expected string + }{ + { + name: "single memory uses branch name in key", + memories: []RepoMemoryEntry{ + {ID: "default", BranchName: "memory/daily-news"}, + }, + expected: "push-repo-memory-${{ github.repository }}-memory/daily-news", + }, + { + name: "multiple memories sorted for deterministic key", + memories: []RepoMemoryEntry{ + {ID: "b", BranchName: "memory/workflow-b"}, + {ID: "a", BranchName: "memory/workflow-a"}, + }, + expected: "push-repo-memory-${{ github.repository }}-memory/workflow-a-memory/workflow-b", + }, + { + name: "non-default target repo is prefixed to branch", + memories: []RepoMemoryEntry{ + {ID: "default", BranchName: "memory/shared", TargetRepo: "other-org/other-repo"}, + }, + expected: "push-repo-memory-${{ github.repository }}-other-org/other-repo:memory/shared", + }, + { + name: "mix of default and custom target repos", + memories: []RepoMemoryEntry{ + {ID: "local", BranchName: "memory/local"}, + {ID: "remote", BranchName: "memory/remote", TargetRepo: "other-org/other-repo"}, + }, + expected: "push-repo-memory-${{ github.repository }}-memory/local-other-org/other-repo:memory/remote", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildPushRepoMemoryConcurrencyGroup(tt.memories) + assert.Equal(t, tt.expected, got, "Concurrency group key should match expected value") + }) + } +} + +// TestPushRepoMemoryJobConcurrencyKey verifies that buildPushRepoMemoryJob sets a concurrency +// key scoped to the actual memory branch rather than a repo-wide key. +func TestPushRepoMemoryJobConcurrencyKey(t *testing.T) { + data := &WorkflowData{ + RepoMemoryConfig: &RepoMemoryConfig{ + Memories: []RepoMemoryEntry{ + {ID: "default", BranchName: "memory/my-workflow"}, + }, + }, + } + + compiler := NewCompiler() + pushJob, err := compiler.buildPushRepoMemoryJob(data, false) + require.NoError(t, err, "Should build push job without error") + require.NotNil(t, pushJob, "Should produce a push job") + + assert.Contains(t, pushJob.Concurrency, "memory/my-workflow", + "Concurrency key should include the memory branch name") + assert.NotContains(t, pushJob.Concurrency, "push-repo-memory-${{ github.repository }}\"", + "Concurrency key should not be the old repo-wide key") +} From 8add4b6db39fecf4c912afd42f3c157daf21ed6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:41:30 +0000 Subject: [PATCH 3/4] fix: use unambiguous pipe separator in repo-memory concurrency key Replace the hyphen separator between branch names in the concurrency group key with a pipe (|) character, which cannot appear in valid git branch names. This prevents accidental key collisions between workflows whose combined branch names could otherwise produce the same string (e.g., memory/foo-bar vs memory/foo + bar). Also update test expected values and add a test case explicitly demonstrating that hyphenated branch names produce unambiguous keys. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/bfc3a2b6-31fa-4406-9f29-97b0018892ae Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../agent-performance-analyzer.lock.yml | 2 +- .github/workflows/audit-workflows.lock.yml | 2 +- .../workflows/code-scanning-fixer.lock.yml | 2 +- .../workflows/copilot-agent-analysis.lock.yml | 2 +- .../copilot-cli-deep-research.lock.yml | 2 +- .../copilot-pr-nlp-analysis.lock.yml | 2 +- .../copilot-pr-prompt-analysis.lock.yml | 2 +- .../copilot-session-insights.lock.yml | 2 +- .../workflows/daily-cli-performance.lock.yml | 2 +- .github/workflows/daily-code-metrics.lock.yml | 2 +- .../daily-community-attribution.lock.yml | 2 +- .../daily-copilot-token-report.lock.yml | 2 +- .github/workflows/daily-news.lock.yml | 2 +- .../daily-testify-uber-super-expert.lock.yml | 2 +- .github/workflows/deep-report.lock.yml | 2 +- .github/workflows/delight.lock.yml | 2 +- .../developer-docs-consolidator.lock.yml | 2 +- .../workflows/discussion-task-miner.lock.yml | 2 +- .github/workflows/firewall-escape.lock.yml | 2 +- .../workflows/glossary-maintainer.lock.yml | 2 +- .github/workflows/metrics-collector.lock.yml | 2 +- .github/workflows/pr-triage-agent.lock.yml | 2 +- .../workflows/security-compliance.lock.yml | 2 +- .../workflows/technical-doc-writer.lock.yml | 2 +- .../weekly-blog-post-writer.lock.yml | 2 +- .../workflow-health-manager.lock.yml | 2 +- pkg/workflow/repo_memory.go | 12 ++++++----- pkg/workflow/repo_memory_test.go | 20 ++++++++++++++----- 28 files changed, 48 insertions(+), 36 deletions(-) diff --git a/.github/workflows/agent-performance-analyzer.lock.yml b/.github/workflows/agent-performance-analyzer.lock.yml index 20e1cce59ff..a1c62326c3e 100644 --- a/.github/workflows/agent-performance-analyzer.lock.yml +++ b/.github/workflows/agent-performance-analyzer.lock.yml @@ -1223,7 +1223,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/meta-orchestrators" + group: "push-repo-memory-${{ github.repository }}|memory/meta-orchestrators" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index c35a6d260d4..cf3608f4cc0 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -1302,7 +1302,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/audit-workflows" + group: "push-repo-memory-${{ github.repository }}|memory/audit-workflows" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 188c7aaf0d4..5e60fd3fea7 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -1173,7 +1173,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/campaigns" + group: "push-repo-memory-${{ github.repository }}|memory/campaigns" cancel-in-progress: false outputs: patch_size_exceeded_campaigns: ${{ steps.push_repo_memory_campaigns.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index b0331e35495..e8a1c1e0a5b 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -1185,7 +1185,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/copilot-agent-analysis" + group: "push-repo-memory-${{ github.repository }}|memory/copilot-agent-analysis" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-cli-deep-research.lock.yml b/.github/workflows/copilot-cli-deep-research.lock.yml index 8a64631f4d0..d1f5f3c217b 100644 --- a/.github/workflows/copilot-cli-deep-research.lock.yml +++ b/.github/workflows/copilot-cli-deep-research.lock.yml @@ -1083,7 +1083,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/copilot-cli-research" + group: "push-repo-memory-${{ github.repository }}|memory/copilot-cli-research" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index 4d3eb50bd99..5d7caa8a9b8 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -1180,7 +1180,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/nlp-analysis" + group: "push-repo-memory-${{ github.repository }}|memory/nlp-analysis" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index c4eb46b2215..2e541a6c32f 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -1116,7 +1116,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/prompt-analysis" + group: "push-repo-memory-${{ github.repository }}|memory/prompt-analysis" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 15ca47f11a4..b06cd86050a 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -1248,7 +1248,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/session-insights" + group: "push-repo-memory-${{ github.repository }}|memory/session-insights" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-cli-performance.lock.yml b/.github/workflows/daily-cli-performance.lock.yml index 7b76ad03f51..f07f034f603 100644 --- a/.github/workflows/daily-cli-performance.lock.yml +++ b/.github/workflows/daily-cli-performance.lock.yml @@ -1329,7 +1329,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/cli-performance" + group: "push-repo-memory-${{ github.repository }}|memory/cli-performance" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index b13fb81e61f..8535246494a 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -1223,7 +1223,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-daily/daily-code-metrics" + group: "push-repo-memory-${{ github.repository }}|daily/daily-code-metrics" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-community-attribution.lock.yml b/.github/workflows/daily-community-attribution.lock.yml index ba882016392..55e34c31546 100644 --- a/.github/workflows/daily-community-attribution.lock.yml +++ b/.github/workflows/daily-community-attribution.lock.yml @@ -1129,7 +1129,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-master" + group: "push-repo-memory-${{ github.repository }}|master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml index 89c63cbf18a..65c334493c5 100644 --- a/.github/workflows/daily-copilot-token-report.lock.yml +++ b/.github/workflows/daily-copilot-token-report.lock.yml @@ -1185,7 +1185,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/token-metrics" + group: "push-repo-memory-${{ github.repository }}|memory/token-metrics" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index d6003827d6f..50f6b382d95 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -1256,7 +1256,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/daily-news" + group: "push-repo-memory-${{ github.repository }}|memory/daily-news" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/daily-testify-uber-super-expert.lock.yml b/.github/workflows/daily-testify-uber-super-expert.lock.yml index 5701a892c42..f3554d0153a 100644 --- a/.github/workflows/daily-testify-uber-super-expert.lock.yml +++ b/.github/workflows/daily-testify-uber-super-expert.lock.yml @@ -1212,7 +1212,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/testify-expert" + group: "push-repo-memory-${{ github.repository }}|memory/testify-expert" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 348152d8a57..a99a7afcb9c 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -1284,7 +1284,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/deep-report" + group: "push-repo-memory-${{ github.repository }}|memory/deep-report" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/delight.lock.yml b/.github/workflows/delight.lock.yml index ed3a50cad59..3198f00a6d1 100644 --- a/.github/workflows/delight.lock.yml +++ b/.github/workflows/delight.lock.yml @@ -1128,7 +1128,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/delight" + group: "push-repo-memory-${{ github.repository }}|memory/delight" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 1b99241b2a6..4fd925f0adc 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -1453,7 +1453,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-master" + group: "push-repo-memory-${{ github.repository }}|master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/discussion-task-miner.lock.yml b/.github/workflows/discussion-task-miner.lock.yml index bd54e11600e..9a300321e88 100644 --- a/.github/workflows/discussion-task-miner.lock.yml +++ b/.github/workflows/discussion-task-miner.lock.yml @@ -1111,7 +1111,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/discussion-task-miner" + group: "push-repo-memory-${{ github.repository }}|memory/discussion-task-miner" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/firewall-escape.lock.yml b/.github/workflows/firewall-escape.lock.yml index 19c2b7f676f..bc6c82a5229 100644 --- a/.github/workflows/firewall-escape.lock.yml +++ b/.github/workflows/firewall-escape.lock.yml @@ -1186,7 +1186,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/firewall-escape" + group: "push-repo-memory-${{ github.repository }}|memory/firewall-escape" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index ed70c79fdaf..39b0d2dc287 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -1348,7 +1348,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-master" + group: "push-repo-memory-${{ github.repository }}|master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/metrics-collector.lock.yml b/.github/workflows/metrics-collector.lock.yml index a18f0f51933..cc284f308da 100644 --- a/.github/workflows/metrics-collector.lock.yml +++ b/.github/workflows/metrics-collector.lock.yml @@ -685,7 +685,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/meta-orchestrators" + group: "push-repo-memory-${{ github.repository }}|memory/meta-orchestrators" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/pr-triage-agent.lock.yml b/.github/workflows/pr-triage-agent.lock.yml index 0339dfc5618..231bebb9724 100644 --- a/.github/workflows/pr-triage-agent.lock.yml +++ b/.github/workflows/pr-triage-agent.lock.yml @@ -1097,7 +1097,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/pr-triage" + group: "push-repo-memory-${{ github.repository }}|memory/pr-triage" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/security-compliance.lock.yml b/.github/workflows/security-compliance.lock.yml index 8050ce147d3..8f1a20cd91f 100644 --- a/.github/workflows/security-compliance.lock.yml +++ b/.github/workflows/security-compliance.lock.yml @@ -1076,7 +1076,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/campaigns" + group: "push-repo-memory-${{ github.repository }}|memory/campaigns" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 7d5b8e23609..4cf58be0ad5 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -1352,7 +1352,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-master" + group: "push-repo-memory-${{ github.repository }}|master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/weekly-blog-post-writer.lock.yml b/.github/workflows/weekly-blog-post-writer.lock.yml index 1d81196ec49..44f48bda708 100644 --- a/.github/workflows/weekly-blog-post-writer.lock.yml +++ b/.github/workflows/weekly-blog-post-writer.lock.yml @@ -1314,7 +1314,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-master" + group: "push-repo-memory-${{ github.repository }}|master" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/.github/workflows/workflow-health-manager.lock.yml b/.github/workflows/workflow-health-manager.lock.yml index 55867a5f85d..eceac1c57ad 100644 --- a/.github/workflows/workflow-health-manager.lock.yml +++ b/.github/workflows/workflow-health-manager.lock.yml @@ -1178,7 +1178,7 @@ jobs: permissions: contents: write concurrency: - group: "push-repo-memory-${{ github.repository }}-memory/meta-orchestrators" + group: "push-repo-memory-${{ github.repository }}|memory/meta-orchestrators" cancel-in-progress: false outputs: patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} diff --git a/pkg/workflow/repo_memory.go b/pkg/workflow/repo_memory.go index 76909729167..4f8e102a2fa 100644 --- a/pkg/workflow/repo_memory.go +++ b/pkg/workflow/repo_memory.go @@ -553,11 +553,13 @@ func generateRepoMemorySteps(builder *strings.Builder, data *WorkflowData) { // write targets—rather than a single repo-wide key—ensures that workflows pushing to // different memory branches do not unnecessarily serialise or cancel each other. // -// Key format: "push-repo-memory-${{ github.repository }}-[-…]" +// Key format: "push-repo-memory-${{ github.repository }}|[|…]" // -// For memories that target a non-default repository, the target repo is prepended to the -// branch name (e.g., "other-owner/other-repo:memory/branch") so that distinct targets -// produce distinct concurrency groups. The branches are sorted for a deterministic key +// The "|" separator is used because it cannot appear in valid git branch names, making +// the key unambiguous even when branch names contain hyphens or slashes. For memories +// that target a non-default repository, the target repo is prepended to the branch name +// (e.g., "other-owner/other-repo:memory/branch") so that distinct targets produce +// distinct concurrency groups. The branches are sorted for a deterministic key // regardless of the order memories are declared in the frontmatter. func buildPushRepoMemoryConcurrencyGroup(memories []RepoMemoryEntry) string { branchKeys := make([]string, 0, len(memories)) @@ -569,7 +571,7 @@ func buildPushRepoMemoryConcurrencyGroup(memories []RepoMemoryEntry) string { branchKeys = append(branchKeys, key) } sort.Strings(branchKeys) - return "push-repo-memory-${{ github.repository }}-" + strings.Join(branchKeys, "-") + return "push-repo-memory-${{ github.repository }}|" + strings.Join(branchKeys, "|") } // buildPushRepoMemoryJob creates a job that downloads repo-memory artifacts and pushes them to git branches diff --git a/pkg/workflow/repo_memory_test.go b/pkg/workflow/repo_memory_test.go index 99b0ca7a856..d330d66d866 100644 --- a/pkg/workflow/repo_memory_test.go +++ b/pkg/workflow/repo_memory_test.go @@ -1206,7 +1206,7 @@ func TestBuildPushRepoMemoryConcurrencyGroup(t *testing.T) { memories: []RepoMemoryEntry{ {ID: "default", BranchName: "memory/daily-news"}, }, - expected: "push-repo-memory-${{ github.repository }}-memory/daily-news", + expected: "push-repo-memory-${{ github.repository }}|memory/daily-news", }, { name: "multiple memories sorted for deterministic key", @@ -1214,14 +1214,14 @@ func TestBuildPushRepoMemoryConcurrencyGroup(t *testing.T) { {ID: "b", BranchName: "memory/workflow-b"}, {ID: "a", BranchName: "memory/workflow-a"}, }, - expected: "push-repo-memory-${{ github.repository }}-memory/workflow-a-memory/workflow-b", + expected: "push-repo-memory-${{ github.repository }}|memory/workflow-a|memory/workflow-b", }, { name: "non-default target repo is prefixed to branch", memories: []RepoMemoryEntry{ {ID: "default", BranchName: "memory/shared", TargetRepo: "other-org/other-repo"}, }, - expected: "push-repo-memory-${{ github.repository }}-other-org/other-repo:memory/shared", + expected: "push-repo-memory-${{ github.repository }}|other-org/other-repo:memory/shared", }, { name: "mix of default and custom target repos", @@ -1229,7 +1229,16 @@ func TestBuildPushRepoMemoryConcurrencyGroup(t *testing.T) { {ID: "local", BranchName: "memory/local"}, {ID: "remote", BranchName: "memory/remote", TargetRepo: "other-org/other-repo"}, }, - expected: "push-repo-memory-${{ github.repository }}-memory/local-other-org/other-repo:memory/remote", + expected: "push-repo-memory-${{ github.repository }}|memory/local|other-org/other-repo:memory/remote", + }, + { + name: "branches with hyphens produce unambiguous keys", + memories: []RepoMemoryEntry{ + {ID: "a", BranchName: "memory/workflow-a"}, + {ID: "b", BranchName: "memory/workflow-b"}, + }, + // "|" separator ensures "memory/workflow-a|memory/workflow-b" ≠ "memory/workflow-a-b" + expected: "push-repo-memory-${{ github.repository }}|memory/workflow-a|memory/workflow-b", }, } @@ -1259,6 +1268,7 @@ func TestPushRepoMemoryJobConcurrencyKey(t *testing.T) { assert.Contains(t, pushJob.Concurrency, "memory/my-workflow", "Concurrency key should include the memory branch name") + // Ensure the old repo-wide-only key is not used assert.NotContains(t, pushJob.Concurrency, "push-repo-memory-${{ github.repository }}\"", - "Concurrency key should not be the old repo-wide key") + "Concurrency key should not be the old repo-wide-only key format") } From 023e7a0b34ea004c15a6b5b9fcc22b8251460a14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:02:44 +0000 Subject: [PATCH 4/4] fix: percent-encode key components to make pipe separator unambiguous The "|" separator used between branch name components in the concurrency group key is not actually forbidden by git check-ref-format, so a user-supplied explicit branch-name could contain "|" and produce an ambiguous key. Introduce encodeConcurrencyKeyPart() which percent-encodes "%" (to avoid double-encoding) and "|" before the components are joined. Normal branch names (auto-generated or using plain alphanumeric/hyphen characters) are unaffected since they contain neither character. Also rename the "branches with hyphens produce unambiguous keys" test to "branches with hyphens use pipe separator in key" and update its comment to describe the current encoding strategy rather than making an incorrect claim about git ref format rules. Add a new test case that explicitly verifies "|" in a branch name is encoded as "%7C". Agent-Logs-Url: https://github.com/github/gh-aw/sessions/903fc18e-9422-49d3-93a3-dea8cb70e5e5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/repo_memory.go | 28 +++++++++++++++++++--------- pkg/workflow/repo_memory_test.go | 13 +++++++++++-- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/pkg/workflow/repo_memory.go b/pkg/workflow/repo_memory.go index 4f8e102a2fa..8024fd8cb12 100644 --- a/pkg/workflow/repo_memory.go +++ b/pkg/workflow/repo_memory.go @@ -553,20 +553,21 @@ func generateRepoMemorySteps(builder *strings.Builder, data *WorkflowData) { // write targets—rather than a single repo-wide key—ensures that workflows pushing to // different memory branches do not unnecessarily serialise or cancel each other. // -// Key format: "push-repo-memory-${{ github.repository }}|[|…]" +// Key format: "push-repo-memory-${{ github.repository }}|[|…]" // -// The "|" separator is used because it cannot appear in valid git branch names, making -// the key unambiguous even when branch names contain hyphens or slashes. For memories -// that target a non-default repository, the target repo is prepended to the branch name -// (e.g., "other-owner/other-repo:memory/branch") so that distinct targets produce -// distinct concurrency groups. The branches are sorted for a deterministic key -// regardless of the order memories are declared in the frontmatter. +// Each key component is percent-encoded (only `%` and `|` are encoded) before joining +// with "|", so the separator is always unambiguous even if a user-supplied branch name +// or target-repo contains a literal "|". For memories that target a non-default +// repository, the target repo is prepended to the branch name +// (e.g., "other-owner%2Fother-repo:memory%2Fbranch" would be encoded if needed) so that +// distinct targets produce distinct concurrency groups. The branches are sorted for a +// deterministic key regardless of the order memories are declared in the frontmatter. func buildPushRepoMemoryConcurrencyGroup(memories []RepoMemoryEntry) string { branchKeys := make([]string, 0, len(memories)) for _, m := range memories { - key := m.BranchName + key := encodeConcurrencyKeyPart(m.BranchName) if m.TargetRepo != "" { - key = m.TargetRepo + ":" + key + key = encodeConcurrencyKeyPart(m.TargetRepo) + ":" + key } branchKeys = append(branchKeys, key) } @@ -574,6 +575,15 @@ func buildPushRepoMemoryConcurrencyGroup(memories []RepoMemoryEntry) string { return "push-repo-memory-${{ github.repository }}|" + strings.Join(branchKeys, "|") } +// encodeConcurrencyKeyPart percent-encodes the characters that would otherwise make the +// concurrency group key ambiguous: "%" (to avoid double-encoding) and "|" (the separator). +// All other characters are left as-is so the key remains human-readable in workflow UIs. +func encodeConcurrencyKeyPart(s string) string { + s = strings.ReplaceAll(s, "%", "%25") + s = strings.ReplaceAll(s, "|", "%7C") + return s +} + // buildPushRepoMemoryJob creates a job that downloads repo-memory artifacts and pushes them to git branches // This job runs after the agent job completes (even if it fails) and requires contents: write permission // If threat detection is enabled, only runs if no threats were detected diff --git a/pkg/workflow/repo_memory_test.go b/pkg/workflow/repo_memory_test.go index d330d66d866..54e89ec12d6 100644 --- a/pkg/workflow/repo_memory_test.go +++ b/pkg/workflow/repo_memory_test.go @@ -1232,14 +1232,23 @@ func TestBuildPushRepoMemoryConcurrencyGroup(t *testing.T) { expected: "push-repo-memory-${{ github.repository }}|memory/local|other-org/other-repo:memory/remote", }, { - name: "branches with hyphens produce unambiguous keys", + name: "branches with hyphens use pipe separator in key", memories: []RepoMemoryEntry{ {ID: "a", BranchName: "memory/workflow-a"}, {ID: "b", BranchName: "memory/workflow-b"}, }, - // "|" separator ensures "memory/workflow-a|memory/workflow-b" ≠ "memory/workflow-a-b" + // This expectation documents the current use of "|" as the list separator in the key. + // If the concurrency key encoding changes (e.g., different delimiter or encoding scheme), + // update this test to match the new encoding strategy. expected: "push-repo-memory-${{ github.repository }}|memory/workflow-a|memory/workflow-b", }, + { + name: "pipe in branch name is percent-encoded so the separator stays unambiguous", + memories: []RepoMemoryEntry{ + {ID: "unusual", BranchName: "memory/foo|bar"}, + }, + expected: "push-repo-memory-${{ github.repository }}|memory/foo%7Cbar", + }, } for _, tt := range tests {