Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@ updates:
directory: "/"
schedule:
interval: "weekly"
assignees:
- "eschaar"
open-pull-requests-limit: 10
labels:
- "dependencies"
- "python"
commit-message:
prefix: "chore(deps)"
groups:
pip-patch-minor:
patterns:
- "*"
update-types:
- "patch"
- "minor"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
assignees:
- "eschaar"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "chore(ci)"
groups:
gha-patch-minor:
patterns:
- "*"
update-types:
- "patch"
- "minor"
26 changes: 26 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# GitHub Workflows

This folder contains repository automation workflows.

For the full CI/CD design and release model, see `docs/design/cicd.md`.

## Quick Map

| Workflow | Trigger | Purpose |
| --- | --- | --- |
| `commit.yml` | push to non-main branches and PRs to `main` | Commit/branch policy and lint/typecheck |
| `check.yml` | push to non-main branches and PRs to `main` | Single-version unit test feedback (py3.11) |
| `verify.yml` | pull_request to `main` | Required verification checks: cross-version test matrix + artifact verify |
| `security.yml` | pull_request to `main` | Required security checks |
| `automerge.yml` | pull_request_target to `main` | Dependabot safe auto-merge policy |
| `release.yml` | push to `main` | Release Please orchestration |
| `publish.yml` | release published | Build and publish to PyPI |

## Operational Notes

1. `commit.yml` handles commit policy and lint/typecheck on branch pushes and PRs.
2. `check.yml` provides single-version test feedback on branch pushes and PRs.
3. `verify.yml` and `security.yml` are the required PR gates.
4. `release.yml` is the only release orchestrator.
5. `publish.yml` is publish-only and never computes versions.
6. Ruleset on `main` should require `Commit`, `Check`, `Verify` (all jobs), and `Security` before merge.
93 changes: 93 additions & 0 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: "Dependabot Safe Auto-merge for Patch and Minor Updates"

on:
pull_request_target:
branches:
- main
types:
- opened
- synchronize
- reopened

permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest

steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Decide if this PR is eligible
id: decision
env:
PACKAGE_ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }}
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
run: |
should_automerge=false

if [[ "$PACKAGE_ECOSYSTEM" == "github-actions" ]]; then
if [[ "$UPDATE_TYPE" == "version-update:semver-patch" || "$UPDATE_TYPE" == "version-update:semver-minor" ]]; then
should_automerge=true
fi
fi

if [[ "$PACKAGE_ECOSYSTEM" == "pip" ]]; then
if [[ "$UPDATE_TYPE" == "version-update:semver-patch" ]]; then
should_automerge=true
fi
fi

echo "should_automerge=$should_automerge" >> "$GITHUB_OUTPUT"
echo "package_ecosystem=$PACKAGE_ECOSYSTEM" >> "$GITHUB_OUTPUT"
echo "update_type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT"

- name: Approve eligible PR
if: steps.decision.outputs.should_automerge == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
event: "APPROVE",
body: "Auto-approved for safe Dependabot update policy."
})

- name: Enable auto-merge for eligible PR
if: steps.decision.outputs.should_automerge == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
try {
await github.graphql(
`mutation($pullRequestId: ID!) {
enablePullRequestAutoMerge(input: {pullRequestId: $pullRequestId, mergeMethod: SQUASH}) {
pullRequest { number }
}
}`,
{ pullRequestId: context.payload.pull_request.node_id }
)
} catch (error) {
core.setFailed(
"Could not enable auto-merge. Ensure repository auto-merge is enabled and branch protections are satisfied.\n" +
error.message
)
}

- name: Log skipped PR
if: steps.decision.outputs.should_automerge != 'true'
run: |
echo "Automerge skipped by policy."
echo "ecosystem=${{ steps.decision.outputs.package_ecosystem }}"
echo "update_type=${{ steps.decision.outputs.update_type }}"
55 changes: 55 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Check workflow.
# Purpose: run fast single-version unit tests for branch and PR feedback.
# This complements verify.yml, which runs the full cross-version matrix and artifact checks.
name: "Check"

on:
push:
# Mainline, merge-queue, and release-please refs are covered by other workflows.
branches-ignore:
- main
- master
- merge/**
- gh-readonly-queue/**
- release-please--branches--**
pull_request:
branches: [main]

concurrency:
# Cancel superseded runs for the same ref.
group: check-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
PYTHON_VERSION: "3.11"
POETRY_VERSION: "2.3.4"
POETRY_VIRTUALENVS_IN_PROJECT: "true"

jobs:
test:
# Single-version unit test run for fast feedback.
name: Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install Poetry
run: pipx install "poetry==${POETRY_VERSION}"

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
cache-dependency-path: poetry.lock

- name: Install dependencies
run: poetry install --no-interaction --no-ansi

- name: Test
run: make test-local
125 changes: 117 additions & 8 deletions .github/workflows/commit.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
# Commit workflow.
# Purpose: fail fast on branch pushes when commit messages do not match
# repository commit conventions.
# Purpose: enforce commit policy and quality checks (format/lint/typecheck).
# Runs on branch pushes and PRs so commit/quality policy can be merge-blocking.
# Unit tests are split into check.yml (single-version) and verify.yml (matrix + artifact verify).
name: Commit

on:
# Validate commits on branch pushes before PR merge.
push:
# Mainline and merge-queue refs are validated by PR/release workflows.
# Mainline, merge-queue, and release-please refs are covered by other workflows.
branches-ignore:
- main
- master
- merge/**
- gh-readonly-queue/**
- release-please--branches--**
pull_request:
branches: [main]

concurrency:
# Cancel superseded commit-message checks on the same branch.
# Cancel superseded runs on the same branch.
group: commit-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
# Read-only is enough for commit metadata checks.
contents: read

env:
PYTHON_VERSION: "3.11"
POETRY_VERSION: "2.3.4"
POETRY_VIRTUALENVS_IN_PROJECT: "true"

jobs:
validate-commit-messages:
# Single gate that validates message format/types and custom scope policy.
validate:
# Validates commit message format, branch naming, and reserved scope policy.
name: Validate Commit Messages
runs-on: ubuntu-latest
steps:
Expand All @@ -35,6 +42,7 @@ jobs:
fetch-depth: 0

- name: Validate commits with commit-check
if: github.event_name == 'push'
uses: commit-check/commit-check-action@v2
with:
# Commit and branch policy is read from cchk.toml in repo root.
Expand All @@ -44,3 +52,104 @@ jobs:
author-email: false
job-summary: true
pr-comments: false

- name: Validate commit messages with commit-check
if: github.event_name == 'pull_request'
uses: commit-check/commit-check-action@v2
with:
# On PR events, validate commit messages only (branch refs are pull/*).
message: true
branch: false
author-name: false
author-email: false
job-summary: true
pr-comments: false

- name: Validate PR branch name
if: github.event_name == 'pull_request'
shell: bash
run: |
BRANCH="${{ github.head_ref }}"

# Release-please uses its own generated branch naming format.
if [[ "$BRANCH" =~ ^release-please--branches--.+$ ]]; then
echo "release-please branch is allowed: $BRANCH"
exit 0
fi

if [[ ! "$BRANCH" =~ ^([a-z0-9-]+)/.+$ ]]; then
echo "ERROR: invalid branch name '$BRANCH'. Expected 'type/description'."
exit 1
fi

TYPE="${BASH_REMATCH[1]}"
case "$TYPE" in
feature|bugfix|hotfix|release|chore|feat|fix|docs|refactor|perf|test|ci|build|style|opt|patch|dependabot)
echo "branch type '$TYPE' is allowed."
;;
*)
echo "ERROR: branch type '$TYPE' is not allowed."
echo "Allowed types: feature bugfix hotfix release chore feat fix docs refactor perf test ci build style opt patch dependabot"
exit 1
;;
esac

- name: Reserve docs(changelog) scope for automation
shell: bash
run: |
TRUSTED_AUTHORS='^(github-actions\[bot\]|vstack-release-bot\[bot\])$'

if [[ "${{ github.event_name }}" == "pull_request" ]]; then
COMMITS="$(git log --format='%h%x09%s%x09%an' "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}")"
else
if [[ "${GITHUB_ACTOR}" =~ $TRUSTED_AUTHORS ]]; then
echo "docs(changelog) scope allowed for automation actor."
exit 0
fi
COMMITS="$(git log -1 --format='%h%x09%s%x09%an' "${{ github.sha }}")"
fi

VIOLATION_FOUND=false
while IFS=$'\t' read -r SHA SUBJECT AUTHOR; do
[[ "$SUBJECT" =~ ^docs\(changelog\): ]] || continue
[[ "$AUTHOR" =~ $TRUSTED_AUTHORS ]] && continue
echo "ERROR: commit $SHA uses reserved docs(changelog) scope (author: $AUTHOR)."
VIOLATION_FOUND=true
done <<< "$COMMITS"

if [[ "$VIOLATION_FOUND" == "true" ]]; then
echo "ERROR: docs(changelog) scope is reserved for automated changelog/release commits."
echo "Use another scope for manual documentation commits."
exit 1
fi

quality:
# Lint, format, and typecheck on the baseline Python version.
name: Format Lint Typecheck
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install Poetry
run: pipx install "poetry==${POETRY_VERSION}"

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: poetry
cache-dependency-path: poetry.lock

- name: Install dependencies
run: poetry install --no-interaction --no-ansi

- name: Format check
run: make format-check

- name: Lint
run: make lint

- name: Typecheck
run: make typecheck
Loading
Loading