Skip to content
2 changes: 1 addition & 1 deletion .github/pull-request-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

**Related issue(s)**
<!-- If you refer to a particular issue, provide its number, othewise, remove this section.
For example, `Resolves #123`, `Fixes #43`, or `See also #33`. The 3rd option will not automatically close the issue after the merge. -->
For example, `Resolves #123` or `Fixes #43` -->

**AI assistance**
<!-- See our AI Usage Policy: https://github.com/asyncapi/generator/blob/master/AI-POLICY.md
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/close-prs-for-unapproved-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Purpose of this workflow is to automatically close PRs that reference
# issues still carrying the 'Awaiting Approval' label, preventing work
# on issues that have not yet been approved by a maintainer.

name: Close PRs linked to unapproved issues

on:
pull_request_target:
types:
- opened
- edited
Comment thread
Adi-204 marked this conversation as resolved.
- reopened

jobs:
close-if-unapproved:
runs-on: ubuntu-latest

steps:
- name: Check linked issue and close PR if unapproved
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const prBody = context.payload.pull_request.body || '';
const prNumber = context.payload.pull_request.number;

const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s#(\d+)/gi;
Comment thread
Adi-204 marked this conversation as resolved.
const matches = [...prBody.matchAll(issuePattern)];

if (matches.length === 0) {
console.log(`No linked issues found in PR #${prNumber} body. Closing PR.`);

await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚫 This PR has been automatically closed because it does not reference a linked issue.\n\nPlease update your PR description to include a reference to the issue it addresses (e.g. \`Fixes #123\`, \`Closes #123\`, or \`Resolves #123\`) and reopen this PR.`,
});

await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});

return;
}

const awaitingLabel = 'Awaiting Approval';

for (const match of matches) {
const issueNumber = parseInt(match[1], 10);
console.log(`Checking issue #${issueNumber} for '${awaitingLabel}' label...`);

try {
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});

if (labels.some(l => l.name === awaitingLabel)) {
console.log(`Issue #${issueNumber} still has '${awaitingLabel}'. Closing PR #${prNumber}.`);

await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚫 This PR has been automatically closed because the linked issue #${issueNumber} has not been approved yet.\n\nPlease wait for a maintainer to approve the issue with the \`/approve\` command before opening a PR.`,
});

await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});

return;
}
} catch (error) {
if (error.status === 404) {
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚫 This PR references issue #${issueNumber}, which does not exist. Please correct the linked issue reference.`,
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
return;
}
console.log(`Could not fetch labels for issue #${issueNumber}: ${error.message}`);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

console.log('All linked issues are approved or no matching label found. PR stays open.');
148 changes: 148 additions & 0 deletions .github/workflows/issue-approve-command.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Purpose of this workflow is to allow core maintainers (listed on the catch-all
# `*` line of the CODEOWNERS file) to approve issues via the /approve command,
# swapping the 'Awaiting Approval' label for 'Approved Issue' and posting an
# instruction comment for contributors.

name: Issue /approve command

on:
issue_comment:
types:
- created

jobs:
approve-issue:
if: >
!github.event.issue.pull_request &&
github.event.issue.state != 'closed' &&
(github.event.comment.body == '/approve' || startsWith(github.event.comment.body, '/approve '))

runs-on: ubuntu-latest

steps:
- name: Checkout CODEOWNERS
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
sparse-checkout: CODEOWNERS
sparse-checkout-cone-mode: false

- name: Check authorization and approve
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ACTOR: ${{ github.actor }}
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const fs = require('fs');
const actor = process.env.ACTOR;

const codeowners = fs.readFileSync('./CODEOWNERS', 'utf8');
const lines = codeowners.split('\n');

const catchAllLine = lines.find(
line => !line.startsWith('#') && line.trim().startsWith('* ')
);

if (!catchAllLine) {
core.setFailed('Could not find the catch-all `*` line in CODEOWNERS');
return;
}

const regex = /@([a-zA-Z0-9_-]+)/g;
const matches = catchAllLine.match(regex);
const approvers = matches
? matches
.map(m => m.substring(1))
.filter(name => !name.includes('bot'))
: [];

core.info(`Core maintainer approvers from CODEOWNERS: ${approvers.join(', ')}`);

if (!approvers.includes(actor)) {
const approverMentions = approvers.map(u => `[@${u}](https://github.com/${u})`).join(', ');
const commentText = `❌ @${actor} is not authorized to use the \`/approve\` command.\nOnly core maintainers (${approverMentions}) can approve issues.`;

console.log(`❌ @${actor} made an unauthorized attempt to use /approve.`);
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentText,
});
return;
}

const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
if (currentLabels.some(l => l.name === 'Approved Issue')) {
console.log('Issue already approved, skipping.');
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `ℹ️ @${actor} this issue is already approved.`,
});
return;
}

const awaitingLabel = 'Awaiting Approval';
if (currentLabels.some(l => l.name === awaitingLabel)) {
console.log(`Removing label '${awaitingLabel}'...`);
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: awaitingLabel,
});
}

const approvedLabel = 'Approved Issue';
const labelColor = '0e8a16';
const labelDescription = 'Issue has been approved by a maintainer and is ready for work';

const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});

if (!repoLabels.some(l => l.name === approvedLabel)) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: approvedLabel,
color: labelColor,
description: labelDescription,
});
} catch (e) {
if (e.status !== 422) throw e;
}
}

console.log(`Adding label '${approvedLabel}' to issue #${context.issue.number}...`);
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [approvedLabel],
});

const commentBody = `👋 Thanks for your patience. This issue has been reviewed and is ready to be worked on.

**Before you start:**

- Comment below to let others know you're working on it (avoids duplicate work)
- Read our [CONTRIBUTING.md](../blob/master/CONTRIBUTING.md) if you haven't already
- Fork the repo, create a feature branch, and open a draft PR early

_Approved by @${actor}_`;

await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
51 changes: 51 additions & 0 deletions .github/workflows/issue-awaiting-approval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Purpose of this workflow is to automatically add the 'Awaiting Approval' label
# to every newly opened issue so maintainers can triage before work begins.

name: Add 'Awaiting Approval' label on new issues

on:
issues:
types:
- opened

jobs:
add-awaiting-approval-label:
runs-on: ubuntu-latest

steps:
- name: Add 'Awaiting Approval' label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
const labelName = 'Awaiting Approval';
const labelColor = 'fbca04';
const labelDescription = 'Issue awaiting maintainer approval before work can begin';

// Ensure the label exists in the repo
const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});

if (!repoLabels.some(l => l.name === labelName)) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: labelName,
color: labelColor,
description: labelDescription,
});
} catch (e) {
if (e.status !== 422) throw e;
}
}

console.log(`Adding label '${labelName}' to issue #${context.issue.number}...`);
await github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [labelName],
});
22 changes: 22 additions & 0 deletions apps/generator/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ We use Github to host code, to track issues and feature requests, as well as acc

[Open an issue](https://github.com/asyncapi/asyncapi/issues/new) **only** if you want to report a bug or a feature. Don't open issues for questions or support, instead join our [Slack workspace](https://www.asyncapi.com/slack-invite) and ask there. Don't forget to follow our [Slack Etiquette](https://github.com/asyncapi/community/blob/master/docs/060-meetings-and-communication/slack-etiquette.md) while interacting with community members! It's more likely you'll get help, and much faster!

### Issue Approval Process

Every newly opened issue is automatically labelled **`Awaiting Approval`**. This label signals that a core maintainer still needs to review and triage the issue before any work should begin.

#### How approval works

1. **A core maintainer reviews the issue** and, if it is valid and ready for work, posts a comment containing the `/approve` command.
2. The automation removes the `Awaiting Approval` label, adds the **`Approved Issue`** label, and posts a comment with contribution instructions.
3. Contributors can now pick up the issue, comment to claim it, and open a pull request.

> **Who can approve?** Only the core maintainers listed on the catch-all (`*`) line of the [`CODEOWNERS`](https://github.com/asyncapi/generator/blob/master/CODEOWNERS) file. Bot accounts are excluded. If anyone else uses `/approve`, a comment is posted explaining they are not authorized.

#### PR gating

Pull requests that reference an issue still carrying the `Awaiting Approval` label (via `Closes #N`, `Fixes #N`, or `Resolves #N`) are **automatically closed**. This ensures no work is merged for issues that have not been vetted. PRs that do not reference any issue at all are also automatically closed.

#### Commands reference

| Command | Who can use it | Effect |
|---------|---------------|--------|
| `/approve` | Core maintainers (from `CODEOWNERS`) | Removes `Awaiting Approval`, adds `Approved Issue`, posts contribution instructions |

## Bug Reports and Feature Requests

Please use our issues templates that provide you with hints on what information we need from you to help you out.
Expand Down
Loading