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
63 changes: 42 additions & 21 deletions .github/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides comprehensive guidance for Claude Code and human engineers wo

The Aignostics Python SDK uses a **sophisticated multi-stage CI/CD pipeline** built on GitHub Actions with:

* **19 workflow files** (8 entry points + 11 reusable workflows)
* **Multiple workflow files**, including both entry-point and reusable workflows
* **Reusable workflow architecture** for modularity and maintainability
* **Environment-based testing** (staging/production with scheduled validation)
* **Multi-category test execution** (unit, integration, e2e, long_running, very_long_running, scheduled)
Expand All @@ -21,7 +21,7 @@ The Aignostics Python SDK uses a **sophisticated multi-stage CI/CD pipeline** bu
```text
┌─────────────────────────────────────────────────────────────────────┐
│ ci-cd.yml (Main Orchestrator) │
Triggered on: push to main, PR, release, tag v*.*.*
│ Triggered on: push to main/release/v*, PR, release, tag v*.*.* │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌───────┐ ┌────────────────┐ ┌────────┐ │
Expand Down Expand Up @@ -56,6 +56,9 @@ The Aignostics Python SDK uses a **sophisticated multi-stage CI/CD pipeline** bu
┌───────────────────────────────────────────────────────────────┐
│ Parallel Entry Points │
├───────────────────────────────────────────────────────────────┤
│ prepare-release.yml → Create release branch │
│ publish-release.yml → Tag + changelog → CI/CD publish │
│ merge-release.yml → Merge branch into main │
│ build-native-only.yml → Native executables (6 platforms) │
│ claude-code-*.yml → PR reviews + interactive sessions │
│ test-scheduled-*.yml → Staging (6h) + Production (24h) │
Expand All @@ -70,7 +73,10 @@ The Aignostics Python SDK uses a **sophisticated multi-stage CI/CD pipeline** bu

| Workflow | Triggers | Purpose | Calls |
|----------|----------|---------|-------|
| **ci-cd.yml** | push(main), PR, release, tag | Main CI/CD pipeline | _lint,_audit, _test,_codeql, _ketryx,_package-publish, _docker-publish |
| **ci-cd.yml** | push(main, release/v*), PR, release, tag | Main CI/CD pipeline | _lint,_audit, _test,_codeql, _ketryx,_package-publish, _docker-publish |
| **prepare-release.yml** | workflow_dispatch | Create release branch + bump version | — |
| **publish-release.yml** | workflow_dispatch | Generate changelog, tag, push → CI/CD | — |
| **merge-release.yml** | workflow_dispatch | Merge release branch into main | — |
| **build-native-only.yml** | push, PR, release (if msg contains `build:native:only`) | Native executable builds | _build-native-only |
| **claude-code-interactive.yml** | workflow_dispatch (manual) | Manual Claude sessions | _claude-code (interactive) |
| **claude-code-automation-pr-review.yml** | PR opened/sync (excludes bots) | Automated PR reviews | _claude-code (automation) |
Expand Down Expand Up @@ -379,7 +385,6 @@ uv run pytest -m "(scheduled or scheduled_only)" -v
* `build:native:only` - Only build native executables
* `skip:test:long_running` - Skip long-running tests
* `enable:test:very_long_running` - Enable very long running tests
* `Bump version:` - Skip CI (version bump commits)

Comment thread
olivermeyer marked this conversation as resolved.
**Usage**:

Expand All @@ -398,6 +403,7 @@ git commit -m "fix: issue skip:test:long_running"
**Triggers**:

* `push` to `main` branch
* `push` to `release/v*` branches (release branch CI)
* `pull_request` to `main` (opened, synchronize, reopened)
* `release` created
* `tags` matching `v*.*.*`
Expand All @@ -415,7 +421,6 @@ Cancels in-progress runs when new commits are pushed to same PR/branch.

* Commit message contains `skip:ci`
* Commit message contains `build:native:only`
* Commit starts with `Bump version:`
* PR has label `skip:ci` or `build:native:only`

**Job Dependencies**:
Expand Down Expand Up @@ -1006,26 +1011,39 @@ make dist_native

### Releasing a Version

1. Ensure `main` branch is clean and all tests pass
2. Run version bump:
Releases use a four-phase workflow triggered from the developer's machine via `gh workflow run`. This lets Ketryx compliance approvals be collected *before* the tag (and thus before publishing to PyPI).

```bash
make bump patch # or minor, major
```
**Phase 1 — Prepare the release branch** (triggers `prepare-release.yml`):

3. This creates a commit and git tag
4. Push with tags:
```bash
make prepare-release 1.2.3 # explicit version
```

Creates `release/vX.Y.Z` from `main`, commits version bump + `uv.lock`, pushes. CI runs on the branch automatically.

**Phase 2 — Collect Ketryx approvals:**

Point the Ketryx release to `release/vX.Y.Z` and collect approvals. Ensure CI is green.

**Phase 3 — Publish** (triggers `publish-release.yml`):

```bash
make publish-release # auto-detects release/v* branch
make publish-release release/v1.2.3 # explicit branch
```

Generates `CHANGELOG.md`, creates annotated `vX.Y.Z` tag, pushes → CI/CD fires on tag → Ketryx check must pass before PyPI publish.

**Phase 4 — Merge back to main** (triggers `merge-release.yml`):

```bash
make merge-release # auto-detects release/v* branch
make merge-release release/v1.2.3 # explicit branch
```

```bash
git push --follow-tags
```
Merges `release/vX.Y.Z` into `main` with `--no-ff`, pushes `main`, deletes the release branch.

5. CI detects tag and triggers:
* Full CI pipeline (lint, audit, test, CodeQL)
* Package build and publish to PyPI
* Docker image build and publish
* GitHub release creation
* Slack notification to team
**Note on branch protection**: `release/v*` branches should be protected so that only the GitHub Actions bot (`aignostics-release-bot[bot]`) can push to them. This enforces the server-side workflow. Configure in GitHub Settings → Branches → Branch protection rules.

### Manual Testing with Claude

Expand Down Expand Up @@ -1070,6 +1088,9 @@ make dist_native
| File | Type | Purpose | Duration |
|------|------|---------|----------|
| `ci-cd.yml` | Entry | Main pipeline orchestration | ~20 min |
| `prepare-release.yml` | Entry | Create release branch + bump version | ~2 min |
| `publish-release.yml` | Entry | Generate changelog, create tag, push | ~2 min |
| `merge-release.yml` | Entry | Merge release branch into main | ~1 min |
| `build-native-only.yml` | Entry | Native build trigger | ~60 min (6 platforms) |
| `claude-code-interactive.yml` | Entry | Manual Claude sessions | varies |
| `claude-code-automation-pr-review.yml` | Entry | Automated PR reviews | ~10 min |
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- "main"
- "release/v*"
Comment thread
olivermeyer marked this conversation as resolved.
tags:
- "v*.*.*"
pull_request:
Expand Down Expand Up @@ -68,7 +69,6 @@ jobs:
if: |
(!contains(needs.get-commit-message.outputs.commit_message, 'skip:ci')) &&
(!contains(needs.get-commit-message.outputs.commit_message, 'build:native:only')) &&
!(github.ref_type == 'branch' && startsWith(needs.get-commit-message.outputs.commit_message, 'Bump version')) &&
(!contains(github.event.pull_request.labels.*.name, 'skip:ci')) &&
(!contains(github.event.pull_request.labels.*.name, 'build:native:only'))
Comment thread
olivermeyer marked this conversation as resolved.
uses: ./.github/workflows/_lint.yml
Expand All @@ -82,7 +82,6 @@ jobs:
if: |
(!contains(needs.get-commit-message.outputs.commit_message, 'skip:ci')) &&
(!contains(needs.get-commit-message.outputs.commit_message, 'build:native:only')) &&
!(github.ref_type == 'branch' && startsWith(needs.get-commit-message.outputs.commit_message, 'Bump version')) &&
(!contains(github.event.pull_request.labels.*.name, 'skip:ci')) &&
(!contains(github.event.pull_request.labels.*.name, 'build:native:only'))
uses: ./.github/workflows/_audit.yml
Expand All @@ -96,7 +95,6 @@ jobs:
if: |
(!contains(needs.get-commit-message.outputs.commit_message, 'skip:ci')) &&
(!contains(needs.get-commit-message.outputs.commit_message, 'build:native:only')) &&
!(github.ref_type == 'branch' && startsWith(needs.get-commit-message.outputs.commit_message, 'Bump version:')) &&
(!contains(github.event.pull_request.labels.*.name, 'skip:ci')) &&
(!contains(github.event.pull_request.labels.*.name, 'build:native:only'))
uses: ./.github/workflows/_test.yml
Expand All @@ -123,7 +121,6 @@ jobs:
if: |
(!contains(needs.get-commit-message.outputs.commit_message, 'skip:ci')) &&
(!contains(needs.get-commit-message.outputs.commit_message, 'build:native:only')) &&
!(github.ref_type == 'branch' && startsWith(needs.get-commit-message.outputs.commit_message, 'Bump version:')) &&
(!contains(github.event.pull_request.labels.*.name, 'skip:ci')) &&
(!contains(github.event.pull_request.labels.*.name, 'build:native:only'))
uses: ./.github/workflows/_codeql.yml
Expand All @@ -138,7 +135,6 @@ jobs:
if: |
(!contains(needs.get-commit-message.outputs.commit_message, 'skip:ci')) &&
(!contains(needs.get-commit-message.outputs.commit_message, 'build:native:only')) &&
!(github.ref_type == 'branch' && startsWith(needs.get-commit-message.outputs.commit_message, 'Bump version:')) &&
(!contains(github.event.pull_request.labels.*.name, 'skip:ci')) &&
(!contains(github.event.pull_request.labels.*.name, 'build:native:only'))
runs-on: ubuntu-latest
Expand All @@ -165,7 +161,6 @@ jobs:
github.actor != 'dependabot[bot]' &&
(!contains(needs.get-commit-message.outputs.commit_message, 'skip:ci')) &&
(!contains(needs.get-commit-message.outputs.commit_message, 'build:native:only')) &&
!(github.ref_type == 'branch' && startsWith(needs.get-commit-message.outputs.commit_message, 'Bump version:')) &&
(!contains(github.event.pull_request.labels.*.name, 'skip:ci')) &&
(!contains(github.event.pull_request.labels.*.name, 'build:native:only'))
uses: ./.github/workflows/_ketryx_report_and_check.yml
Expand Down
101 changes: 101 additions & 0 deletions .github/workflows/merge-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: "Merge Release"

on:
workflow_dispatch:
inputs:
branch:
description: 'Release branch to merge (e.g. "release/v1.1.0"). Auto-detected if omitted.'
required: false
type: string

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
merge-release:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
Comment thread
olivermeyer marked this conversation as resolved.
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}

- name: Configure git identity
run: |
git config --global user.name "aignostics-release-bot[bot]"
git config --global user.email "aignostics-release-bot[bot]@users.noreply.github.com"

- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
ref: main
fetch-depth: 0

- name: Resolve release branch
id: branch
env:
INPUT: ${{ inputs.branch }}
run: |
if [ -n "$INPUT" ]; then
BRANCH="$INPUT"
else
MATCHES=$(git ls-remote --heads origin 'release/v*' | awk '{print $2}' | sed 's|refs/heads/||')
if [ -z "$MATCHES" ]; then
echo "❌ No release/v* branches found."
exit 1
fi
COUNT=$(echo "$MATCHES" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 1 ]; then
echo "❌ Multiple release/v* branches found. Specify one explicitly:"
echo "$MATCHES"
exit 1
fi
BRANCH="$MATCHES"
fi
Comment thread
olivermeyer marked this conversation as resolved.
# Validate branch name pattern
if ! echo "$BRANCH" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "❌ Branch '${BRANCH}' does not match 'release/vX.Y.Z'. Aborting."
exit 1
fi
# Validate branch exists on origin
if ! git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
echo "❌ Branch '${BRANCH}' does not exist on origin. Aborting."
exit 1
fi
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
echo "✅ Using release branch: ${BRANCH}"

- name: Fetch remote
run: git fetch origin

- name: Merge release branch into main
run: |
BRANCH="${{ steps.branch.outputs.branch }}"
git merge --no-ff "origin/${BRANCH}" -m "chore: merge ${BRANCH} into main"

Comment on lines +75 to +82
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

After git fetch origin, the local main branch is not updated to match origin/main before merging. If new commits land on main between the initial checkout and the later git push origin main, the push can be rejected (non-fast-forward) and the workflow will fail mid-release. Consider resetting/pulling main to origin/main after the fetch (e.g., fast-forward only) before running the merge, so the merge is based on the latest remote main.

Copilot uses AI. Check for mistakes.
- name: Push main
run: git push origin main

- name: Delete remote release branch
run: git push origin --delete "${{ steps.branch.outputs.branch }}"

- name: Print job summary
run: |
BRANCH="${{ steps.branch.outputs.branch }}"
cat >> "$GITHUB_STEP_SUMMARY" << EOF
## ✅ Release merged

| | |
|---|---|
| **Branch** | \`${BRANCH}\` |
| **Merged into** | \`main\` |

The release branch has been merged into \`main\` and deleted.
EOF
110 changes: 110 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: "Prepare Release"

on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. "1.3.0")'
required: true
type: string

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
prepare-release:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
Comment thread
olivermeyer marked this conversation as resolved.
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}

- name: Configure git identity
run: |
git config --global user.name "aignostics-release-bot[bot]"
git config --global user.email "aignostics-release-bot[bot]@users.noreply.github.com"

- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
ref: main
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version-file: "pyproject.toml"
enable-cache: true
cache-dependency-glob: uv.lock

- name: Validate version
id: version
env:
INPUT: ${{ inputs.version }}
run: |
# Must be an explicit x.y.z semver
if ! echo "$INPUT" | grep -qE '^\d+\.\d+\.\d+$'; then
echo "❌ Invalid version: '$INPUT'. Must be an explicit semver like '1.3.0'."
exit 1
fi

CURRENT=$(cat VERSION)
BRANCH_NAME="release/v${INPUT}"
echo "new_version=${INPUT}" >> "$GITHUB_OUTPUT"
echo "branch_name=${BRANCH_NAME}" >> "$GITHUB_OUTPUT"
echo "current_version=${CURRENT}" >> "$GITHUB_OUTPUT"

Comment thread
olivermeyer marked this conversation as resolved.
- name: Validate release refs do not already exist
run: |
BRANCH_NAME="${{ steps.version.outputs.branch_name }}"
TAG_NAME="v${{ steps.version.outputs.new_version }}"

if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" > /dev/null 2>&1; then
echo "::error::Release branch '${BRANCH_NAME}' already exists on origin."
exit 1
fi

if git ls-remote --exit-code --tags origin "${TAG_NAME}" > /dev/null 2>&1; then
echo "::error::Release tag '${TAG_NAME}' already exists on origin."
exit 1
fi

- name: Create release branch
run: git checkout -b "${{ steps.version.outputs.branch_name }}"

- name: Bump version
run: uv run --frozen bump-my-version bump --new-version "${{ steps.version.outputs.new_version }}"

- name: Push release branch
run: git push -u origin "${{ steps.version.outputs.branch_name }}"

- name: Print next-steps summary
run: |
cat >> "$GITHUB_STEP_SUMMARY" << EOF
## ✅ Release branch created

| | |
|---|---|
| **Branch** | \`${{ steps.version.outputs.branch_name }}\` |
| **Version** | ${{ steps.version.outputs.current_version }} → ${{ steps.version.outputs.new_version }} |

### Next steps

1. Point your Ketryx release to branch \`${{ steps.version.outputs.branch_name }}\` and collect approvals.
2. Once approvals are in place, run:
\`\`\`bash
make publish-release
\`\`\`
3. After the tag CI pipeline completes successfully and the package is published, merge back:
\`\`\`bash
make merge-release
\`\`\`
EOF
Loading
Loading