Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
88 changes: 88 additions & 0 deletions .github/scripts/pr-governance-issue-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// PR Governance: Check that a PR has at least one linked GitHub issue.
// Posts a comment if no issue is found and fails the check.
module.exports = async ({ github, context, core }) => {
const pr = context.payload.pull_request;

// Skip for dependabot and automated PRs
const skipAuthors = ['dependabot[bot]', 'dependabot', 'app/dependabot'];
if (skipAuthors.includes(pr.user.login)) {
console.log(`Skipping: automated PR by ${pr.user.login}`);
core.setOutput('skipped', 'true');
return;
}

// Skip for PRs with specific labels
const labels = pr.labels.map(l => l.name);
const skipLabels = ['skip-governance'];
if (labels.some(l => skipLabels.includes(l))) {
Comment thread
rajeshkamal5050 marked this conversation as resolved.
console.log(`Skipping: PR has exempt label`);
core.setOutput('skipped', 'true');
return;
}

// Check linked issues via GitHub's closingIssuesReferences API
// Covers closing keywords (Fixes/Closes/Resolves #123) and sidebar links
const query = `query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 10) {
nodes { number }
}
}
}
}`;

const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
number: pr.number,
});

const linkedIssues = result.repository.pullRequest.closingIssuesReferences.nodes;
const linkedIssueNumbers = linkedIssues.map(i => i.number);

Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
if (linkedIssueNumbers.length === 0) {
const BOT_MARKER = '<!-- pr-governance-priority -->';
const comments = await github.paginate(github.rest.issues.listComments, {
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100,
});
const existingComment = comments.find(c => c.body && c.body.includes(BOT_MARKER));
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated

const commentBody = [
BOT_MARKER,
`### 🔗 Linked Issue Required`,
'',
'Thanks for the contribution! Please link a GitHub issue to this PR by adding `Fixes #123` to the description or using the sidebar.',
'No issue yet? Feel free to [create one](https://github.com/Azure/azure-dev/issues/new)!',
].join('\n');

try {
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: commentBody,
});
}
Comment thread
rajeshkamal5050 marked this conversation as resolved.
} catch (e) {
console.log(`Could not post comment (expected for fork PRs): ${e.message}`);
}

core.setFailed('PR must be linked to a GitHub issue.');
return;
}

console.log(`✅ PR has linked issue(s): ${linkedIssueNumbers.join(', ')}`);
core.setOutput('issue_numbers', JSON.stringify(linkedIssueNumbers));
};
206 changes: 206 additions & 0 deletions .github/scripts/pr-governance-priority-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// PR Governance: Check sprint/milestone status of linked issues
// and post informational comments to help contributors understand prioritization.
module.exports = async ({ github, context, core }) => {
const issueNumbers = JSON.parse(process.env.ISSUE_NUMBERS);
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
const pr = context.payload.pull_request;
const projectToken = process.env.PROJECT_TOKEN;

// Determine current month milestone name (e.g., "April 2026")
const now = new Date();
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
const currentMilestoneName = `${monthNames[now.getMonth()]} ${now.getFullYear()}`;
let issueDetails = [];

// Check sprint assignment via Project #182 (if token available)
let sprintInfo = {};
if (projectToken) {
try {
async function graphqlWithToken(query, token) {
const response = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Authorization': `bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
});
const json = await response.json();
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
if (json.errors) throw new Error(json.errors[0].message);
return json.data;
}

// Get current sprint iteration
const sprintData = await graphqlWithToken(`{
organization(login: "Azure") {
projectV2(number: 182) {
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
field(name: "Sprint") {
... on ProjectV2IterationField {
id
configuration {
iterations { id title startDate duration }
}
}
}
}
}
}`, projectToken);

const iterations = sprintData.organization.projectV2.field.configuration.iterations;

// Find the current sprint (today falls within start + duration)
const today = new Date();
const currentSprint = iterations.find(iter => {
const start = new Date(iter.startDate);
const end = new Date(start);
end.setDate(end.getDate() + iter.duration);
return today >= start && today < end;
});

if (currentSprint) {
console.log(`Current sprint: ${currentSprint.title}`);

// Query sprint assignment per issue
for (const issueNum of issueNumbers) {
try {
const issueData = await graphqlWithToken(`{
repository(owner: "Azure", name: "azure-dev") {
issue(number: ${issueNum}) {
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
projectItems(first: 10) {
nodes {
project { number }
fieldValueByName(name: "Sprint") {
... on ProjectV2ItemFieldIterationValue {
title
}
}
}
}
}
}
}`, projectToken);

const projectItems = issueData.repository.issue.projectItems.nodes;
const match = projectItems.find(item =>
item.project.number === 182 && item.fieldValueByName?.title
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated
);
if (match) {
sprintInfo[issueNum] = match.fieldValueByName.title;
console.log(`Issue #${issueNum} sprint: ${match.fieldValueByName.title}`);
}
} catch (err) {
console.log(`Could not check sprint for issue #${issueNum}: ${err.message}`);
}
}
}
} catch (err) {
console.log(`Sprint check skipped: ${err.message}`);
}
} else {
console.log('Sprint check skipped: no PROJECT_READ_TOKEN');
}

// If sprint found, skip milestone check entirely
const hasCurrentSprint = issueNumbers.some(n => sprintInfo[n]);

if (!hasCurrentSprint) {
// Fetch milestones for each issue
for (const issueNum of issueNumbers) {
try {
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNum,
});

const milestone = issue.data.milestone;
const milestoneTitle = milestone ? milestone.title : 'None';

issueDetails.push({
number: issueNum,
milestone: milestoneTitle,
sprint: null,
isCurrentMonth: milestoneTitle === currentMilestoneName,
});
} catch (err) {
console.log(`Could not fetch issue #${issueNum}: ${err.message}`);
}
}
}

const hasCurrentMilestone = issueDetails.some(i => i.isCurrentMonth);

// Find existing bot comment to update
Comment thread
rajeshkamal5050 marked this conversation as resolved.
const BOT_MARKER = '<!-- pr-governance-priority -->';
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
per_page: 100,
});
const existingComment = comments.find(c => c.body && c.body.includes(BOT_MARKER));

let commentBody = '';

if (hasCurrentSprint) {
const sprintName = Object.values(sprintInfo)[0] || 'current sprint';
console.log(`✅ Issue is in current sprint: ${sprintName}. All good!`);

// Delete existing comment if one was posted earlier
if (existingComment) {
try {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
});
console.log('Removed prior governance comment — issue is now in sprint');
} catch (e) {
console.log(`Could not remove comment (expected for fork PRs): ${e.message}`);
}
}
return;
} else if (hasCurrentMilestone) {
console.log('✅ Issue is in the current milestone');
commentBody = [
BOT_MARKER,
`### 📋 Milestone: ${currentMilestoneName}`,
'',
`This work is tracked for **${currentMilestoneName}**. The team will review it soon!`,
].join('\n');
} else {
console.log('ℹ️ Issue is not in current sprint or milestone');
commentBody = [
BOT_MARKER,
`### 📋 Prioritization Note`,
'',
`Thanks for the contribution! The linked issue isn't in the current milestone yet.`,
'Review may take a bit longer — reach out to **@rajeshkamal5050** or **@kristenwomack** if you\'d like to discuss prioritization.',
].join('\n');
}

// Post or update comment
try {
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: commentBody,
});
console.log('Updated existing governance comment');
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: commentBody,
});
console.log('Posted governance comment');
}
} catch (e) {
console.log(`Could not post comment (expected for fork PRs): ${e.message}`);
}
};
38 changes: 38 additions & 0 deletions .github/workflows/pr-governance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: pr-governance

on:
pull_request:
branches: [main]
types: [opened, edited, synchronize, labeled, unlabeled]
Comment thread
rajeshkamal5050 marked this conversation as resolved.
Outdated

permissions:
pull-requests: write
issues: write
contents: read

jobs:
governance-checks:
name: PR Governance
runs-on: ubuntu-latest
Comment thread
rajeshkamal5050 marked this conversation as resolved.
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Check linked issue
id: issue-check
uses: actions/github-script@v7
with:
script: |
const script = require('./.github/scripts/pr-governance-issue-check.js');
await script({ github, context, core });

- name: Check issue priority
if: steps.issue-check.outputs.skipped != 'true' && steps.issue-check.outcome == 'success'
uses: actions/github-script@v7
env:
PROJECT_TOKEN: ${{ secrets.PROJECT_READ_TOKEN }}
ISSUE_NUMBERS: ${{ steps.issue-check.outputs.issue_numbers }}
with:
script: |
const script = require('./.github/scripts/pr-governance-priority-check.js');
await script({ github, context, core });
10 changes: 6 additions & 4 deletions .vscode/cspell-github-user-aliases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ isatty
joho
karolz
kitsiosk
kristenwomack
LianwMS
Lunatico9
mattn
Menghua1
mikekistler
Mstiekema
multierr
Expand All @@ -34,21 +36,21 @@ otiai10
pamelafox
pauldotyu
pbnj
rajeshkamal
richardpark-msft
rujche
Saipriya
saragluna
scottaddie
sebastianmattar
sergi
sethvargo
spboyer
stretchr
tidwall
theckman
TheEskhaton
tidwall
tonybaloney
vivazqu
weilim
Yionse
Saipriya
Menghua1
spboyer
3 changes: 2 additions & 1 deletion cli/azd/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ In general, to make contributions a smooth and easy experience, we encourage the
- Check existing issues for [bugs][bug issues] or [enhancements][enhancement issues].
- Open an issue if things aren't working as expected, or if an enhancement is being proposed.
- Start a conversation on the issue if you are thinking of submitting a pull request.
- Submit a pull request. The `azd` team will work with you to review the changes and provide feedback. Once the pull request is accepted, a member will merge the changes. Thank you for taking time out of your day to help improve our community!
- Submit a pull request **linked to the issue** (e.g., add `Fixes #123` to the PR description). PRs without a linked issue will be flagged by our automated checks. Issues in the current milestone get priority review — if yours isn't prioritized yet, tag **@rajeshkamal5050** or **@kristenwomack** and we'll help get it sorted.
- The `azd` team will work with you to review the changes and provide feedback. Once the pull request is accepted, a member will merge the changes. Thank you for taking time out of your day to help improve our community!

## Building `azd`

Expand Down
Loading