Skip to content

Commit 731686b

Browse files
authored
Merge branch 'main' into copilot/issue-intents-runtime-feature
2 parents d08ca84 + e33e3e5 commit 731686b

8 files changed

Lines changed: 353 additions & 107 deletions

File tree

CONTRIBUTING.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,9 @@ This project follows the GitHub Community Guidelines. Please be respectful and i
555555
556556
Releases are defined in `.github/workflows/release.md` and triggered from the compiled GitHub Actions workflow.
557557

558-
The team follows semantic versioning on a best-effort basis.
558+
The team follows a **weekly or bi-weekly minor release cadence**, similar to VS Code's release practices. Version numbers increment the minor component on each release cycle — not on the basis of change scope. Patch releases are reserved for urgent fixes between cycles; major releases are used for significant breaking changes only.
559559

560-
> **Note:** The release workflow publishes the new version as a **prerelease** on GitHub with `latest=false`. Prereleases are floated for a few days. On Monday, maintainers promote the last known good prerelease to stable so `latest` resolves to that release.
560+
> **Note:** The release workflow publishes the new version as a **prerelease** on GitHub with `latest=false`. Prereleases are floated for a few days. On Monday, maintainers promote the last known good prerelease to stable so `latest` resolves to that release. Immediately after promotion, a new minor pre-release is kicked off to start the next cycle.
561561
562562
### Steps
563563

@@ -589,10 +589,17 @@ The team follows semantic versioning on a best-effort basis.
589589

590590
Users who install with `version: latest` (the default) will now receive the new release.
591591

592+
5. **Start the next release cycle** _(immediately after promotion)_
593+
594+
Following the weekly/bi-weekly cadence, kick off a new `minor` release right after promoting the previous one to stable. Repeat steps 1–3 to publish it as a prerelease. This prerelease then floats until the next Monday, when it becomes the new stable release.
595+
596+
> [!TIP]
597+
> Always select `minor` when starting a new cycle. Use `patch` only for urgent fixes within a cycle, and `major` only for significant breaking changes.
598+
592599
### Summary
593600

594601
```
595-
Launch release action
602+
Launch release action (minor)
596603
597604
598605
Workflow pushes tag & pauses
@@ -609,11 +616,14 @@ Approve the gh-aw-actions-release environment gate
609616
610617
Release published as prerelease 🎉
611618
612-
▼ (manual)
619+
▼ (Monday — manual)
613620
Promote prerelease → full release on GitHub Releases page
614621
615622
616623
'latest' now resolves to the new version ✅
624+
625+
▼ (same day — start next cycle)
626+
Launch next minor release action → new prerelease published
617627
```
618628

619629
## 🎯 Why This Contribution Model?

actions/setup/js/assign_agent_helpers.cjs

Lines changed: 143 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,77 @@ const { getErrorMessage } = require("./error_helpers.cjs");
1010
*/
1111

1212
/**
13-
* Map agent names to their GitHub bot login names
14-
* @type {Record<string, string>}
13+
* Map agent names to their GitHub bot login aliases.
14+
* Keep the most common/current alias first so logs have a stable primary name.
15+
* @type {Record<string, string[]>}
1516
*/
1617
const AGENT_LOGIN_NAMES = {
17-
copilot: "copilot-swe-agent",
18+
copilot: ["copilot-swe-agent", "github-copilot-enterprise", "github-copilot-enterprise[bot]", "github-copilot", "github-copilot[bot]"],
1819
};
1920

21+
/**
22+
* Normalize a GitHub login for internal matching.
23+
* @param {string} login
24+
* @returns {string}
25+
*/
26+
function normalizeLogin(login) {
27+
return login.startsWith("@") ? login.slice(1) : login;
28+
}
29+
30+
/**
31+
* Reverse lookup of assignee aliases to canonical agent names.
32+
* @type {Record<string, string>}
33+
*/
34+
const AGENT_NAME_BY_LOGIN = Object.fromEntries(Object.entries(AGENT_LOGIN_NAMES).flatMap(([agentName, logins]) => logins.map(login => [normalizeLogin(login), agentName])));
35+
36+
/**
37+
* GitHub can surface bots either via type="Bot" or a [bot] login suffix.
38+
* Check both because assignee responses are not always consistent across endpoints.
39+
* @param {{login?: string, type?: string}|null|undefined} assignee
40+
* @returns {boolean}
41+
*/
42+
function isBotAssignee(assignee) {
43+
return assignee?.type === "Bot" || Boolean(assignee?.login?.endsWith("[bot]"));
44+
}
45+
46+
/**
47+
* Return the known GitHub login aliases for an agent.
48+
* @param {string} agentName
49+
* @returns {string[]}
50+
*/
51+
function getAgentLogins(agentName) {
52+
const logins = AGENT_LOGIN_NAMES[agentName];
53+
if (!logins) return [];
54+
return logins;
55+
}
56+
2057
/**
2158
* Check if an assignee is a known coding agent (bot)
2259
* @param {string} assignee - Assignee name (may include @ prefix)
2360
* @returns {string|null} Agent name if it's a known agent, null otherwise
2461
*/
2562
function getAgentName(assignee) {
2663
// Normalize: remove @ prefix if present
27-
const normalized = assignee.startsWith("@") ? assignee.slice(1) : assignee;
64+
const normalized = normalizeLogin(assignee);
2865

2966
// Check if it's a known agent
3067
if (AGENT_LOGIN_NAMES[normalized]) {
3168
return normalized;
3269
}
33-
34-
return null;
70+
return AGENT_NAME_BY_LOGIN[normalized] || null;
3571
}
3672

3773
/**
3874
* Return list of coding agent bot login names that are currently available as assignable actors
39-
* (intersection of suggestedActors and known AGENT_LOGIN_NAMES values)
75+
* in this repository, as determined by checkUserCanBeAssigned.
4076
* @param {string} owner
4177
* @param {string} repo
4278
* @param {Object} [githubClient] - Authenticated GitHub client (defaults to global github)
4379
* @returns {Promise<string[]>}
4480
*/
4581
async function getAvailableAgentLogins(owner, repo, githubClient = github) {
46-
const knownValues = Object.values(AGENT_LOGIN_NAMES);
82+
// Deduplicate defensively so future alias additions across agents do not duplicate REST lookups.
83+
const knownValues = [...new Set(Object.values(AGENT_LOGIN_NAMES).flat())];
4784
const available = [];
4885
for (const login of knownValues) {
4986
try {
@@ -63,6 +100,46 @@ async function getAvailableAgentLogins(owner, repo, githubClient = github) {
63100
return available.sort();
64101
}
65102

103+
/**
104+
* Return assignable bot logins from the repository assignee list.
105+
* @param {string} owner
106+
* @param {string} repo
107+
* @param {Object} [githubClient]
108+
* @returns {Promise<string[]>}
109+
*/
110+
async function getAssignableBots(owner, repo, githubClient = github) {
111+
try {
112+
const assignees = [];
113+
let page = 1;
114+
let pageData = [];
115+
const MAX_PAGES = 5; // Limit to 5 pages (500 assignees) to bound API calls on large repositories
116+
117+
do {
118+
const response = await githubClient.rest.issues.listAssignees({
119+
owner,
120+
repo,
121+
per_page: 100,
122+
page,
123+
});
124+
pageData = Array.isArray(response.data) ? response.data : [];
125+
assignees.push(...pageData);
126+
page++;
127+
} while (pageData.length === 100 && page <= MAX_PAGES);
128+
129+
return [
130+
...new Set(
131+
assignees
132+
.filter(isBotAssignee)
133+
.map(assignee => assignee.login)
134+
.filter(Boolean)
135+
),
136+
].sort();
137+
} catch (error) {
138+
core.debug(`Failed to list assignable bots for ${owner}/${repo}: ${getErrorMessage(error)}`);
139+
return [];
140+
}
141+
}
142+
66143
/**
67144
* Find an agent that can be assigned in the repository using REST
68145
* @param {string} owner - Repository owner
@@ -72,44 +149,66 @@ async function getAvailableAgentLogins(owner, repo, githubClient = github) {
72149
* @returns {Promise<string|null>} Agent ID or null if not found
73150
*/
74151
async function findAgent(owner, repo, agentName, githubClient = github) {
75-
const loginName = AGENT_LOGIN_NAMES[agentName];
76-
if (!loginName) {
152+
const loginNames = getAgentLogins(agentName);
153+
if (loginNames.length === 0) {
77154
core.error(`Unknown agent: ${agentName}. Supported agents: ${Object.keys(AGENT_LOGIN_NAMES).join(", ")}`);
78155
return null;
79156
}
80157

81-
try {
82-
await githubClient.rest.issues.checkUserCanBeAssigned({
83-
owner,
84-
repo,
85-
assignee: loginName,
86-
});
87-
const { data: agentUser } = await githubClient.rest.users.getByUsername({ username: loginName });
88-
return String(agentUser.id);
89-
} catch (error) {
90-
const errorMessage = getErrorMessage(error);
91-
core.error(`Failed to find ${agentName} agent: ${errorMessage}`);
92-
if (
93-
errorMessage.includes("Bad credentials") ||
94-
errorMessage.includes("Not Authenticated") ||
95-
errorMessage.includes("Resource not accessible") ||
96-
errorMessage.includes("Insufficient permissions") ||
97-
errorMessage.includes("requires authentication")
98-
) {
99-
throw error;
100-
}
101-
const available = await getAvailableAgentLogins(owner, repo, githubClient);
102-
core.warning(`${agentName} coding agent (${loginName}) is not available as an assignee for this repository`);
103-
if (available.length > 0) {
104-
core.info(`Available assignable coding agents: ${available.join(", ")}`);
105-
} else {
106-
core.info("No coding agents are currently assignable in this repository.");
158+
core.info(`Trying ${loginNames.length} ${agentName} assignee aliases: ${loginNames.join(", ")}`);
159+
160+
const aliasFailures = [];
161+
for (const loginName of loginNames) {
162+
try {
163+
core.info(`Checking assignee alias: ${loginName}`);
164+
await githubClient.rest.issues.checkUserCanBeAssigned({
165+
owner,
166+
repo,
167+
assignee: loginName,
168+
});
169+
} catch (checkError) {
170+
const errorMessage = getErrorMessage(checkError);
171+
const status = checkError?.status;
172+
const statusLabel = status ? ` (${status})` : "";
173+
aliasFailures.push(`${loginName}${statusLabel}: ${errorMessage}`);
174+
if (
175+
errorMessage.includes("Bad credentials") ||
176+
errorMessage.includes("Not Authenticated") ||
177+
errorMessage.includes("Resource not accessible") ||
178+
errorMessage.includes("Insufficient permissions") ||
179+
errorMessage.includes("requires authentication")
180+
) {
181+
core.error(`Failed to check assignee alias ${loginName} for ${agentName}: ${errorMessage}`);
182+
throw checkError;
183+
}
184+
core.info(`Assignee alias ${loginName} was not assignable: ${errorMessage}`);
185+
continue;
107186
}
108-
if (agentName === "copilot") {
109-
core.info("Please visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot");
187+
// Alias confirmed assignable — resolve the user ID separately
188+
try {
189+
const { data: agentUser } = await githubClient.rest.users.getByUsername({ username: loginName });
190+
core.info(`Resolved ${agentName} agent via assignee alias ${loginName}`);
191+
return String(agentUser.id);
192+
} catch (lookupError) {
193+
core.warning(`Alias ${loginName} is assignable but user lookup failed: ${getErrorMessage(lookupError)}`);
110194
}
111-
return null;
112195
}
196+
197+
const bots = await getAssignableBots(owner, repo, githubClient);
198+
core.warning(`${agentName} coding agent aliases are not available as assignees for this repository`);
199+
core.info(`Assignee aliases tried: ${loginNames.join(", ")}`);
200+
if (aliasFailures.length > 0) {
201+
core.info(`Alias lookup results: ${aliasFailures.join(" | ")}`);
202+
}
203+
if (bots.length > 0) {
204+
core.info(`Assignable bots in this repository: ${bots.join(", ")}`);
205+
} else {
206+
core.info("No assignable bots found in this repository.");
207+
}
208+
if (agentName === "copilot") {
209+
core.info("Please visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot");
210+
}
211+
return null;
113212
}
114213

115214
/**
@@ -439,11 +538,7 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) {
439538
core.info(`Looking for ${agentName} coding agent...`);
440539
const agentId = await findAgent(owner, repo, agentName);
441540
if (!agentId) {
442-
const error = `${agentName} coding agent is not available for this repository`;
443-
// Enrich with available agent logins
444-
const available = await getAvailableAgentLogins(owner, repo);
445-
const enrichedError = available.length > 0 ? `${error} (available agents: ${available.join(", ")})` : error;
446-
return { success: false, error: enrichedError };
541+
return { success: false, error: `${agentName} coding agent is not available for this repository` };
447542
}
448543
core.info(`Found ${agentName} coding agent (ID: ${agentId})`);
449544

@@ -457,7 +552,8 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) {
457552
core.info(`Issue context: ${issueDetails.issueId}`);
458553

459554
// Check if agent is already assigned
460-
if (issueDetails.currentAssignees.some(a => a.id === agentId || a.login === AGENT_LOGIN_NAMES[agentName])) {
555+
const knownLogins = getAgentLogins(agentName);
556+
if (issueDetails.currentAssignees.some(a => a.id === agentId || knownLogins.includes(a.login))) {
461557
core.info(`${agentName} is already assigned to issue #${issueNumber}`);
462558
return { success: true };
463559
}
@@ -481,7 +577,9 @@ async function assignAgentToIssueByName(owner, repo, issueNumber, agentName) {
481577
module.exports = {
482578
AGENT_LOGIN_NAMES,
483579
getAgentName,
580+
getAgentLogins,
484581
getAvailableAgentLogins,
582+
getAssignableBots,
485583
findAgent,
486584
getIssueDetails,
487585
getPullRequestDetails,

0 commit comments

Comments
 (0)