fix(ci): make the vouch check speak, unblock list updates, widen the docs exemption (#6974)
This commit is contained in:
@@ -44,6 +44,7 @@ jobs:
|
||||
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
|
||||
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
|
||||
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
|
||||
github_scripts: ${{ steps.filter.outputs.github_scripts }}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
@@ -101,6 +102,13 @@ jobs:
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
- '.github/workflows/docs-llms-txt-check.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
github_scripts:
|
||||
- '.github/scripts/**'
|
||||
- '.github/VOUCHED.td'
|
||||
- '.github/workflows/pr-gate.yml'
|
||||
- '.github/workflows/vouch-check-pr.yml'
|
||||
- '.github/workflows/issue-labeler.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
|
||||
python-sdk:
|
||||
name: Python SDK
|
||||
@@ -177,6 +185,24 @@ jobs:
|
||||
uses: ./.github/workflows/docs-llms-txt-check.yml
|
||||
secrets: inherit
|
||||
|
||||
github-scripts:
|
||||
name: GitHub Scripts
|
||||
needs: changes
|
||||
if: needs.changes.outputs.github_scripts == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Run .github/scripts tests
|
||||
run: |
|
||||
for test in .github/scripts/*.test.js; do
|
||||
echo "::group::$test"
|
||||
node "$test"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
gate:
|
||||
name: CI Gate
|
||||
needs:
|
||||
@@ -192,6 +218,7 @@ jobs:
|
||||
- n8n-nodes-mem0
|
||||
- zapier-mem0
|
||||
- docs-llms-txt
|
||||
- github-scripts
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -2,11 +2,13 @@ name: PR Gate
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited, ready_for_review]
|
||||
types: [opened, reopened, ready_for_review, edited]
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
concurrency:
|
||||
group: pr-gate-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
group: pr-gate-${{ github.event_name }}-${{ github.event.action }}-${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
|
||||
|
||||
env:
|
||||
GATE_EFFECTIVE_FROM: '2026-08-12T00:00:00Z'
|
||||
@@ -19,8 +21,11 @@ permissions:
|
||||
jobs:
|
||||
gate:
|
||||
if: >-
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.action != 'edited' &&
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.user.type != 'Bot' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -47,7 +52,9 @@ jobs:
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner, repo, pull_number: pr.number, per_page: 100,
|
||||
});
|
||||
if (files.length > 0 && files.every((file) => file.filename.startsWith('docs/'))) {
|
||||
const rootDocs = new Set(['README.md', 'CONTRIBUTING.md', 'CODE_OF_CONDUCT.md', 'SECURITY.md']);
|
||||
const isDocs = (filename) => filename.startsWith('docs/') || rootDocs.has(filename);
|
||||
if (files.length > 0 && files.every((file) => isDocs(file.filename))) {
|
||||
core.info('Docs-only PR, gate skipped');
|
||||
return;
|
||||
}
|
||||
@@ -75,6 +82,7 @@ jobs:
|
||||
}
|
||||
|
||||
const body = [
|
||||
'<!-- pr-gate -->',
|
||||
'Thanks for taking the time to open this.',
|
||||
'',
|
||||
'We only review pull requests that fix an issue we have already agreed to take on, so this one is closed for now.',
|
||||
@@ -84,10 +92,9 @@ jobs:
|
||||
'',
|
||||
'1. Make sure an issue describes the problem, with the version you are on, a runnable reproduction, and the real output or traceback you saw.',
|
||||
'2. Link it from this pull request description with `Closes #<number>`.',
|
||||
'3. Ask a maintainer to label that issue `accepted`.',
|
||||
'4. Reopen this pull request. The check runs again and it stays open.',
|
||||
'3. Ask a maintainer to label that issue `accepted`. This pull request reopens by itself when they do.',
|
||||
'',
|
||||
'Already linked an accepted issue? Edit the description to include `Closes #<number>` and reopen. The check reruns automatically.',
|
||||
'Issue already labeled `accepted`? Just add `Closes #<number>` to the description. That reopens this too.',
|
||||
'',
|
||||
'Documentation-only changes skip this gate entirely.',
|
||||
'',
|
||||
@@ -101,3 +108,107 @@ jobs:
|
||||
owner, repo, pull_number: pr.number, state: 'closed',
|
||||
});
|
||||
core.info(`Closed #${pr.number}: no accepted issue linked`);
|
||||
|
||||
reopen:
|
||||
if: >-
|
||||
(github.event_name == 'issues' && github.event.label.name == 'accepted') ||
|
||||
(github.event.action == 'edited' && github.event.pull_request.state == 'closed')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const marker = '<!-- pr-gate -->';
|
||||
|
||||
const denounced = await (async () => {
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner, repo, path: '.github/VOUCHED.td',
|
||||
ref: context.payload.repository.default_branch,
|
||||
});
|
||||
return new Set(Buffer.from(data.content, 'base64').toString('utf8')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith('-'))
|
||||
.map((line) => line.slice(1).split(/\s+/)[0].split(':').pop().toLowerCase())
|
||||
.filter(Boolean));
|
||||
} catch (error) {
|
||||
core.warning(`Could not read VOUCHED.td, treating nobody as denounced: ${error.message}`);
|
||||
return new Set();
|
||||
}
|
||||
})();
|
||||
|
||||
const isReopenable = async (number) => {
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
state
|
||||
author { login }
|
||||
closingIssuesReferences(first: 20) {
|
||||
nodes { labels(first: 50) { nodes { name } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, repo, number },
|
||||
);
|
||||
const pullRequest = repository.pullRequest;
|
||||
if (denounced.has(pullRequest.author?.login?.toLowerCase())) {
|
||||
core.info(`#${number} is from a denounced author. Vouch outranks this gate.`);
|
||||
return false;
|
||||
}
|
||||
return pullRequest.state === 'CLOSED' &&
|
||||
pullRequest.closingIssuesReferences.nodes.some((issue) =>
|
||||
issue.labels.nodes.some((label) => label.name === 'accepted'));
|
||||
};
|
||||
|
||||
let candidates;
|
||||
if (context.eventName === 'issues') {
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
issue(number: $number) {
|
||||
closedByPullRequestsReferences(first: 20, includeClosedPrs: true) {
|
||||
nodes { number }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, repo, number: context.payload.issue.number },
|
||||
);
|
||||
candidates = repository.issue.closedByPullRequestsReferences.nodes.map((pr) => pr.number);
|
||||
} else {
|
||||
candidates = [context.payload.pull_request.number];
|
||||
}
|
||||
|
||||
for (const number of candidates) {
|
||||
if (!(await isReopenable(number))) {
|
||||
core.info(`#${number} is not a closed pull request linking an accepted issue. Skipped.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: number, per_page: 100,
|
||||
});
|
||||
if (!comments.some((comment) => comment.body?.startsWith(marker))) {
|
||||
core.info(`#${number} was not closed by this gate. Left alone.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.pulls.update({
|
||||
owner, repo, pull_number: number, state: 'open',
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Could not reopen #${number}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: number,
|
||||
body: 'An `accepted` issue is linked now, so this is open again and ready for review.',
|
||||
});
|
||||
core.info(`Reopened #${number}`);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,44 @@ jobs:
|
||||
check:
|
||||
if: >-
|
||||
github.event.pull_request.user.type != 'Bot' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mitchellh/vouch/action/check-pr@d66fa29a64600490892131ad87597c30c91fcac4 # v1.5.0
|
||||
id: vouch
|
||||
with:
|
||||
pr-number: ${{ github.event.pull_request.number }}
|
||||
auto-close: false
|
||||
require-vouch: false
|
||||
auto-close: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- if: steps.vouch.outputs.status == 'allowed'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
const marker = '<!-- vouch-check -->';
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: pr.number, per_page: 100,
|
||||
});
|
||||
if (comments.some((comment) => comment.body?.startsWith(marker))) {
|
||||
core.info('Vouch comment already posted, skipped.');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
`Hi @${context.payload.pull_request.user.login}, thanks for opening this pull request.`,
|
||||
'',
|
||||
"This is just a soft check: you are not yet in this repo's vouched contributor list (`.github/VOUCHED.td`). Nothing is blocked and there is nothing you need to do.",
|
||||
'',
|
||||
`A maintainer can vouch for you by commenting \`!vouch @${context.payload.pull_request.user.login}\` on any issue.`,
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number, body,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,6 @@ jobs:
|
||||
denounce-keyword: "!denounce"
|
||||
unvouch-keyword: "!unvouch"
|
||||
pull-request: "true"
|
||||
merge-immediately: "true"
|
||||
merge-immediately: "false"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
Reference in New Issue
Block a user