Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/issue-duplication-detector.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions scripts/ci/postprocess-smoke-workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,64 @@ describe('copilotModelEmptyFallbackRegex', () => {
expect(result).toBe(input);
});
});

// ── Issue duplication detector conclusion concurrency tests ───────────────────
// Mirrors the patterns in postprocess-smoke-workflows.ts.
// If those patterns change, these tests will catch regressions.

const issueDuplicationConclusionConcurrencyRegex =
/([ ]+group: "gh-aw-conclusion-issue-duplication-detector)("\n[ ]+cancel-in-progress: false)/;
const issueDuplicationConclusionConcurrencySentinel =
'gh-aw-conclusion-issue-duplication-detector-${{ github.event.issue.number';

describe('issueDuplicationConclusionConcurrencyRegex', () => {
const ORIGINAL_CONCURRENCY =
' concurrency:\n' +
' group: "gh-aw-conclusion-issue-duplication-detector"\n' +
' cancel-in-progress: false\n';

it('should match the compiler-generated shared conclusion concurrency group', () => {
expect(issueDuplicationConclusionConcurrencyRegex.test(ORIGINAL_CONCURRENCY)).toBe(true);
});

it('should transform the group to include the issue number', () => {
const result = ORIGINAL_CONCURRENCY.replace(
issueDuplicationConclusionConcurrencyRegex,
`$1-\${{ github.event.issue.number || github.run_id }}$2`
);
expect(result).toContain('${{ github.event.issue.number || github.run_id }}');
expect(result).toContain('cancel-in-progress: false');
expect(result).not.toContain(
'"gh-aw-conclusion-issue-duplication-detector"\n'
);
});

it('should NOT match already-per-issue group (idempotency via sentinel)', () => {
const alreadyUpdated =
' concurrency:\n' +
' group: "gh-aw-conclusion-issue-duplication-detector-${{ github.event.issue.number || github.run_id }}"\n' +
' cancel-in-progress: false\n';
// The sentinel string is present in the already-updated content, so the
// postprocess script skips the transform. Additionally, the regex itself
// does NOT match the updated form because the closing quote is no longer
// immediately after "issue-duplication-detector" — both guards agree.
expect(alreadyUpdated.includes(issueDuplicationConclusionConcurrencySentinel)).toBe(true);
expect(issueDuplicationConclusionConcurrencyRegex.test(alreadyUpdated)).toBe(false);
});

it('should preserve cancel-in-progress: false in the output', () => {
const result = ORIGINAL_CONCURRENCY.replace(
issueDuplicationConclusionConcurrencyRegex,
`$1-\${{ github.event.issue.number || github.run_id }}$2`
);
expect(result).toContain('cancel-in-progress: false');
});

it('should keep the workflow name prefix in the group', () => {
const result = ORIGINAL_CONCURRENCY.replace(
issueDuplicationConclusionConcurrencyRegex,
`$1-\${{ github.event.issue.number || github.run_id }}$2`
);
expect(result).toContain('gh-aw-conclusion-issue-duplication-detector-');
});
});
44 changes: 44 additions & 0 deletions scripts/ci/postprocess-smoke-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,20 @@ const cacheRestoreKeyPrefixRegex =
/(memory-none-nopolicy-(?:\$\{\{ env\.GH_AW_WORKFLOW_ID_SANITIZED \}\}|[a-z0-9-]+)-)(\n)/g;
const cacheDateRestoreKeySentinel = 'env.CACHE_MEMORY_DATE }}';

// Fix for issue-duplication-detector.lock.yml: make the conclusion job's
// concurrency group per-issue instead of per-workflow. Without this, when
// multiple issues are opened simultaneously (batch triggers), all conclusion
// jobs queue in the same single-slot group. GitHub Actions allows only one
// pending run per group; a third arriving cancels the current pending one,
// causing 40%+ error rates in busy periods.
//
// Change: "gh-aw-conclusion-issue-duplication-detector"
// → "gh-aw-conclusion-issue-duplication-detector-${{ github.event.issue.number || github.run_id }}"
const issueDuplicationConclusionConcurrencyRegex =
/([ ]+group: "gh-aw-conclusion-issue-duplication-detector)("\n[ ]+cancel-in-progress: false)/;
const issueDuplicationConclusionConcurrencySentinel =
'gh-aw-conclusion-issue-duplication-detector-${{ github.event.issue.number';

// Builds the YAML for the "Strip execute bits" step.
function buildStripExecBitsStep(indent: string): string {
const i = indent;
Expand Down Expand Up @@ -422,6 +436,36 @@ for (const workflowPath of workflowPaths) {
console.log(` 'Copy Copilot session state' step already updated`);
}

// For issue-duplication-detector: scope the conclusion job's concurrency
// group to the triggering issue number so that concurrent runs for different
// issues don't block each other's conclusion jobs. The compiler generates a
// single shared group ("gh-aw-conclusion-issue-duplication-detector") which
// causes conclusion jobs to queue in a 1-slot group; when more than two
// complete simultaneously the pending job is cancelled by the next arrival.
const isIssueDuplicationDetector = workflowPath.includes(
'issue-duplication-detector.lock.yml'
);
if (isIssueDuplicationDetector) {
if (!content.includes(issueDuplicationConclusionConcurrencySentinel)) {
const concMatch = content.match(issueDuplicationConclusionConcurrencyRegex);
if (concMatch) {
content = content.replace(
issueDuplicationConclusionConcurrencyRegex,
`$1-\${{ github.event.issue.number || github.run_id }}$2`
);
modified = true;
console.log(` Scoped conclusion concurrency group to per-issue for issue-duplication-detector`);
} else {
console.warn(
` WARNING: Could not find conclusion concurrency group in issue-duplication-detector. ` +
`The compiled lock file may have changed structure. Manual review required.`
);
}
} else {
console.log(` Conclusion concurrency group already per-issue for issue-duplication-detector`);
}
}

// Exclude unused Playwright/browser tools from Copilot CLI for smoke-copilot.
// The Copilot CLI includes 21 built-in browser_* tools when --allow-all-tools is set.
// These tools are never used in smoke-copilot but add ~10,500 tokens/turn of dead weight.
Expand Down
Loading