Skip to content

Commit 8e6c39f

Browse files
committed
CI: add per-commit workflow with sequential testing
Single job that checks out each commit in the range and runs the full CI suite (lint, format, sort, typecheck, test) sequentially. Triggered by the ci:per-commit label on PRs and on push to main.
1 parent 4c870ed commit 8e6c39f

4 files changed

Lines changed: 131 additions & 45 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
# Check if the ci:per-commit label is present.
3+
#
4+
# For pull_request events, checks labels from the event payload.
5+
# For push events, looks up the originating PR via the GitHub API.
6+
#
7+
# Usage:
8+
# check-ci-per-commit-label.sh <event_name> <repository> <sha> [labels_json]
9+
#
10+
# Arguments:
11+
# event_name - "pull_request" or "push"
12+
# repository - e.g. "owner/repo"
13+
# sha - commit SHA
14+
# labels_json - JSON array of label names (required for pull_request)
15+
#
16+
# Output:
17+
# Prints has_label=true or has_label=false (for GITHUB_OUTPUT)
18+
set -euo pipefail
19+
20+
event_name="${1:?Usage: check-ci-per-commit-label.sh <event_name> <repository> <sha> [labels_json]}"
21+
repository="${2:?Missing repository}"
22+
sha="${3:?Missing sha}"
23+
labels_json="${4:-}"
24+
25+
HAS_LABEL="false"
26+
27+
if [ "$event_name" = "pull_request" ]; then
28+
if echo "$labels_json" | grep -q "ci:per-commit"; then
29+
HAS_LABEL="true"
30+
fi
31+
else
32+
PRS=$(gh api \
33+
"repos/${repository}/commits/${sha}/pulls" \
34+
--jq '.[].number')
35+
for pr in $PRS; do
36+
LABELS=$(gh api \
37+
"repos/${repository}/pulls/${pr}" \
38+
--jq '.labels[].name')
39+
if echo "$LABELS" | grep -q "^ci:per-commit$"; then
40+
HAS_LABEL="true"
41+
break
42+
fi
43+
done
44+
fi
45+
46+
echo "has_label=${HAS_LABEL}"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
# Run the full CI checks on each commit in the given range, sequentially.
3+
# Stops at the first commit that fails.
4+
#
5+
# Usage:
6+
# run-ci-per-commit.sh <base_sha> <head_sha>
7+
#
8+
# Arguments:
9+
# base_sha - the base commit (exclusive)
10+
# head_sha - the head commit (inclusive)
11+
set -euo pipefail
12+
13+
base="${1:?Usage: run-ci-per-commit.sh <base_sha> <head_sha>}"
14+
head="${2:?Missing head_sha}"
15+
16+
commits=$(git rev-list --reverse "${base}..${head}")
17+
total=$(echo "$commits" | wc -l | tr -d ' ')
18+
current=0
19+
20+
for commit in $commits; do
21+
current=$((current + 1))
22+
short=$(git rev-parse --short "$commit")
23+
subject=$(git log -1 --format=%s "$commit")
24+
echo ""
25+
echo "=== [$current/$total] Testing ${short}: ${subject} ==="
26+
echo ""
27+
28+
git checkout --quiet "$commit"
29+
make install
30+
make lint
31+
make check-format
32+
make check-sort
33+
make typecheck
34+
make test
35+
36+
echo ""
37+
echo "=== [$current/$total] PASSED: ${short} ==="
38+
done
39+
40+
echo ""
41+
echo "All ${total} commit(s) passed CI checks."

.github/workflows/action.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ jobs:
1616
with:
1717
python-version: ${{ matrix.python-version }}
1818
- name: Install dependencies
19-
run: uv sync
19+
run: make install
2020
- name: lint
21-
run: uv run ruff check .
21+
run: make lint
2222
- name: format check
23-
run: uv run black --check .
23+
run: make check-format
2424
- name: sort-check
25-
run: uv run isort --check-only .
25+
run: make check-sort
2626
- name: typecheck
27-
run: uv run mypy l9format
27+
run: make typecheck
2828
- name: test
29-
run: uv run pytest
29+
run: make test
3030
- name: security-audit
31-
run: uv run pip-audit
31+
run: make security-check
3232

.github/workflows/ci-per-commit.yaml

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ on:
44
push:
55
branches:
66
- main
7+
pull_request:
8+
types: [labeled]
9+
branches:
10+
- main
711

812
jobs:
913
check-label:
@@ -12,53 +16,48 @@ jobs:
1216
outputs:
1317
has_label: ${{ steps.check.outputs.has_label }}
1418
steps:
15-
- name: Find associated PR and check label
19+
- uses: actions/checkout@v6
20+
- name: Check for label
1621
id: check
1722
env:
1823
GH_TOKEN: ${{ github.token }}
1924
run: |
20-
PRS=$(gh api \
21-
"repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \
22-
--jq '.[].number')
23-
HAS_LABEL="false"
24-
for pr in $PRS; do
25-
LABELS=$(gh api \
26-
"repos/${{ github.repository }}/pulls/${pr}" \
27-
--jq '.labels[].name')
28-
if echo "$LABELS" | grep -q "^ci:per-commit$"; then
29-
HAS_LABEL="true"
30-
break
31-
fi
32-
done
33-
echo "has_label=${HAS_LABEL}" >> "$GITHUB_OUTPUT"
25+
.github/scripts/check-ci-per-commit-label.sh \
26+
"${{ github.event_name }}" \
27+
"${{ github.repository }}" \
28+
"${{ github.sha }}" \
29+
'${{ toJSON(github.event.pull_request.labels.*.name) }}' \
30+
>> "$GITHUB_OUTPUT"
3431
35-
ci:
36-
name: CI
32+
ci-per-commit:
33+
name: CI per commit
3734
needs: check-label
3835
if: needs.check-label.outputs.has_label == 'true'
39-
strategy:
40-
fail-fast: false
41-
matrix:
42-
python-version: ["3.11", "3.12", "3.13"]
43-
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
44-
runs-on: ${{ matrix.os }}
36+
runs-on: ubuntu-latest
4537
steps:
4638
- uses: actions/checkout@v6
39+
with:
40+
fetch-depth: 0
4741
- name: Install uv
4842
uses: astral-sh/setup-uv@v6
4943
with:
50-
python-version: ${{ matrix.python-version }}
51-
- name: Install dependencies
52-
run: uv sync
53-
- name: lint
54-
run: uv run ruff check .
55-
- name: format check
56-
run: uv run black --check .
57-
- name: sort-check
58-
run: uv run isort --check-only .
59-
- name: typecheck
60-
run: uv run mypy l9format
61-
- name: test
62-
run: uv run pytest
63-
- name: security-audit
64-
run: uv run pip-audit
44+
python-version: "3.13"
45+
- name: Determine commit range
46+
id: range
47+
env:
48+
GH_TOKEN: ${{ github.token }}
49+
run: |
50+
if [ "${{ github.event_name }}" = "pull_request" ]; then
51+
BASE="${{ github.event.pull_request.base.sha }}"
52+
HEAD="${{ github.event.pull_request.head.sha }}"
53+
else
54+
BASE="${{ github.event.before }}"
55+
HEAD="${{ github.sha }}"
56+
fi
57+
echo "base=${BASE}" >> "$GITHUB_OUTPUT"
58+
echo "head=${HEAD}" >> "$GITHUB_OUTPUT"
59+
- name: Run CI on each commit
60+
run: |
61+
.github/scripts/run-ci-per-commit.sh \
62+
"${{ steps.range.outputs.base }}" \
63+
"${{ steps.range.outputs.head }}"

0 commit comments

Comments
 (0)