Skip to content

CI — fix: decouple needs_onboarding from notebook_ready needs_onboarding now reflects only knowledge_ready — the notebook step is independent and its absence should not trigger the full onboarding flow. Fixes CI failure where ~/.devcoach/learning-state.md never exists. #102

CI — fix: decouple needs_onboarding from notebook_ready needs_onboarding now reflects only knowledge_ready — the notebook step is independent and its absence should not trigger the full onboarding flow. Fixes CI failure where ~/.devcoach/learning-state.md never exists.

CI — fix: decouple needs_onboarding from notebook_ready needs_onboarding now reflects only knowledge_ready — the notebook step is independent and its absence should not trigger the full onboarding flow. Fixes CI failure where ~/.devcoach/learning-state.md never exists. #102

Workflow file for this run

name: CI
run-name: >-
${{ github.event_name == 'workflow_dispatch'
&& format('CI — New release ({0})', inputs.version != '' && inputs.version || inputs.bump)
|| (startsWith(github.ref, 'refs/tags/v')
&& format('CI — New release ({0})', github.ref_name)
|| (github.event_name == 'pull_request'
&& format('CI - PR {0}', github.event.pull_request.title)
|| format('CI — {0}', github.event.head_commit.message))) }}
# Covers three scenarios:
#
# 1. Push to main / pull request → lint + test + build + sonar + pages
# 2. Tag push (v*) → lint + test + build + publish + GitHub Release
# 3. workflow_dispatch (Release) → bump version → tag → lint + test + build + publish + GitHub Release
#
# The bump job only runs on workflow_dispatch. Lint/test/build always run.
# Publish and GitHub Release run whenever a tag is present.
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: false
default: patch
type: choice
options: [patch, minor, major]
version:
description: "Exact version (overrides bump, e.g. 1.0.0)"
required: false
default: ""
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Bump ────────────────────────────────────────────────────────────────
# Only runs on workflow_dispatch. Bumps pyproject.toml, commits, tags, pushes.
bump:
name: Bump version and tag
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Compute next version
id: ver
run: |
if [ -n "${{ inputs.version }}" ]; then
NEXT="${{ inputs.version }}"
else
CURRENT=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ inputs.bump }}" in
major) NEXT="$((MAJOR+1)).0.0" ;;
minor) NEXT="${MAJOR}.$((MINOR+1)).0" ;;
patch) NEXT="${MAJOR}.${MINOR}.$((PATCH+1))" ;;
esac
fi
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
echo "tag=v$NEXT" >> "$GITHUB_OUTPUT"
- name: Bump version in pyproject.toml and sonar-project.properties
run: |
sed -i 's/^version = ".*"/version = "${{ steps.ver.outputs.version }}"/' pyproject.toml
sed -i 's/^sonar.projectVersion=.*/sonar.projectVersion=${{ steps.ver.outputs.version }}/' sonar-project.properties
- name: Commit and tag
run: |
git add pyproject.toml sonar-project.properties
git commit -m "chore: bump version to ${{ steps.ver.outputs.version }}"
git tag "${{ steps.ver.outputs.tag }}"
git push --atomic origin main "${{ steps.ver.outputs.tag }}"
# ── Lint ────────────────────────────────────────────────────────────────
lint:
name: Lint (ruff)
needs: [bump]
if: always() && (needs.bump.result == 'success' || needs.bump.result == 'skipped')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag || github.ref }}
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- run: uv sync --group dev
- run: uv run ruff check src/ tests/
- run: uv run ruff format --check src/ tests/
# ── Tests ───────────────────────────────────────────────────────────────
test:
name: Test (Python ${{ matrix.python-version }})
needs: [bump]
if: always() && (needs.bump.result == 'success' || needs.bump.result == 'skipped')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag || github.ref }}
- uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --group dev
- name: Run tests
run: uv run pytest tests/ -v --tb=short
# ── Build ───────────────────────────────────────────────────────────────
build:
name: Build distribution
needs: [bump, lint, test]
if: always() && (needs.bump.result == 'success' || needs.bump.result == 'skipped') && needs.lint.result == 'success' && needs.test.result == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag || github.ref }}
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- run: uv build
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
retention-days: 7
# ── Coverage comment on PR ──────────────────────────────────────────────
# Posts (and updates) a coverage summary comment on every PR push.
# Reads the .coverage file generated by pytest-cov — no extra deps needed.
coverage:
name: Coverage comment
needs: [bump]
if: >
github.event_name == 'pull_request'
&& (needs.bump.result == 'success' || needs.bump.result == 'skipped')
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- run: uv sync --group dev
- name: Run tests with coverage
run: uv run pytest tests/ --cov=src/devcoach --cov-report=xml
- uses: py-cov-action/python-coverage-comment-action@v3
with:
GITHUB_TOKEN: ${{ github.token }}
MINIMUM_GREEN: 90
MINIMUM_ORANGE: 80
# ── SonarCloud scan + quality gate ─────────────────────────────────────
# CI-based analysis: feeds coverage.xml to SonarCloud.
# Skipped on pull_request events — SONAR_TOKEN is not available to PR
# workflows (GitHub restricts secret access). Runs on main / tags / dispatch
# where the token is available.
# Requires Automatic Analysis to be DISABLED on sonarcloud.io
# (Administration → Analysis Method).
sonar:
name: SonarCloud scan
needs: [bump, test]
if: >
always()
&& github.event_name != 'pull_request'
&& (needs.bump.result == 'success' || needs.bump.result == 'skipped')
&& needs.test.result == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag || github.ref }}
fetch-depth: 0
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- run: uv sync --group dev
- name: Run tests with coverage
run: uv run pytest tests/ -v --tb=short --cov=src/devcoach --cov-report=xml
- uses: sonarsource/sonarqube-scan-action@v8
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# ── GitHub Pages ────────────────────────────────────────────────────────
# Builds MkDocs site and deploys to GitHub Pages on every push to main.
pages:
name: Deploy docs to GitHub Pages
needs: [bump, build]
if: always() && needs.build.result == 'success' && github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
- run: uv sync --group docs
- run: uv run mkdocs build
- uses: actions/upload-pages-artifact@v5
with:
path: site/
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v5
# ── Publish ─────────────────────────────────────────────────────────────
# Runs when a tag is present: either created by the bump job (workflow_dispatch)
# or pushed directly (git push origin v1.2.3).
# Uses PyPI Trusted Publishing (OIDC) — no API tokens needed.
#
# One-time setup on PyPI: project=devcoach, owner=UltimaPhoenix,
# repo=dev-coach, workflow=ci.yml, environment=pypi
publish:
name: Publish to PyPI
needs: [bump, build]
if: always() && needs.build.result == 'success' && (needs.bump.result == 'success' || startsWith(github.ref, 'refs/tags/v'))
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/devcoach
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
# ── GitHub Release ──────────────────────────────────────────────────────
github-release:
name: Create GitHub Release
needs: [bump, publish]
if: always() && needs.publish.result == 'success' && (needs.bump.result == 'success' || startsWith(github.ref, 'refs/tags/v'))
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag || github.ref }}
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.bump.outputs.tag || github.ref_name }}
files: dist/*
generate_release_notes: true
fail_on_unmatched_files: true