Skip to content

Commit 86a6faa

Browse files
Merge main, resolve extension.yaml conflict (take main's version + requiredAzdVersion)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents fe4304a + 5f49219 commit 86a6faa

84 files changed

Lines changed: 7113 additions & 283 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
44
"features": {
55
"ghcr.io/devcontainers/features/go:1": {
6-
"version": "1.26"
6+
"version": "1.26.1"
77
},
88
"ghcr.io/devcontainers/features/docker-in-docker:2.11.0": {
99
"version": "latest",
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// PR Governance: Check that a PR has at least one linked GitHub issue.
2+
// Posts a comment if no issue is found and fails the check.
3+
module.exports = async ({ github, context, core }) => {
4+
const pr = context.payload.pull_request;
5+
6+
// Skip for draft PRs
7+
if (pr.draft) {
8+
console.log('Skipping: draft PR');
9+
core.setOutput('skipped', 'true');
10+
return;
11+
}
12+
13+
// Skip for dependabot and automated PRs
14+
const skipAuthors = ['dependabot[bot]', 'dependabot', 'app/dependabot'];
15+
if (skipAuthors.includes(pr.user.login)) {
16+
console.log(`Skipping: automated PR by ${pr.user.login}`);
17+
core.setOutput('skipped', 'true');
18+
return;
19+
}
20+
21+
// Skip for PRs with specific labels
22+
const labels = pr.labels.map(l => l.name);
23+
const skipLabels = ['skip-governance'];
24+
if (labels.some(l => skipLabels.includes(l))) {
25+
console.log(`Skipping: PR has exempt label`);
26+
core.setOutput('skipped', 'true');
27+
return;
28+
}
29+
30+
// Check linked issues via GitHub's closingIssuesReferences API
31+
// Covers closing keywords (Fixes/Closes/Resolves #123) and sidebar links
32+
const query = `query($owner: String!, $repo: String!, $number: Int!) {
33+
repository(owner: $owner, name: $repo) {
34+
pullRequest(number: $number) {
35+
closingIssuesReferences(first: 10) {
36+
nodes { number }
37+
}
38+
}
39+
}
40+
}`;
41+
42+
let linkedIssueNumbers = [];
43+
try {
44+
const result = await github.graphql(query, {
45+
owner: context.repo.owner,
46+
repo: context.repo.repo,
47+
number: pr.number,
48+
});
49+
linkedIssueNumbers = result?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map(i => i.number) || [];
50+
} catch (err) {
51+
core.warning(`Could not check linked issues: ${err.message}`);
52+
return;
53+
}
54+
55+
const BOT_MARKER = '<!-- pr-governance-issue -->';
56+
57+
if (linkedIssueNumbers.length === 0) {
58+
const commentBody = [
59+
BOT_MARKER,
60+
`### 🔗 Linked Issue Required`,
61+
'',
62+
'Thanks for the contribution! Please link a GitHub issue to this PR by adding `Fixes #123` to the description or using the sidebar.',
63+
'No issue yet? Feel free to [create one](https://github.com/Azure/azure-dev/issues/new)!',
64+
].join('\n');
65+
66+
try {
67+
const comments = await github.paginate(github.rest.issues.listComments, {
68+
owner: context.repo.owner,
69+
repo: context.repo.repo,
70+
issue_number: pr.number,
71+
per_page: 100,
72+
});
73+
const existingComment = comments.find(c => c.body?.includes(BOT_MARKER));
74+
75+
if (existingComment) {
76+
await github.rest.issues.updateComment({
77+
owner: context.repo.owner,
78+
repo: context.repo.repo,
79+
comment_id: existingComment.id,
80+
body: commentBody,
81+
});
82+
} else {
83+
await github.rest.issues.createComment({
84+
owner: context.repo.owner,
85+
repo: context.repo.repo,
86+
issue_number: pr.number,
87+
body: commentBody,
88+
});
89+
}
90+
} catch (e) {
91+
console.log(`Could not post comment (expected for fork PRs): ${e.message}`);
92+
}
93+
94+
core.setFailed('PR must be linked to a GitHub issue.');
95+
return;
96+
}
97+
98+
// Issues found — clean up any prior "link issue" comment
99+
try {
100+
const comments = await github.paginate(github.rest.issues.listComments, {
101+
owner: context.repo.owner,
102+
repo: context.repo.repo,
103+
issue_number: pr.number,
104+
per_page: 100,
105+
});
106+
const existingComment = comments.find(c => c.body?.includes(BOT_MARKER));
107+
if (existingComment) {
108+
await github.rest.issues.deleteComment({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
comment_id: existingComment.id,
112+
});
113+
console.log('Removed prior "link issue" comment');
114+
}
115+
} catch (e) {
116+
console.log(`Could not clean up comment: ${e.message}`);
117+
}
118+
119+
console.log(`✅ PR has linked issue(s): ${linkedIssueNumbers.join(', ')}`);
120+
core.setOutput('issue_numbers', JSON.stringify(linkedIssueNumbers));
121+
};
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// PR Governance: Check sprint/milestone status of linked issues
2+
// and post informational comments to help contributors understand prioritization.
3+
const PROJECT_NUMBER = 182;
4+
5+
module.exports = async ({ github, context, core }) => {
6+
let issueNumbers;
7+
try {
8+
issueNumbers = JSON.parse(process.env.ISSUE_NUMBERS || '[]');
9+
} catch {
10+
console.log('No valid issue numbers provided, skipping priority check');
11+
return;
12+
}
13+
if (!Array.isArray(issueNumbers) || issueNumbers.length === 0) return;
14+
const pr = context.payload.pull_request;
15+
const projectToken = process.env.PROJECT_TOKEN;
16+
17+
// Determine current month milestone name (e.g., "April 2026")
18+
const now = new Date();
19+
const monthNames = [
20+
'January', 'February', 'March', 'April', 'May', 'June',
21+
'July', 'August', 'September', 'October', 'November', 'December'
22+
];
23+
const currentMilestoneName = `${monthNames[now.getMonth()]} ${now.getFullYear()}`;
24+
let issueDetails = [];
25+
26+
// Check sprint assignment via Project #182 (if token available)
27+
let sprintInfo = {};
28+
if (projectToken) {
29+
try {
30+
async function graphqlWithToken(query, token) {
31+
const response = await fetch('https://api.github.com/graphql', {
32+
method: 'POST',
33+
headers: {
34+
'Authorization': `bearer ${token}`,
35+
'Content-Type': 'application/json',
36+
},
37+
body: JSON.stringify({ query }),
38+
});
39+
const json = response.ok ? await response.json() : null;
40+
if (!response.ok) throw new Error(`GitHub API returned ${response.status}: ${response.statusText}`);
41+
if (json.errors) throw new Error(json.errors[0].message);
42+
return json.data;
43+
}
44+
45+
// Get current sprint iteration
46+
const sprintData = await graphqlWithToken(`{
47+
organization(login: "Azure") {
48+
projectV2(number: ${PROJECT_NUMBER}) {
49+
field(name: "Sprint") {
50+
... on ProjectV2IterationField {
51+
id
52+
configuration {
53+
iterations { id title startDate duration }
54+
}
55+
}
56+
}
57+
}
58+
}
59+
}`, projectToken);
60+
61+
const iterations = sprintData.organization.projectV2.field.configuration.iterations;
62+
63+
// Find the current sprint (today falls within start + duration)
64+
const today = new Date();
65+
const currentSprint = iterations.find(iter => {
66+
const start = new Date(iter.startDate);
67+
const end = new Date(start);
68+
end.setDate(end.getDate() + iter.duration);
69+
return today >= start && today < end;
70+
});
71+
72+
if (currentSprint) {
73+
console.log(`Current sprint: ${currentSprint.title}`);
74+
75+
// Query sprint assignment per issue
76+
for (const issueNum of issueNumbers) {
77+
const num = parseInt(issueNum, 10);
78+
if (isNaN(num)) continue;
79+
try {
80+
const issueData = await graphqlWithToken(`{
81+
repository(owner: "Azure", name: "azure-dev") {
82+
issue(number: ${num}) {
83+
projectItems(first: 10) {
84+
nodes {
85+
project { number }
86+
fieldValueByName(name: "Sprint") {
87+
... on ProjectV2ItemFieldIterationValue {
88+
title
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}`, projectToken);
96+
97+
const projectItems = issueData.repository.issue.projectItems.nodes;
98+
const match = projectItems.find(item =>
99+
item.project.number === PROJECT_NUMBER && item.fieldValueByName?.title === currentSprint.title
100+
);
101+
if (match) {
102+
sprintInfo[issueNum] = match.fieldValueByName.title;
103+
console.log(`Issue #${issueNum} sprint: ${match.fieldValueByName.title}`);
104+
}
105+
} catch (err) {
106+
console.log(`Could not check sprint for issue #${issueNum}: ${err.message}`);
107+
}
108+
}
109+
}
110+
} catch (err) {
111+
console.log(`Sprint check skipped: ${err.message}`);
112+
}
113+
} else {
114+
console.log('Sprint check skipped: no PROJECT_READ_TOKEN');
115+
}
116+
117+
// If sprint found, skip milestone check entirely
118+
const hasCurrentSprint = issueNumbers.some(n => sprintInfo[n]);
119+
120+
if (!hasCurrentSprint) {
121+
// Fetch milestones for each issue
122+
for (const issueNum of issueNumbers) {
123+
try {
124+
const issue = await github.rest.issues.get({
125+
owner: context.repo.owner,
126+
repo: context.repo.repo,
127+
issue_number: issueNum,
128+
});
129+
130+
const milestone = issue.data.milestone;
131+
const milestoneTitle = milestone ? milestone.title : 'None';
132+
133+
issueDetails.push({
134+
number: issueNum,
135+
milestone: milestoneTitle,
136+
sprint: null,
137+
isCurrentMonth: milestoneTitle === currentMilestoneName,
138+
});
139+
} catch (err) {
140+
console.log(`Could not fetch issue #${issueNum}: ${err.message}`);
141+
}
142+
}
143+
}
144+
145+
const hasCurrentMilestone = issueDetails.some(i => i.isCurrentMonth);
146+
const allLookupsFailed = !hasCurrentSprint && issueDetails.length === 0;
147+
148+
if (allLookupsFailed) {
149+
console.log('⚠️ Could not determine sprint or milestone status — skipping comment');
150+
return;
151+
}
152+
153+
// Find existing bot comment to update
154+
const BOT_MARKER = '<!-- pr-governance-priority -->';
155+
let existingComment;
156+
try {
157+
const comments = await github.paginate(github.rest.issues.listComments, {
158+
owner: context.repo.owner,
159+
repo: context.repo.repo,
160+
issue_number: pr.number,
161+
per_page: 100,
162+
});
163+
existingComment = comments.find(c => c.body?.includes(BOT_MARKER));
164+
} catch (e) {
165+
console.log(`Could not list comments (expected for fork PRs): ${e.message}`);
166+
}
167+
168+
let commentBody = '';
169+
170+
if (hasCurrentSprint) {
171+
const sprintName = Object.values(sprintInfo)[0] || 'current sprint';
172+
console.log(`✅ Issue is in current sprint: ${sprintName}. All good!`);
173+
174+
// Delete existing comment if one was posted earlier
175+
if (existingComment) {
176+
try {
177+
await github.rest.issues.deleteComment({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
comment_id: existingComment.id,
181+
});
182+
console.log('Removed prior governance comment — issue is now in sprint');
183+
} catch (e) {
184+
console.log(`Could not remove comment (expected for fork PRs): ${e.message}`);
185+
}
186+
}
187+
return;
188+
} else if (hasCurrentMilestone) {
189+
console.log('✅ Issue is in the current milestone');
190+
commentBody = [
191+
BOT_MARKER,
192+
`### 📋 Milestone: ${currentMilestoneName}`,
193+
'',
194+
`This work is tracked for **${currentMilestoneName}**. The team will review it soon!`,
195+
].join('\n');
196+
} else {
197+
console.log('ℹ️ Issue is not in current sprint or milestone');
198+
commentBody = [
199+
BOT_MARKER,
200+
`### 📋 Prioritization Note`,
201+
'',
202+
`Thanks for the contribution! The linked issue isn't in the current milestone yet.`,
203+
'Review may take a bit longer — reach out to **@rajeshkamal5050** or **@kristenwomack** if you\'d like to discuss prioritization.',
204+
].join('\n');
205+
}
206+
207+
// Post or update comment
208+
try {
209+
if (existingComment) {
210+
await github.rest.issues.updateComment({
211+
owner: context.repo.owner,
212+
repo: context.repo.repo,
213+
comment_id: existingComment.id,
214+
body: commentBody,
215+
});
216+
console.log('Updated existing governance comment');
217+
} else {
218+
await github.rest.issues.createComment({
219+
owner: context.repo.owner,
220+
repo: context.repo.repo,
221+
issue_number: pr.number,
222+
body: commentBody,
223+
});
224+
console.log('Posted governance comment');
225+
}
226+
} catch (e) {
227+
console.log(`Could not post comment (expected for fork PRs): ${e.message}`);
228+
}
229+
};

.github/skills/changelog-generation/SKILL.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ Auto-detect scope from the current working directory:
4545

4646
### Step 2 — Determine Version & Update Files
4747

48-
Per [references/scope-rules.md](references/scope-rules.md) § Version Files.
48+
**Files to update (core):** `cli/azd/CHANGELOG.md`, `cli/version.txt`
49+
**Files to update (extension):** `<extension>/CHANGELOG.md`, `<extension>/version.txt`, `<extension>/extension.yaml`
4950

50-
- **Core**: derive version from the existing unreleased header (strip `-beta.*` and `(Unreleased)`), use today's date.
51-
- **Extension**: ask the user for the new version number via `ask_user`.
51+
For version derivation rules, see [references/scope-rules.md](references/scope-rules.md) § Version Files.
52+
53+
- **Core**: derive version from the existing unreleased header (strip `-beta.*` and `(Unreleased)`), use today's date. Update `cli/version.txt` to the released version.
54+
- **Extension**: ask the user for the new version number via `ask_user`. Update both `version.txt` and `extension.yaml` — they must match exactly.
5255

5356
Present the version and date to the user for confirmation before writing any files.
5457

0 commit comments

Comments
 (0)