-
Notifications
You must be signed in to change notification settings - Fork 1
Add CI workflows and multi-session PR guard #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
52945a7
39a3fce
8390e29
e8b1aba
c5f753a
121f04d
93a9eec
4177d61
9b681e3
cf4b646
06e9dd3
4e8b096
d5eab0c
feb27cf
3f7e5ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Claude Development Notes | ||
|
|
||
| This file contains guidance for Claude Code when working in this repository. | ||
| It is excluded from distributions via `.gitattributes export-ignore`. | ||
|
|
||
| ## CI Monitoring After Every Push | ||
|
|
||
| **REQUIRED**: After every `git push`, immediately start a background task to | ||
| monitor the CI run for that push. If you pushed to both pgxntool and | ||
| pgxntool-test, start a background task for each repo — do not monitor them | ||
| sequentially. | ||
|
|
||
| Use `gh run watch` or poll with `gh run list` / `gh pr checks` in the | ||
| background task. Report failures to the user as soon as they are detected; | ||
| do not wait for all jobs to finish before reporting. | ||
|
|
||
| ## Multiple Concurrent Sessions | ||
|
|
||
| It is common to have multiple Claude Code sessions open simultaneously across | ||
| pgxntool and pgxntool-test. To avoid cross-session interference: | ||
|
|
||
| **If you are asked to do something on an existing PR that you did not open or | ||
| are not already working on in this session, immediately ask for confirmation | ||
| before proceeding.** For example: "I see PR #32 exists. Were you asking me to | ||
| work on that, or did you mean to send this to a different session?" | ||
|
|
||
| This applies to: editing PR branches, pushing to them, closing/reopening them, | ||
| adding commits, modifying PR descriptions, or any other PR-level action. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # .github/workflows — CI Architecture | ||
|
|
||
| ## Workflow files | ||
|
|
||
| - **`ci.yml`** — main CI for pgxntool pull requests. Runs `check-test-pr` (verifies | ||
| the paired pgxntool-test PR's CI passed), then optionally runs `test` (only for the | ||
| commit-with-no-tests path — see below). | ||
| - **`protect-label.yml`** — enforces that only maintainers with write access can apply | ||
| or remove the `commit-with-no-tests` label. | ||
|
|
||
| ## Normal CI flow (paired test PR exists) | ||
|
|
||
| When a pgxntool PR has a corresponding open PR in pgxntool-test with the same branch | ||
| name, the `check-test-pr` job polls (up to 20 minutes) for that test PR's CI to | ||
| complete and pass. If it passes, pgxntool CI passes — **no tests run here**. Tests run | ||
| exactly once, in pgxntool-test's own CI. | ||
|
|
||
| ## commit-with-no-tests path | ||
|
|
||
| When a maintainer applies the `commit-with-no-tests` label (and no paired test PR | ||
| exists), the `test` job runs tests directly in pgxntool CI against pgxntool-test/master. | ||
| This is the rare exception, not the norm. | ||
|
|
||
| ## Cross-repo reusable workflow — tradeoffs and constraints | ||
|
|
||
| The `test` job calls a reusable workflow from pgxntool-test: | ||
| ```yaml | ||
| uses: Postgres-Extensions/pgxntool-test/.github/workflows/run-tests.yml@<ref> | ||
| ``` | ||
|
|
||
| GitHub Actions requires the `uses:` ref to be a **static string** — expressions like | ||
| `${{ }}` are not supported in the repo/path portion or the `@ref` suffix in practice. | ||
|
|
||
| ### The @branch → @master ref | ||
|
|
||
| While developing on a feature branch where pgxntool-test also has changes, this ref | ||
| is set to `@<branch>` so CI can find `run-tests.yml` before it lands on master. | ||
|
|
||
| **IMPORTANT**: This ref must be updated to `@master` before pgxntool merges. The | ||
| correct merge order is: **pgxntool-test merges first**, then update this ref to | ||
| `@master`, then pgxntool merges. | ||
|
|
||
| **For Claude**: Do NOT leave a `@<branch>` ref without explicit user approval. The | ||
| user merges directly from the PR page — there are no manual steps between merges. | ||
| See `.github/workflows/CLAUDE.md` in pgxntool-test for the full picture. | ||
|
|
||
| ### Changes to run-tests.yml | ||
|
|
||
| `run-tests.yml` lives in pgxntool-test and is the single source of truth for all test | ||
| steps. If it changes, pgxntool's CI uses `@master` — so it won't see the new version | ||
| until pgxntool-test merges. This is acceptable because: | ||
| - Changes to `run-tests.yml` require a paired test PR (not commit-with-no-tests) | ||
| - When a paired test PR exists, pgxntool's `test` job is skipped anyway | ||
| - The two scenarios are mutually exclusive in practice | ||
|
|
||
| ## Label name | ||
|
|
||
| The label `commit-with-no-tests` is defined as a const (`NO_TEST_LABEL`) in `ci.yml` | ||
| and as `LABEL` in `protect-label.yml`. The job-level `if:` condition in | ||
| `protect-label.yml` must also use the literal string (YAML can't reference JS consts) | ||
| — keep these in sync if the label name ever changes. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,286 @@ | ||||||||||||||||||||||||||||||||||||||||
| name: CI | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||
| pull_request: | ||||||||||||||||||||||||||||||||||||||||
| # We use 'pull_request' (not 'pull_request_target') deliberately. | ||||||||||||||||||||||||||||||||||||||||
| # 'pull_request_target' runs with write access to the base repo, which is | ||||||||||||||||||||||||||||||||||||||||
| # a security risk for untrusted fork code. Since this workflow only reads | ||||||||||||||||||||||||||||||||||||||||
| # from other public repos (no secrets needed), 'pull_request' is correct | ||||||||||||||||||||||||||||||||||||||||
| # and safe even for fork PRs. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||||||||
| pull-requests: read | ||||||||||||||||||||||||||||||||||||||||
| checks: read | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||||||||||||
| group: ci-pr-${{ github.event.pull_request.number }} | ||||||||||||||||||||||||||||||||||||||||
| cancel-in-progress: true | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||
| check-test-pr: | ||||||||||||||||||||||||||||||||||||||||
| name: Check for paired pgxntool-test PR | ||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||
| # This check polls until the paired pgxntool-test CI run completes | ||||||||||||||||||||||||||||||||||||||||
| # (up to 20 minutes). The job timeout gives a few minutes of headroom. | ||||||||||||||||||||||||||||||||||||||||
| timeout-minutes: 25 | ||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||||||||
| run-tests: ${{ steps.check.outputs.run_tests }} | ||||||||||||||||||||||||||||||||||||||||
| test-ref: ${{ steps.check.outputs.test_ref }} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||
| - name: Find paired pgxntool-test PR or check commit-with-no-tests label | ||||||||||||||||||||||||||||||||||||||||
| id: check | ||||||||||||||||||||||||||||||||||||||||
| uses: actions/github-script@v7 | ||||||||||||||||||||||||||||||||||||||||
|
jnasbyupgrade marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||
| # GITHUB_TOKEN is sufficient for reading public repos. If these repos | ||||||||||||||||||||||||||||||||||||||||
| # are ever made private, replace with a PAT stored as a secret with | ||||||||||||||||||||||||||||||||||||||||
| # 'repo' scope on both repos. Note: PAT expiration causes silent | ||||||||||||||||||||||||||||||||||||||||
| # failures here — the API returns 401 and the job errors out instead | ||||||||||||||||||||||||||||||||||||||||
| # of failing gracefully with a useful message. | ||||||||||||||||||||||||||||||||||||||||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||
| script: | | ||||||||||||||||||||||||||||||||||||||||
| const branch = context.payload.pull_request.head.ref; | ||||||||||||||||||||||||||||||||||||||||
| const prNumber = context.payload.pull_request.number; | ||||||||||||||||||||||||||||||||||||||||
| // Single source of truth for the label name. Must also match the | ||||||||||||||||||||||||||||||||||||||||
| // literal string in the protect-label.yml job-level `if:` condition | ||||||||||||||||||||||||||||||||||||||||
| // (YAML expressions can't reference JS constants). | ||||||||||||||||||||||||||||||||||||||||
| const NO_TEST_LABEL = 'commit-with-no-tests'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // master-to-master PRs have no paired test PR by convention. | ||||||||||||||||||||||||||||||||||||||||
| // Run tests against pgxntool-test/master directly. | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // If a fork PR's branch is named 'master', that's almost certainly | ||||||||||||||||||||||||||||||||||||||||
| // a mistake (contributors should use a feature branch), but we | ||||||||||||||||||||||||||||||||||||||||
| // don't block it — just warn visibly as an annotation on the run. | ||||||||||||||||||||||||||||||||||||||||
| // Note: pull_request gives a read-only token for fork PRs, so we | ||||||||||||||||||||||||||||||||||||||||
| // can't post a PR comment back to the upstream repo from here. | ||||||||||||||||||||||||||||||||||||||||
| if (branch === 'master') { | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gate the shortcut on the base branch too. Line 57 implements this as “head branch is Proposed fix const branch = context.payload.pull_request.head.ref;
+ const baseBranch = context.payload.pull_request.base.ref;
const prNumber = context.payload.pull_request.number;
@@
- if (branch === 'master') {
+ if (branch === 'master' && baseBranch === 'master') {
const headRepo = context.payload.pull_request.head.repo;
const isBaseRepo =
headRepo?.owner?.login === context.repo.owner &&
headRepo?.name === context.repo.repo;🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| const headRepo = context.payload.pull_request.head.repo; | ||||||||||||||||||||||||||||||||||||||||
| const isBaseRepo = | ||||||||||||||||||||||||||||||||||||||||
| headRepo?.owner?.login === context.repo.owner && | ||||||||||||||||||||||||||||||||||||||||
| headRepo?.name === context.repo.repo; | ||||||||||||||||||||||||||||||||||||||||
| if (!isBaseRepo) { | ||||||||||||||||||||||||||||||||||||||||
| core.warning( | ||||||||||||||||||||||||||||||||||||||||
| `PR head branch is named 'master' but comes from a fork ` + | ||||||||||||||||||||||||||||||||||||||||
| `(${headRepo?.full_name ?? 'unknown'}). Contributors should ` + | ||||||||||||||||||||||||||||||||||||||||
| `use a feature branch, not master. Proceeding with tests ` + | ||||||||||||||||||||||||||||||||||||||||
| `against pgxntool-test/master.` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| core.setOutput('run_tests', 'true'); | ||||||||||||||||||||||||||||||||||||||||
| core.setOutput('test_ref', 'master'); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restrict the Right now any PR whose source branch is named Proposed fix- if (branch === 'master') {
+ const headRepo = context.payload.pull_request.head.repo;
+ const isBaseRepoHead =
+ headRepo?.owner?.login === context.repo.owner &&
+ headRepo?.name === context.repo.repo;
+
+ if (branch === 'master' && isBaseRepoHead) {
core.setOutput('run_tests', 'true');
core.setOutput('test_ref', 'master');
return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hrm... it's probably a mistake if someone commits to master on a fork. We shouldn't dis-allow it, but it'd be nice if CI posted a warning. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
// master-to-master PRs have no paired test PR by convention.
// Run tests against pgxntool-test/master directly.
if (branch === 'master') {
+ const headRepo = context.payload.pull_request.head.repo;
+ const isBaseRepoHead =
+ headRepo?.owner?.login === context.repo.owner &&
+ headRepo?.name === context.repo.repo;
+
+ if (!isBaseRepoHead) {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body:
+ '⚠️ **Heads up:** This PR was opened from the `master` branch of a fork. ' +
+ 'Using `master` as a feature branch is usually a mistake — consider ' +
+ 'creating a dedicated branch for your changes instead. CI will proceed ' +
+ 'normally, but please double-check this is intentional.',
+ });
+ }
+
core.setOutput('run_tests', 'true');
core.setOutput('test_ref', 'master');
return;
}One small thing to be aware of: because this uses jobs:
check-test-pr:
permissions:
pull-requests: writeWithout that, the comment call will silently fail (or error) on fork PRs since the default |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // The owner of this PR's head repo — the contributor's fork owner | ||||||||||||||||||||||||||||||||||||||||
| // for fork PRs, or the base repo owner for maintainer PRs. | ||||||||||||||||||||||||||||||||||||||||
| // The paired pgxntool-test PR must come from the SAME owner. | ||||||||||||||||||||||||||||||||||||||||
| // We never cross-match PRs across different contributors' forks. | ||||||||||||||||||||||||||||||||||||||||
| const prOwner = context.payload.pull_request.head.repo?.owner?.login; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Look for open pgxntool-test PRs with the SAME branch name AND | ||||||||||||||||||||||||||||||||||||||||
| // the same fork owner. Branch names must match exactly. | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // The GitHub API's 'head' filter requires "owner:branch" format. | ||||||||||||||||||||||||||||||||||||||||
| // We list all open PRs and filter locally — safe for repos with | ||||||||||||||||||||||||||||||||||||||||
| // few open PRs, and avoids needing to know the fork repo name. | ||||||||||||||||||||||||||||||||||||||||
| // paginate() fetches all pages automatically, so this is correct | ||||||||||||||||||||||||||||||||||||||||
| // even if pgxntool-test ever exceeds 100 open PRs (the per_page cap). | ||||||||||||||||||||||||||||||||||||||||
| const prs = await github.paginate(github.rest.pulls.list, { | ||||||||||||||||||||||||||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||||||||||||||||||||||||||
| repo: 'pgxntool-test', | ||||||||||||||||||||||||||||||||||||||||
| state: 'open', | ||||||||||||||||||||||||||||||||||||||||
| per_page: 100 | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const matching = prs.filter(pr => | ||||||||||||||||||||||||||||||||||||||||
| pr.head.ref === branch && | ||||||||||||||||||||||||||||||||||||||||
| pr.head.repo?.owner?.login === prOwner | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| if (matching.length > 1) { | ||||||||||||||||||||||||||||||||||||||||
| core.setFailed( | ||||||||||||||||||||||||||||||||||||||||
| `Multiple open pgxntool-test PRs from ${prOwner} match branch ` + | ||||||||||||||||||||||||||||||||||||||||
| `'${branch}'. Cannot determine which one to use.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `Close all but one, then re-run this check.` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const testPR = matching.length === 1 ? matching[0] : null; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (testPR) { | ||||||||||||||||||||||||||||||||||||||||
| // Error if the no-test label is also set — that's contradictory. | ||||||||||||||||||||||||||||||||||||||||
| // Re-fetch the PR live (not from payload) in case the label was | ||||||||||||||||||||||||||||||||||||||||
| // added after this workflow was triggered. | ||||||||||||||||||||||||||||||||||||||||
| const { data: currentPR } = await github.rest.pulls.get({ | ||||||||||||||||||||||||||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||||||||||||||||||||||||||
| repo: context.repo.repo, | ||||||||||||||||||||||||||||||||||||||||
| pull_number: prNumber | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| if (currentPR.labels.some(l => l.name === NO_TEST_LABEL)) { | ||||||||||||||||||||||||||||||||||||||||
| core.setFailed( | ||||||||||||||||||||||||||||||||||||||||
| `PR has the '${NO_TEST_LABEL}' label, but a paired ` + | ||||||||||||||||||||||||||||||||||||||||
| `pgxntool-test PR #${testPR.number} exists on branch '${branch}'.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `Remove the '${NO_TEST_LABEL}' label — it should only be used ` + | ||||||||||||||||||||||||||||||||||||||||
| `when there is genuinely no paired test PR.` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // A paired test PR exists. Verify its CI passed for the exact | ||||||||||||||||||||||||||||||||||||||||
| // current HEAD SHA and that the run is recent enough to be valid. | ||||||||||||||||||||||||||||||||||||||||
| const sha = testPR.head.sha; | ||||||||||||||||||||||||||||||||||||||||
| const testPRUrl = | ||||||||||||||||||||||||||||||||||||||||
| `https://github.com/${context.repo.owner}/pgxntool-test/pull/${testPR.number}`; | ||||||||||||||||||||||||||||||||||||||||
| const recheckUrl = | ||||||||||||||||||||||||||||||||||||||||
| `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}/checks`; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| core.info(`Found pgxntool-test PR #${testPR.number} (${sha.slice(0, 7)})`); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Poll until all check runs for the exact HEAD SHA complete. | ||||||||||||||||||||||||||||||||||||||||
| // Using 'ref: sha' (not branch name) ensures we only see runs for | ||||||||||||||||||||||||||||||||||||||||
| // this commit — never stale runs from an older push on the same branch. | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // We poll rather than fail immediately because both repos are often | ||||||||||||||||||||||||||||||||||||||||
| // pushed close together. When that happens, pgxntool CI starts while | ||||||||||||||||||||||||||||||||||||||||
| // pgxntool-test CI may not have queued yet. We wait up to 20 minutes. | ||||||||||||||||||||||||||||||||||||||||
| const POLL_INTERVAL_MS = 30 * 1000; | ||||||||||||||||||||||||||||||||||||||||
| const MAX_WAIT_MS = 20 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||
| const waitStart = Date.now(); | ||||||||||||||||||||||||||||||||||||||||
| let runs; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| while (true) { | ||||||||||||||||||||||||||||||||||||||||
| // per_page: 100 is intentional here — a single commit will | ||||||||||||||||||||||||||||||||||||||||
| // not realistically have 100+ CI check runs, so pagination | ||||||||||||||||||||||||||||||||||||||||
| // is unnecessary. (pulls.list uses paginate() above because | ||||||||||||||||||||||||||||||||||||||||
| // an active repo could have many open PRs.) | ||||||||||||||||||||||||||||||||||||||||
| const { data: checks } = await github.rest.checks.listForRef({ | ||||||||||||||||||||||||||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||||||||||||||||||||||||||
| repo: 'pgxntool-test', | ||||||||||||||||||||||||||||||||||||||||
| ref: sha, | ||||||||||||||||||||||||||||||||||||||||
| per_page: 100 | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| runs = checks.check_runs; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const incomplete = runs.filter(r => r.status !== 'completed'); | ||||||||||||||||||||||||||||||||||||||||
| if (runs.length > 0 && incomplete.length === 0) break; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const elapsed = Date.now() - waitStart; | ||||||||||||||||||||||||||||||||||||||||
| if (elapsed >= MAX_WAIT_MS) { | ||||||||||||||||||||||||||||||||||||||||
| const mins = Math.round(elapsed / 60000); | ||||||||||||||||||||||||||||||||||||||||
| if (runs.length === 0) { | ||||||||||||||||||||||||||||||||||||||||
| core.setFailed( | ||||||||||||||||||||||||||||||||||||||||
| `pgxntool-test PR #${testPR.number} has no CI runs for ` + | ||||||||||||||||||||||||||||||||||||||||
| `SHA ${sha.slice(0, 7)} after waiting ${mins} min.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `Push a commit (or manually re-run CI) on the test PR:\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Test PR: ${testPRUrl}\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Re-run this check: ${recheckUrl}` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| const names = incomplete.map(r => r.name).join(', '); | ||||||||||||||||||||||||||||||||||||||||
| core.setFailed( | ||||||||||||||||||||||||||||||||||||||||
| `pgxntool-test PR #${testPR.number} CI did not finish within ` + | ||||||||||||||||||||||||||||||||||||||||
| `${mins} min for SHA ${sha.slice(0, 7)}: ${names}\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Test PR: ${testPRUrl}\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Re-run this check: ${recheckUrl}` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (runs.length === 0) { | ||||||||||||||||||||||||||||||||||||||||
| core.info(`No CI runs yet for pgxntool-test PR #${testPR.number} (${sha.slice(0, 7)}); waiting 30s...`); | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| const names = incomplete.map(r => r.name).join(', '); | ||||||||||||||||||||||||||||||||||||||||
| core.info(`pgxntool-test CI still running (${names}); waiting 30s...`); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // All checks complete — look for failures. | ||||||||||||||||||||||||||||||||||||||||
| // 'success', 'skipped', 'neutral' are non-blocking. | ||||||||||||||||||||||||||||||||||||||||
| const failed = runs.filter( | ||||||||||||||||||||||||||||||||||||||||
| r => !['success', 'skipped', 'neutral'].includes(r.conclusion) | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| if (failed.length > 0) { | ||||||||||||||||||||||||||||||||||||||||
| const names = failed.map(r => `${r.name} (${r.conclusion})`).join(', '); | ||||||||||||||||||||||||||||||||||||||||
| core.setFailed( | ||||||||||||||||||||||||||||||||||||||||
| `pgxntool-test PR #${testPR.number} CI failed for ` + | ||||||||||||||||||||||||||||||||||||||||
| `SHA ${sha.slice(0, 7)}: ${names}\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `Fix the test PR CI, then re-run this check:\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Test PR: ${testPRUrl}\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Re-run this check: ${recheckUrl}` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| core.info( | ||||||||||||||||||||||||||||||||||||||||
| `pgxntool-test PR #${testPR.number} CI passed for ` + | ||||||||||||||||||||||||||||||||||||||||
| `SHA ${sha.slice(0, 7)} — tests run there, not here.` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| core.setOutput('run_tests', 'false'); | ||||||||||||||||||||||||||||||||||||||||
| core.setOutput('test_ref', sha); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // No paired test PR found. Check for the NO_TEST_LABEL label, | ||||||||||||||||||||||||||||||||||||||||
| // which a maintainer can apply when a pgxntool change genuinely | ||||||||||||||||||||||||||||||||||||||||
| // needs no test changes (unusual). | ||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||
| // We make a live API call rather than reading from the event | ||||||||||||||||||||||||||||||||||||||||
| // payload. The payload is a snapshot from when this workflow was | ||||||||||||||||||||||||||||||||||||||||
| // triggered — a maintainer may have added the label after that. | ||||||||||||||||||||||||||||||||||||||||
| const { data: pr } = await github.rest.pulls.get({ | ||||||||||||||||||||||||||||||||||||||||
| owner: context.repo.owner, | ||||||||||||||||||||||||||||||||||||||||
| repo: context.repo.repo, | ||||||||||||||||||||||||||||||||||||||||
| pull_number: prNumber | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (pr.labels.some(l => l.name === NO_TEST_LABEL)) { | ||||||||||||||||||||||||||||||||||||||||
| core.info( | ||||||||||||||||||||||||||||||||||||||||
| `'${NO_TEST_LABEL}' label is present; running tests ` + | ||||||||||||||||||||||||||||||||||||||||
| "against pgxntool-test/master. The protect-label workflow " + | ||||||||||||||||||||||||||||||||||||||||
| "ensures only maintainers can apply this label." | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| core.setOutput('run_tests', 'true'); | ||||||||||||||||||||||||||||||||||||||||
| core.setOutput('test_ref', 'master'); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Neither a paired test PR nor the override label was found. | ||||||||||||||||||||||||||||||||||||||||
| // Fail with a clear, actionable message. | ||||||||||||||||||||||||||||||||||||||||
| core.setFailed( | ||||||||||||||||||||||||||||||||||||||||
| `No paired pgxntool-test PR found for branch '${branch}', ` + | ||||||||||||||||||||||||||||||||||||||||
| `and no '${NO_TEST_LABEL}' label on this PR.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `pgxntool changes should always be paired with matching test\n` + | ||||||||||||||||||||||||||||||||||||||||
| `changes in pgxntool-test. This check enforces that pairing.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `To resolve:\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` 1. Open a PR in pgxntool-test from a branch ALSO named '${branch}'.\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Branch names must match exactly for the pairing to work.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+254
to
+256
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention the same-owner requirement in the failure guidance. Lines 96-99 require both the same branch name and the same head owner, but this remediation only tells contributors to match the branch. Following it from the wrong fork owner will still fail CI. ✏️ Proposed patch- ` 1. Open a PR in pgxntool-test from a branch ALSO named '${branch}'.\n` +
- ` Branch names must match exactly for the pairing to work.\n\n` +
+ ` 1. Open a PR in pgxntool-test from the same GitHub owner ` +
+ `('${prOwner}') using a branch ALSO named '${branch}'.\n` +
+ ` Both owner and branch name must match exactly for the pairing to work.\n\n` +📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| ` 2. If this pgxntool change truly needs no test updates (unusual),\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` ask a maintainer to apply the '${NO_TEST_LABEL}' label.\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` Only maintainers can apply this label. It is not a normal\n` + | ||||||||||||||||||||||||||||||||||||||||
| ` shortcut — most pgxntool changes require test updates.\n\n` + | ||||||||||||||||||||||||||||||||||||||||
| `See: https://github.com/Postgres-Extensions/pgxntool-test#ci-and-contributing` | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| test: | ||||||||||||||||||||||||||||||||||||||||
| needs: check-test-pr | ||||||||||||||||||||||||||||||||||||||||
| if: needs.check-test-pr.outputs.run-tests == 'true' | ||||||||||||||||||||||||||||||||||||||||
| # ----------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||
| # CROSS-REPO REUSABLE WORKFLOW — READ BEFORE CHANGING THIS REF | ||||||||||||||||||||||||||||||||||||||||
| # See: .github/workflows/CLAUDE.md for full architecture notes. | ||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||
| # The ref (@add-ci / @master) must be a static string — GitHub Actions | ||||||||||||||||||||||||||||||||||||||||
| # does not support expressions in uses:. During development on a feature | ||||||||||||||||||||||||||||||||||||||||
| # branch the ref is @<branch> so CI can find run-tests.yml before it | ||||||||||||||||||||||||||||||||||||||||
| # lands on master. Before this PR merges, two things must happen: | ||||||||||||||||||||||||||||||||||||||||
| # 1. pgxntool-test/<branch> merges to master first | ||||||||||||||||||||||||||||||||||||||||
| # 2. This ref is updated from @<branch> to @master | ||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||
| # CURRENT REF: @add-ci (temporary — pgxntool-test/add-ci not yet merged) | ||||||||||||||||||||||||||||||||||||||||
| # ----------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||
| uses: Postgres-Extensions/pgxntool-test/.github/workflows/run-tests.yml@add-ci | ||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||
| pgxntool-branch: ${{ github.event.pull_request.head.ref }} | ||||||||||||||||||||||||||||||||||||||||
| pgxntool-test-ref: master | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+264
to
+283
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
tmp="$(mktemp)"
curl -fsSL \
https://raw.githubusercontent.com/Postgres-Extensions/pgxntool-test/add-ci/.github/workflows/run-tests.yml \
-o "$tmp"
echo "=== Permission-sensitive steps in run-tests.yml@add-ci ==="
rg -n "^\s*permissions:|actions/checkout|upload-artifact|download-artifact|github-token|gh api" "$tmp"Repository: Postgres-Extensions/pgxntool Length of output: 282 🏁 Script executed: head -100 .github/workflows/ci.yml | cat -nRepository: Postgres-Extensions/pgxntool Length of output: 5374 🏁 Script executed: curl -fsSL https://raw.githubusercontent.com/Postgres-Extensions/pgxntool-test/add-ci/.github/workflows/run-tests.yml | head -100 | cat -nRepository: Postgres-Extensions/pgxntool Length of output: 5206
The workflow-level Add 🧰 Tools🪛 zizmor (1.25.2)[error] 280-280: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.