Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
03ce83d
docs: add CI and Contributing section to README
jnasbyupgrade May 15, 2026
9a02bfc
Add CI workflow and multi-session PR guard
jnasbyupgrade May 15, 2026
3320dd6
fix: use full clone for pgxntool checkout (git subtree requires it)
jnasbyupgrade May 15, 2026
cb08a2c
docs: require CI monitoring background task after every push
jnasbyupgrade May 15, 2026
d202419
fix: install asciidoctor via gem; add branch context reporting
jnasbyupgrade May 15, 2026
76e2536
Add /ci skill for post-push CI monitoring
jnasbyupgrade May 15, 2026
dc1631c
fix: pre-install pgtap to prevent concurrent-install race condition
jnasbyupgrade May 15, 2026
6493b3e
ci skill: add OVERALL line, exit code contract, timeout exit code
jnasbyupgrade May 15, 2026
7374c07
fix: ci skill edge cases and race condition documentation
jnasbyupgrade May 15, 2026
3c2ffee
fix: replace undefined fail() with error() in BATS tests
jnasbyupgrade May 15, 2026
62ec931
Merge fix-pgtap-tests: replace undefined fail() with error()
jnasbyupgrade May 15, 2026
dd3563d
docs: update CI docs for new check-test-pr design
jnasbyupgrade May 15, 2026
aa10c53
fix(ci): add sync warning for duplicated test job
jnasbyupgrade May 19, 2026
89fa235
refactor(ci): extract reusable workflow; fix fork support; tighten er…
jnasbyupgrade May 19, 2026
90e2cad
docs(ci): add CLAUDE.md with architecture notes; annotate reusable wo…
jnasbyupgrade May 19, 2026
1b0ed7d
fix(ci): wait 30s for SHA indexing before falling back to branch lookup
jnasbyupgrade May 19, 2026
b518ed9
fix(ci): collapse pgxntool-ref + pgxntool-branch into pgxntool-branch
jnasbyupgrade May 19, 2026
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
96 changes: 96 additions & 0 deletions .claude/skills/ci/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
name: ci
description: |
Monitor GitHub Actions CI runs for pgxntool and/or pgxntool-test after a push.
Reports which branches are under test, per-job pass/fail, and failure details.
Uses shell scripts for all heavy work to minimize context consumption.

Use when: "monitor CI", "watch CI", "check CI", "/ci"
allowed-tools: Bash(bash .claude/skills/ci/scripts/*), Read
---

# CI Monitor Skill

Monitor GitHub Actions CI across both repos after a push. Always run in background.

## Usage

- `/ci` — monitor the most recent CI run on both repos for the current branch
- `/ci pgxntool-test` — monitor pgxntool-test only
- `/ci pgxntool` — monitor pgxntool only
- `/ci <branch> <pgxntool-sha> <pgxntool-test-sha>` — monitor specific push SHAs (most reliable)

## Workflow

### 1. Start Monitor (Background)

After every `git push`, immediately launch:

```bash
bash .claude/skills/ci/scripts/monitor-ci.sh [repos] [branch] [sha1] [sha2]
```

Arguments:
- `repos`: `both` (default), `pgxntool-test`, or `pgxntool`
- `branch`: the branch just pushed (default: current git branch)
- `sha1`: SHA pushed to pgxntool-test (optional but recommended)
- `sha2`: SHA pushed to pgxntool (optional but recommended)

When pushing to both repos, always pass the SHAs to avoid a race condition where
`--branch` might pick up a different concurrent push on the same branch.

> **Race condition note**: `gh run list --branch` returns the most recent run on
> that branch — if two pushes happen close together (e.g. two sessions pushing
> in parallel), it may pick up the wrong run. Passing `--commit SHA` targets the
> exact push and avoids this. When SHA is unavailable, always verify the
> `=== BRANCHES: ===` line in the output matches the code you pushed.

**Always use `run_in_background: true`.**

### 2. Read Results

When the background task completes, read the output. The script emits:

```
[pgxntool-test] Run 12345678 found
[pgxntool-test] === BRANCHES: pgxntool-test=feature/foo pgxntool=feature/foo ===
[pgxntool-test] Polling... (running: 🐘 PostgreSQL 13, 🐘 PostgreSQL 15)
[pgxntool-test] PASS 🐘 PostgreSQL 12
[pgxntool-test] PASS 🐘 PostgreSQL 15
[pgxntool-test] FAIL 🐘 PostgreSQL 13
[pgxntool-test] Run completed: FAILURE
[pgxntool-test] === FAILURE: 🐘 PostgreSQL 13 ===
... failure log lines ...
OVERALL: FAIL
```
Comment on lines +54 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Specify a language for the fenced output block.

Line 54 uses an unlabeled fenced block, which triggers markdownlint MD040.

Suggested patch
-```
+```text
 [pgxntool-test] Run 12345678 found
 [pgxntool-test] === BRANCHES: pgxntool-test=feature/foo pgxntool=feature/foo ===
 [pgxntool-test] Polling... (running: 🐘 PostgreSQL 13, 🐘 PostgreSQL 15)
 [pgxntool-test] PASS  🐘 PostgreSQL 12
 [pgxntool-test] PASS  🐘 PostgreSQL 15
 [pgxntool-test] FAIL  🐘 PostgreSQL 13
 [pgxntool-test] Run completed: FAILURE
 [pgxntool-test] === FAILURE: 🐘 PostgreSQL 13 ===
 ... failure log lines ...
 OVERALL: FAIL
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 54-54: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/ci/SKILL.md around lines 54 - 65, The unlabeled fenced code
block in .claude/skills/ci/SKILL.md (the block starting at the shown sample
output) triggers markdownlint MD040; fix it by adding a language identifier
(e.g., "text") after the opening triple backticks so the block becomes ```text,
ensuring the fenced output is labeled and the lint rule is satisfied.


The **last line is always `OVERALL: <STATUS>`**. Check this first:

| OVERALL | Exit code | Meaning |
|---------|-----------|---------|
| `ALL_PASS` | 0 | All jobs green — safe to proceed |
| `FAIL` | 1 | One or more jobs failed — stop and report |
| `TIMEOUT` | 2 | Run(s) did not complete within timeout |

**Always verify the `=== BRANCHES ===` line** matches the code you just pushed —
this is your primary safeguard against the `--branch` race condition. If the
branches don't match, cancel the run and re-trigger: `gh run cancel <id> --repo
<repo>` then re-push or re-run via `gh run rerun`.

### 3. Enforce Results

**CRITICAL RULES:**

1. Any CI failure must be **reported to the user immediately**. Do not continue with other work.
2. Start diagnosis from the **first** `not ok` line to understand the root cause, but do not assume later failures are cascading or caused by it — treat each failure as likely real and needing its own investigation. Failures in separate test files are typically unrelated; even multiple failures within the same file may be independent.
3. Failures in our workflow files (dependency installs, git config, etc.) are our problem to fix.
4. Failures in test code (not ok from BATS) may be pre-existing — report to user and ask before touching test files.
5. Never rationalize failures as "pre-existing" or "unrelated" without explicitly telling the user.
6. If CI is taking longer than expected on pgxntool, it may be waiting up to 20 min for pgxntool-test CI to complete — that is normal.

## Key rules

1. **ALWAYS** monitor CI after every push — use this skill, never `gh run watch` directly
2. When pushing to both repos, start two background monitors simultaneously (one per repo)
3. Pass the exact push SHA when available — `--branch` has a race condition on rapid pushes
4. The `=== BRANCHES ===` line in the output confirms which code is under test — always verify it matches your intent
221 changes: 221 additions & 0 deletions .claude/skills/ci/scripts/monitor-ci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
#!/usr/bin/env bash
# monitor-ci.sh [repos] [branch] [sha_pgxntool_test] [sha_pgxntool]
#
# Monitor GitHub Actions CI runs for pgxntool-test and/or pgxntool.
# Designed to be run in background by Claude after every git push.
#
# Arguments:
# repos : "both" (default), "pgxntool-test", or "pgxntool"
# branch : branch name (default: current git branch)
# sha_pgxntool_test: exact SHA pushed to pgxntool-test (optional)
# sha_pgxntool : exact SHA pushed to pgxntool (optional)
#
# Exit codes:
# 0 : ALL_PASS — all jobs succeeded
# 1 : FAIL — one or more jobs failed
# 2 : TIMEOUT — run(s) did not complete within the timeout
# 3 : NO_RUNS — no CI run found for this branch after waiting
Comment on lines +13 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Exit code 3 (NO_RUNS) is documented but never returned.

The header documents exit code 3 for "no CI run found," but line 80 returns 1 in that case. Either update the documentation to remove exit code 3, or update line 80 to return 3.

Option A: Update documentation to match implementation
 # Exit codes:
 #   0 : ALL_PASS  — all jobs succeeded
-#   1 : FAIL      — one or more jobs failed
+#   1 : FAIL      — one or more jobs failed, or no CI run found
 #   2 : TIMEOUT   — run(s) did not complete within the timeout
-#   3 : NO_RUNS   — no CI run found for this branch after waiting
Option B: Update implementation to match documentation
       if [[ $elapsed -ge $timeout ]]; then
         echo "$label ERROR: no CI run found after ${timeout}s" >&2
-        return 1
+        return 3
       fi

Also update the final status reporting (lines 213-218) to handle exit code 3:

 if [[ $exit_code -eq 0 ]]; then
   echo "OVERALL: ALL_PASS"
 elif [[ $exit_code -eq 2 ]]; then
   echo "OVERALL: TIMEOUT"
+elif [[ $exit_code -eq 3 ]]; then
+  echo "OVERALL: NO_RUNS"
 else
   echo "OVERALL: FAIL"
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Exit codes:
# 0 : ALL_PASS — all jobs succeeded
# 1 : FAIL — one or more jobs failed
# 2 : TIMEOUT — run(s) did not complete within the timeout
# 3 : NO_RUNS — no CI run found for this branch after waiting
# Exit codes:
# 0 : ALL_PASS — all jobs succeeded
# 1 : FAIL — one or more jobs failed, or no CI run found
# 2 : TIMEOUT — run(s) did not complete within the timeout
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/ci/scripts/monitor-ci.sh around lines 13 - 17, The header of
monitor-ci.sh documents exit code 3 (NO_RUNS) but the script returns 1 in the
"no CI run found" branch; either remove exit code 3 from the comment block or
implement it: change the branch that currently returns 1 for "no runs found" to
exit 3 (NO_RUNS) and update the final status reporting logic (the block that
handles exit codes near the end of the script) to include a case for exit code 3
with an appropriate message; locate the "no CI run found" return in the
polling/wait loop and the final status switch/if that reports statuses and
update both so comments and behavior match.

#
# Requires: gh CLI authenticated with repo access.

set -euo pipefail

REPOS="${1:-both}"
BRANCH="${2:-$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")}"
SHA_TEST="${3:-}"
SHA_PGXN="${4:-}"

# Derive owner from the current repo (works for forks too)
_current_repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)
_owner=$(echo "$_current_repo" | cut -d/ -f1)
if [[ -z "$_owner" ]]; then
# fallback if gh can't determine the repo
_owner="Postgres-Extensions"
fi
REPO_TEST="${_owner}/pgxntool-test"
REPO_PGXN="${_owner}/pgxntool"

# pgxntool CI can wait up to 20 min for pgxntool-test CI to complete, then
# runs tests itself (commit-with-no-tests case). Allow 35 min total.
# pgxntool-test runs typically take 5-10 min (resolve + 6 PG matrix jobs).
TIMEOUT_TEST=900 # 15 minutes
TIMEOUT_PGXN=2100 # 35 minutes
POLL_INTERVAL=10 # seconds between status polls

# ─── Helper: wait for a run to appear, then poll until done ──────────────────
monitor_one() {
local repo="$1"
local branch="$2"
local sha="$3"
local timeout="$4"
local label="[$repo]"
local elapsed=0

# Step 1: find the run ID.
# When a SHA is provided, wait up to 30s for GitHub to index that exact run
# before falling back to the branch lookup. Without this wait, rapid pushes
# cause the branch fallback to pick up the previous run instead of the new one.
local run_id=""
local sha_wait=0
local SHA_INDEX_WAIT=30 # seconds to wait for SHA indexing before branch fallback
echo "$label Waiting for CI run on branch '$branch'..."
while [[ -z "$run_id" ]]; do
if [[ -n "$sha" ]]; then
run_id=$(gh run list --repo "$repo" --commit "$sha" \
--json databaseId --jq '.[0].databaseId // empty' 2>/dev/null || true)
fi
if [[ -z "$run_id" && -n "$branch" && ( -z "$sha" || $sha_wait -ge $SHA_INDEX_WAIT ) ]]; then
# Only fall back to branch once the SHA wait window has elapsed (or no SHA given).
# NOTE: this can pick up a different run if two pushes happen rapidly.
run_id=$(gh run list --repo "$repo" --branch "$branch" \
--event pull_request --limit 1 \
--json databaseId --jq '.[0].databaseId // empty' 2>/dev/null || true)
fi
if [[ -z "$run_id" ]]; then
sleep 5
elapsed=$((elapsed + 5))
sha_wait=$((sha_wait + 5))
if [[ $elapsed -ge $timeout ]]; then
echo "$label ERROR: no CI run found after ${timeout}s" >&2
return 1
fi
fi
done
echo "$label Run $run_id found"

# Step 2: extract the BRANCHES line as soon as the first job starts.
# We use the direct jobs API (fast ~1s) rather than the zip-download log path
# (slow 3-10s). We only need one job — all jobs emit the same BRANCHES line.
local branches_line=""
local attempts=0
while [[ -z "$branches_line" && $elapsed -lt $timeout ]]; do
local first_job_id
first_job_id=$(gh run view "$run_id" --repo "$repo" \
--json jobs --jq '[.jobs[].databaseId][0] // empty' 2>/dev/null || true)

if [[ -n "$first_job_id" ]]; then
# grep may return non-zero if the line isn't present yet — that's fine.
branches_line=$(gh api "repos/${repo}/actions/jobs/${first_job_id}/logs" \
2>/dev/null | grep "^=== BRANCHES:" | tail -1 || true)
fi

if [[ -z "$branches_line" ]]; then
attempts=$((attempts + 1))
if [[ $attempts -ge 3 ]]; then
# Give up waiting for the BRANCHES line and move on to polling.
echo "$label (BRANCHES line not yet available; proceeding to poll)"
break
fi
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
fi
done
if [[ -n "$branches_line" ]]; then
echo "$label $branches_line"
fi

# Step 3: poll until all jobs complete.
local status="in_progress"
local result=""
while [[ "$status" != "completed" && $elapsed -lt $timeout ]]; do
result=$(gh run view "$run_id" --repo "$repo" \
--json status,conclusion,jobs \
--jq '{status: .status, conclusion: .conclusion,
jobs: [.jobs[] | {name: .name, status: .status, conclusion: .conclusion}]}' \
2>/dev/null || true)

if [[ -z "$result" ]]; then
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
continue
fi

status=$(echo "$result" | jq -r '.status')

if [[ "$status" != "completed" ]]; then
local running
running=$(echo "$result" | jq -r \
'[.jobs[] | select(.status == "in_progress") | .name] | join(", ")' || true)
if [[ -n "$running" ]]; then
echo "$label Polling... (running: $running)"
fi
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
fi
done

if [[ $elapsed -ge $timeout ]]; then
echo "$label ERROR: timed out after ${timeout}s" >&2
return 2
fi

# Step 4: report per-job outcomes.
local conclusion
conclusion=$(echo "$result" | jq -r '.conclusion')
echo "$label Run $run_id completed: $(echo "$conclusion" | tr '[:lower:]' '[:upper:]')"
echo "$result" | jq -r '.jobs[] | "\(if .conclusion == "success" then "PASS" elif .conclusion == null then .status else .conclusion | ascii_upcase end) \(.name)"' \
| sed "s|^|$label |"

# Step 5: for failed jobs, print the failure log (last 60 lines per job).
if [[ "$conclusion" != "success" ]]; then
local failed_job_ids
failed_job_ids=$(gh run view "$run_id" --repo "$repo" \
--json jobs \
--jq '[.jobs[] | select(.conclusion == "failure") | .databaseId] | .[]' \
2>/dev/null || true)

for job_id in $failed_job_ids; do
local job_name
job_name=$(gh run view "$run_id" --repo "$repo" \
--json jobs \
--jq --argjson id "$job_id" \
'[.jobs[] | select(.databaseId == $id) | .name] | .[0]' 2>/dev/null || true)
echo ""
echo "$label === FAILURE: ${job_name:-job $job_id} ==="
# Use --log-failed to get only the failed step output, keeping output compact.
gh run view --repo "$repo" --job "$job_id" --log-failed 2>&1 \
| grep -v "^$" | tail -60 || true
done

return 1
fi

return 0
}

# ─── Main: run monitors in parallel or series ─────────────────────────────────
exit_code=0
pid_test=""
pid_pgxn=""

case "$REPOS" in
pgxntool-test)
monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" || exit_code=1
;;
pgxntool)
monitor_one "$REPO_PGXN" "$BRANCH" "$SHA_PGXN" "$TIMEOUT_PGXN" || exit_code=1
;;
Comment on lines +192 to +197
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve monitor_one return codes in single-repo mode.

Line 189 and Line 192 force any non-zero result to 1, so timeout (2) is downgraded to fail and OVERALL: TIMEOUT cannot occur for single-repo runs.

Suggested patch
 case "$REPOS" in
   pgxntool-test)
-    monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" || exit_code=1
+    monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" \
+      || { r=$?; [[ $r -gt $exit_code ]] && exit_code=$r; }
     ;;
   pgxntool)
-    monitor_one "$REPO_PGXN" "$BRANCH" "$SHA_PGXN" "$TIMEOUT_PGXN" || exit_code=1
+    monitor_one "$REPO_PGXN" "$BRANCH" "$SHA_PGXN" "$TIMEOUT_PGXN" \
+      || { r=$?; [[ $r -gt $exit_code ]] && exit_code=$r; }
     ;;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pgxntool-test)
monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" || exit_code=1
;;
pgxntool)
monitor_one "$REPO_PGXN" "$BRANCH" "$SHA_PGXN" "$TIMEOUT_PGXN" || exit_code=1
;;
pgxntool-test)
monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" \
|| { r=$?; [[ $r -gt $exit_code ]] && exit_code=$r; }
;;
pgxntool)
monitor_one "$REPO_PGXN" "$BRANCH" "$SHA_PGXN" "$TIMEOUT_PGXN" \
|| { r=$?; [[ $r -gt $exit_code ]] && exit_code=$r; }
;;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/ci/scripts/monitor-ci.sh around lines 188 - 193, The case
arms for 'pgxntool-test' and 'pgxntool' currently force any non-zero exit to 1
(using "|| exit_code=1"), which loses distinct exit codes like timeout (2);
change each arm to call monitor_one and capture its exact exit status (e.g., run
monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" then set
exit_code to its $? only if non-zero) so monitor_one's return codes are
preserved; reference the monitor_one invocation in the pgxntool-test and
pgxntool case branches and ensure you do not coerce non-zero results to 1.

both|*)
# Run both in parallel. Each writes to stdout (interleaved but prefixed with
# the repo name for readability). Capture both PIDs and wait for both.
monitor_one "$REPO_TEST" "$BRANCH" "$SHA_TEST" "$TIMEOUT_TEST" &
pid_test=$!
monitor_one "$REPO_PGXN" "$BRANCH" "$SHA_PGXN" "$TIMEOUT_PGXN" &
pid_pgxn=$!

wait "$pid_test" || { r=$?; echo "[both] pgxntool-test CI FAILED"; [[ $r -gt $exit_code ]] && exit_code=$r; }
wait "$pid_pgxn" || { r=$?; echo "[both] pgxntool CI FAILED"; [[ $r -gt $exit_code ]] && exit_code=$r; }
;;
esac

# Emit a parseable summary line. Claude should check this line rather than
# parsing the full output. Convention matches the test skill's STATUS line.
if [[ $exit_code -eq 0 ]]; then
echo "OVERALL: ALL_PASS"
elif [[ $exit_code -eq 2 ]]; then
echo "OVERALL: TIMEOUT"
else
echo "OVERALL: FAIL"
fi

exit $exit_code
57 changes: 57 additions & 0 deletions .github/workflows/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# .github/workflows — CI Architecture

## Workflow files

- **`ci.yml`** — main CI for pgxntool-test pull requests. Runs a `resolve` job
(determines which pgxntool branch to test against), then calls `run-tests.yml`.
- **`run-tests.yml`** — reusable workflow (`workflow_call`). Single source of truth
for all test steps (PostgreSQL matrix, checkouts, git config, pgtap install, etc.).
Called by both `ci.yml` here and by `pgxntool/ci.yml` for the commit-with-no-tests path.

## Cross-repo reusable workflow — tradeoffs and constraints

`run-tests.yml` is referenced from **pgxntool** as:
```yaml
uses: Postgres-Extensions/pgxntool-test/.github/workflows/run-tests.yml@<ref>
```

GitHub Actions requires the `uses:` ref to be a **static string** — expressions are
not supported. This creates an unavoidable structural constraint:

### Merge order requirement

**pgxntool-test MUST be merged before pgxntool** whenever both repos change in the
same feature branch. Here's why:

- pgxntool's `ci.yml` pins to `run-tests.yml@master`
- While developing on a branch, pgxntool's `ci.yml` temporarily uses `@<branch>`
- When it's time to merge, pgxntool-test must land on master first so that
`run-tests.yml@master` exists before pgxntool's CI tries to use it

### What this means for `run-tests.yml` changes

- Changes to `run-tests.yml` are always tested through **pgxntool-test's own CI**
(which uses `./.github/workflows/run-tests.yml` — a local ref that always sees
the current branch version).
- pgxntool's CI (commit-with-no-tests path) uses `run-tests.yml@master`. Until
pgxntool-test merges, pgxntool CI will use the old master version.
- These two scenarios are mutually exclusive in practice: the commit-with-no-tests
path only runs when there is NO paired test PR. If you're changing `run-tests.yml`,
you have a paired test PR, so pgxntool's test job is skipped anyway.

### The @branch → @master transition

While developing on a feature branch, pgxntool's `ci.yml` uses `@<branch>` so CI
can find `run-tests.yml` before it lands on master. Before pgxntool can be merged,
pgxntool-test must merge first and then pgxntool's ref must be updated to `@master`.

**For Claude**: This transition is NOT automatic. You must get explicit user approval
before leaving a `@<branch>` ref in pgxntool's `ci.yml`. Do not assume this will be
handled at merge time — the user merges directly from the PR page with no manual steps.
The safe approach is to coordinate the merge order explicitly with the user.

## Expanding the matrix

The PostgreSQL version matrix (`pg: [17, 16, 15, 14, 13, 12]`) is hardcoded in
`run-tests.yml`. GitHub Actions does not support passing a matrix as a workflow_call
input. To add or remove a PG version, edit `run-tests.yml` directly.
Loading
Loading