Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59d6fa1833 | ||
|
|
d035b620e0 | ||
|
|
5987adee64 | ||
|
|
cb84dae050 | ||
|
|
ed0dfce98b |
@@ -10,9 +10,11 @@ name: Build Images
|
|||||||
# to PyPI, so the image always has the latest released version.
|
# to PyPI, so the image always has the latest released version.
|
||||||
# - Manually via workflow_dispatch
|
# - Manually via workflow_dispatch
|
||||||
#
|
#
|
||||||
|
# Consolidated into 2 jobs (from 3):
|
||||||
|
# build-and-push (includes release-commit detection) ──→ cleanup
|
||||||
|
#
|
||||||
# The workflow builds 3 tier images in sequence:
|
# The workflow builds 3 tier images in sequence:
|
||||||
# ci-base → ci-quality → ci-full
|
# ci-base → ci-quality → ci-full
|
||||||
#
|
|
||||||
# Each tier builds FROM the previous one, so they must be built in order.
|
# Each tier builds FROM the previous one, so they must be built in order.
|
||||||
# After pushing, a cleanup job removes old versions (keeps last 2 + latest).
|
# After pushing, a cleanup job removes old versions (keeps last 2 + latest).
|
||||||
|
|
||||||
@@ -28,9 +30,9 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-type:
|
build-and-push:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 5
|
timeout-minutes: 30
|
||||||
outputs:
|
outputs:
|
||||||
is-release: ${{ steps.check.outputs.is-release }}
|
is-release: ${{ steps.check.outputs.is-release }}
|
||||||
steps:
|
steps:
|
||||||
@@ -40,7 +42,7 @@ jobs:
|
|||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-ci
|
run: make setup-release
|
||||||
- name: Check if this is a release commit
|
- name: Check if this is a release commit
|
||||||
id: check
|
id: check
|
||||||
env:
|
env:
|
||||||
@@ -48,25 +50,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
python3 -m devx.ci.detect_release_commit
|
python3 -m devx.ci.detect_release_commit
|
||||||
|
|
||||||
build-and-push:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: >-
|
|
||||||
needs.detect-type.outputs.is-release == 'false' && (
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
|
||||||
)
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-release
|
|
||||||
- name: Docker registry login
|
- name: Docker registry login
|
||||||
|
if: >-
|
||||||
|
steps.check.outputs.is-release == 'false' && (
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
||||||
|
)
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
@@ -78,6 +67,11 @@ jobs:
|
|||||||
if [ -z "$_TOKEN" ]; then echo "Gitea API token not set — skipping Docker login"; exit 1; fi
|
if [ -z "$_TOKEN" ]; then echo "Gitea API token not set — skipping Docker login"; exit 1; fi
|
||||||
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
||||||
- name: Build and push tier images
|
- name: Build and push tier images
|
||||||
|
if: >-
|
||||||
|
steps.check.outputs.is-release == 'false' && (
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
||||||
|
)
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
@@ -123,7 +117,7 @@ jobs:
|
|||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
needs: [build-and-push]
|
needs: [build-and-push]
|
||||||
if: always() && needs.build-and-push.result == 'success'
|
if: always() && needs.build-and-push.result == 'success' && needs.build-and-push.outputs.is-release == 'false'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+59
-78
@@ -5,20 +5,35 @@ on:
|
|||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||||
|
PYTHONPATH: src
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality:
|
# Single validation job that merges: quality, detect-changes,
|
||||||
|
# release-dry-run, pr-review, and pre-merge-check.
|
||||||
|
# Uses ci-full image (has git-cliff for release-dry-run).
|
||||||
|
# Saves ~4x checkout+setup overhead vs 5 separate jobs.
|
||||||
|
validate:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 15
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
outputs:
|
||||||
|
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-image
|
run: make setup-image
|
||||||
|
# --- quality steps ---
|
||||||
- name: Lint all
|
- name: Lint all
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
@@ -29,14 +44,11 @@ jobs:
|
|||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
make pytest-cov
|
make pytest-cov
|
||||||
- name: Check unit test speed
|
- name: Check unit test speed
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.tools.check_test_speed --max-seconds 6 --max-single-seconds 0.5
|
python3 -m devx.tools.check_test_speed --max-seconds 6 --max-single-seconds 0.5
|
||||||
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_DOC_COVERAGE_STRICT: "1"
|
DEVX_DOC_COVERAGE_STRICT: "1"
|
||||||
DEVX_VALE_LEVEL: warning
|
DEVX_VALE_LEVEL: warning
|
||||||
run: |
|
run: |
|
||||||
@@ -44,8 +56,6 @@ jobs:
|
|||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
make devx-docs-check
|
make devx-docs-check
|
||||||
- name: Translation completeness check
|
- name: Translation completeness check
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.check_translations
|
python3 -m devx.ci.check_translations
|
||||||
@@ -66,97 +76,69 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
||||||
fi
|
fi
|
||||||
|
# --- detect-changes step ---
|
||||||
detect-changes:
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
outputs:
|
|
||||||
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image
|
|
||||||
- name: Detect changed paths
|
- name: Detect changed paths
|
||||||
id: detect
|
id: detect
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.classify_changes \
|
python3 -m devx.ci.classify_changes \
|
||||||
--base "origin/master" \
|
--base "origin/master" \
|
||||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||||
--github-output
|
--github-output
|
||||||
|
# --- validate-pr + pr-review steps (PR only) ---
|
||||||
release-dry-run:
|
- name: Validate auto-merge preconditions
|
||||||
needs: [quality, detect-changes]
|
if: github.event_name == 'pull_request'
|
||||||
if: needs.detect-changes.outputs.user-facing-changed == 'true'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
run: make setup-image
|
DEVX_VIKUNJA_PROJECT_ID: "8"
|
||||||
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.check_auto_merge_ready \
|
||||||
|
--branch "$HEAD_REF" \
|
||||||
|
--pr-title "$PR_TITLE" \
|
||||||
|
--repo "$REPOSITORY" \
|
||||||
|
--pr-number "$PR_NUMBER"
|
||||||
|
- name: Run automated PR review
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
set -euo pipefail
|
||||||
|
python3 -m devx.ci.pr_review \
|
||||||
|
"${{ github.event.number }}" \
|
||||||
|
"${{ github.repository }}"
|
||||||
|
# --- release-dry-run step (conditional) ---
|
||||||
- name: Release dry-run validation
|
- name: Release dry-run validation
|
||||||
env:
|
if: steps.detect.outputs.user-facing-changed == 'true'
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.release --dry-run
|
python3 -m devx.ci.release --dry-run
|
||||||
|
- name: Notify on failure
|
||||||
pr-review:
|
if: failure()
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-image
|
|
||||||
- name: Run automated PR review
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.pr_review \
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
"${{ github.event.number }}" \
|
python3 -m devx.ci.notify_failure \
|
||||||
"${{ github.repository }}"
|
--repo "${{ github.repository }}" \
|
||||||
|
--run-id "${{ github.run_id }}" \
|
||||||
|
--workflow "ci/validate" \
|
||||||
|
--commit "${{ github.sha }}" \
|
||||||
|
--auto-login
|
||||||
|
|
||||||
auto-merge:
|
auto-merge:
|
||||||
# Auto-merge runs after all CI checks pass. It reads the task ID
|
# Auto-merge runs after validate passes. It reads the task ID
|
||||||
# from the branch name, validates the PR title, and squash-merges.
|
# from the branch name, validates the PR title, and squash-merges.
|
||||||
# Uses always() so it runs even when detect-changes skips (no user-facing changes).
|
needs: [validate]
|
||||||
needs: [quality, detect-changes, pr-review, release-dry-run]
|
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
github.event_name == 'pull_request' &&
|
github.event_name == 'pull_request' &&
|
||||||
needs.quality.result == 'success' &&
|
needs.validate.result == 'success'
|
||||||
needs.pr-review.result == 'success' &&
|
|
||||||
(needs.release-dry-run.result == 'success' || needs.release-dry-run.result == 'skipped')
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
@@ -175,9 +157,9 @@ jobs:
|
|||||||
- name: Post approval review
|
- name: Post approval review
|
||||||
env:
|
env:
|
||||||
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.pr_review \
|
python3 -m devx.ci.pr_review \
|
||||||
@@ -186,13 +168,12 @@ jobs:
|
|||||||
--event APPROVE \
|
--event APPROVE \
|
||||||
--checklist-confirmed \
|
--checklist-confirmed \
|
||||||
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
||||||
--body "Auto-approved: all CI checks passed (quality, pr-review, release-dry-run)."
|
--body "Auto-approved: all CI checks passed (validate job)."
|
||||||
- name: Squash merge with task ID
|
- name: Squash merge with task ID
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
DEVX_VIKUNJA_PROJECT_ID: "8"
|
DEVX_VIKUNJA_PROJECT_ID: "8"
|
||||||
PYTHONPATH: src
|
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
|||||||
+108
-261
@@ -1,39 +1,39 @@
|
|||||||
name: Post-merge
|
name: Post-merge
|
||||||
|
|
||||||
# Runs on every push to master. A single workflow with conditional jobs
|
# Runs on every push to master (after CI workflow merges a PR).
|
||||||
# for release, publish, wiki sync, badges, and Vikunja task updates.
|
# Consolidated into 2 jobs (from 7) to reduce runner overhead:
|
||||||
|
# detect-and-configure ──→ release-and-maintain
|
||||||
#
|
#
|
||||||
# Job dependency graph:
|
# Job 1: detect release commit, validate commit msg, configure repo
|
||||||
|
# (branch protection, labels).
|
||||||
|
# Job 2: release + publish + sync-wiki + vikunja + badges.
|
||||||
|
# Individual steps are conditional on job 1 outputs.
|
||||||
#
|
#
|
||||||
# detect-type ──┬── validate-commit-msg (skip if release commit)
|
# The badges step always runs (even on release commits) so version
|
||||||
# ├── release (skip if release commit)
|
# badge picks up the new __version__. It runs last so it sees the
|
||||||
# │ └── publish (needs release — builds & publishes to PyPI)
|
# new version if release created one.
|
||||||
# ├── badges (needs release — ALWAYS runs, waits for release
|
|
||||||
# │ so version badge picks up new __version__)
|
|
||||||
# ├── configure-repo (independent — skip if release commit)
|
|
||||||
# ├── sync-wiki (skip if release commit — runs for ALL merges)
|
|
||||||
# └── vikunja (skip if release commit — runs for ALL merges)
|
|
||||||
#
|
|
||||||
# sync-wiki and vikunja run for ALL non-release commits, not just when
|
|
||||||
# release succeeds. This ensures the wiki and task tracker are updated
|
|
||||||
# even for infrastructure-only changes (docs, CI config, etc.).
|
|
||||||
#
|
|
||||||
# The badges job uses `if: always()` and needs `release` so it waits for
|
|
||||||
# the release job to complete (whether it ran or was skipped). This ensures
|
|
||||||
# the version badge always reflects the latest __version__ on master.
|
|
||||||
# Badges run on every push to master, including release commits.
|
|
||||||
#
|
#
|
||||||
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
||||||
# job (which depends on release) builds and publishes the package to the
|
# step builds and publishes the package to the Gitea PyPI registry.
|
||||||
# Gitea PyPI registry. The release commit's post-merge run still updates
|
# The release commit's post-merge run still updates badges. Other
|
||||||
# badges (version badge picks up the new version). Other jobs skip.
|
# steps (sync-wiki, vikunja) skip on release commits.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: post-merge-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||||
|
PYTHONPATH: src
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-type:
|
detect-and-configure:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
@@ -42,184 +42,67 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
outputs:
|
outputs:
|
||||||
is-release: ${{ steps.check.outputs.is-release }}
|
is-release: ${{ steps.check.outputs.is-release }}
|
||||||
|
is-automated: ${{ steps.check.outputs.is-automated }}
|
||||||
|
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-image
|
run: make setup-image
|
||||||
|
- name: Ensure branch protection and labels
|
||||||
|
env:
|
||||||
|
DEVX_REPO_NAME: devx
|
||||||
|
DEVX_REPO_OWNER: oblachno-oss
|
||||||
|
DEVX_STATUS_CHECKS: "CI / validate (pull_request)"
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.tools.configure_repo
|
||||||
- name: Check if this is a release commit
|
- name: Check if this is a release commit
|
||||||
id: check
|
id: check
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.detect_release_commit
|
python3 -m devx.ci.detect_release_commit
|
||||||
|
|
||||||
validate-commit-msg:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image
|
|
||||||
- name: Validate latest commit message
|
- name: Validate latest commit message
|
||||||
env:
|
if: steps.check.outputs.is-automated == 'false'
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
git log -1 --format=%B > commit-msg.txt
|
git log -1 --format=%B > commit-msg.txt
|
||||||
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||||
rm -f commit-msg.txt
|
rm -f commit-msg.txt
|
||||||
|
- name: Detect changed paths
|
||||||
|
id: detect
|
||||||
|
if: steps.check.outputs.is-release == 'false'
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.classify_changes \
|
||||||
|
--base "HEAD~1" \
|
||||||
|
--head "HEAD" \
|
||||||
|
--github-output
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.notify_failure \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--run-id "${{ github.run_id }}" \
|
||||||
|
--workflow "post-merge/detect-and-configure" \
|
||||||
|
--commit "${{ github.sha }}" \
|
||||||
|
--auto-login
|
||||||
|
|
||||||
release:
|
release-and-maintain:
|
||||||
needs: [detect-type]
|
needs: [detect-and-configure]
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
if: always() && needs.detect-and-configure.result == 'success'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.release-tag.outputs.tag }}
|
tag: ${{ steps.release-tag.outputs.tag }}
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image
|
|
||||||
- name: Configure git
|
|
||||||
run: |
|
|
||||||
git config user.name "devx-ci-bot"
|
|
||||||
git config user.email "devx-ci-bot@oblachno.fyi"
|
|
||||||
- name: Run release
|
|
||||||
id: release-tag
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.release
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/release" \
|
|
||||||
--commit "${{ github.sha }}" \
|
|
||||||
--auto-login
|
|
||||||
|
|
||||||
publish:
|
|
||||||
needs: [release]
|
|
||||||
if: needs.release.outputs.tag != ''
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ needs.release.outputs.tag }}
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image EXTRAS=release
|
|
||||||
- name: Build and publish release
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.publish "${{ needs.release.outputs.tag }}" "${{ github.repository }}" --auto-login
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/publish" \
|
|
||||||
--commit "${{ github.sha }}" \
|
|
||||||
--auto-login
|
|
||||||
|
|
||||||
sync-wiki:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
concurrency:
|
|
||||||
group: sync-wiki-${{ github.repository }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image
|
|
||||||
- name: Sync documentation to wiki
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/sync-wiki" \
|
|
||||||
--commit "${{ github.sha }}" \
|
|
||||||
--auto-login
|
|
||||||
|
|
||||||
badges:
|
|
||||||
needs: [detect-type, release]
|
|
||||||
if: always()
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -229,108 +112,72 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: master
|
ref: master
|
||||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
- name: Fetch latest master
|
|
||||||
run: |
|
|
||||||
git fetch origin master
|
|
||||||
git reset --hard origin/master
|
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-image
|
run: make setup-image EXTRAS=release
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config user.name "devx-ci-bot"
|
||||||
|
git config user.email "devx-ci-bot@oblachno.fyi"
|
||||||
|
# --- release + publish (only if user-facing changes, not a release commit) ---
|
||||||
|
- name: Run release
|
||||||
|
id: release-tag
|
||||||
|
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.release
|
||||||
|
- name: Build and publish release
|
||||||
|
if: steps.release-tag.outputs.tag != ''
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
git fetch --tags
|
||||||
|
git checkout "${{ steps.release-tag.outputs.tag }}"
|
||||||
|
python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
|
||||||
|
# --- sync-wiki + vikunja (skip on automated/release commits) ---
|
||||||
|
- name: Sync documentation to wiki
|
||||||
|
if: needs.detect-and-configure.outputs.is-automated == 'false'
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
||||||
|
- name: Update Vikunja task
|
||||||
|
if: needs.detect-and-configure.outputs.is-automated == 'false'
|
||||||
|
env:
|
||||||
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
|
DEVX_VIKUNJA_PROJECT_ID: "8"
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||||
|
# --- badges (always run — even on release commits) ---
|
||||||
- name: Generate and push badges
|
- name: Generate and push badges
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
# Fetch latest master to pick up any release commit that was pushed
|
||||||
|
git fetch origin master
|
||||||
|
git reset --hard origin/master
|
||||||
python3 -m devx.ci.push_badges
|
python3 -m devx.ci.push_badges
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/badges" \
|
|
||||||
--commit "${{ github.sha }}" \
|
|
||||||
--auto-login
|
|
||||||
|
|
||||||
vikunja:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image
|
|
||||||
- name: Update Vikunja task
|
|
||||||
env:
|
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
|
||||||
DEVX_VIKUNJA_PROJECT_ID: "8"
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.notify_failure \
|
python3 -m devx.ci.notify_failure \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--run-id "${{ github.run_id }}" \
|
--run-id "${{ github.run_id }}" \
|
||||||
--workflow "post-merge/vikunja" \
|
--workflow "post-merge/release-and-maintain" \
|
||||||
--commit "${{ github.sha }}" \
|
|
||||||
--auto-login
|
|
||||||
|
|
||||||
configure-repo:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
run: make setup-image
|
|
||||||
- name: Ensure branch protection and labels
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_REPO_NAME: devx
|
|
||||||
DEVX_REPO_OWNER: oblachno-oss
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
python3 -m devx.tools.configure_repo
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/configure-repo" \
|
|
||||||
--commit "${{ github.sha }}" \
|
--commit "${{ github.sha }}" \
|
||||||
--auto-login
|
--auto-login
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
|
|||||||
|
|
||||||
Both run via `make workflow-check` and are part of `make lint-all`.
|
Both run via `make workflow-check` and are part of `make lint-all`.
|
||||||
The pre-commit hook runs actionlint automatically when workflow files change.
|
The pre-commit hook runs actionlint automatically when workflow files change.
|
||||||
The CI `quality` job runs `make setup-quality` then `make lint-all`.
|
The CI `validate` job runs `make setup-image` then `make lint-all`.
|
||||||
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
|
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -148,13 +148,13 @@ Every change to master goes through this workflow. No exceptions.
|
|||||||
### Branch Protection (Required Gitea Settings)
|
### Branch Protection (Required Gitea Settings)
|
||||||
|
|
||||||
Branch protection and labels are automatically configured by
|
Branch protection and labels are automatically configured by
|
||||||
`python -m devx.tools.configure_repo`, which runs as a `configure-repo` job in
|
`python -m devx.tools.configure_repo`, which runs as a step in the
|
||||||
the post-merge workflow on every push to master.
|
`detect-and-configure` job in the post-merge workflow on every push to master.
|
||||||
|
|
||||||
The following rules are enforced for `master`:
|
The following rules are enforced for `master`:
|
||||||
- **Require pull request**: No direct pushes to master
|
- **Require pull request**: No direct pushes to master
|
||||||
- **Require approval review**: At least 1 `APPROVE` review before merge
|
- **Require approval review**: At least 1 `APPROVE` review before merge
|
||||||
- **Require status checks**: CI quality must pass
|
- **Require status checks**: CI validate must pass
|
||||||
- **Block force pushes**: No history rewriting on master
|
- **Block force pushes**: No history rewriting on master
|
||||||
|
|
||||||
### 1. Create Vikunja Task
|
### 1. Create Vikunja Task
|
||||||
@@ -192,8 +192,9 @@ docs: update README
|
|||||||
|
|
||||||
### 6. Review the PR
|
### 6. Review the PR
|
||||||
|
|
||||||
**Automated review (CI `pr-review` job):** Every PR triggers an automated
|
**Automated review (CI `validate` job):** Every PR triggers an automated
|
||||||
review via `python -m devx.ci.pr_review`. This job posts a review with
|
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
|
||||||
|
This posts a review with
|
||||||
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found):
|
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found):
|
||||||
|
|
||||||
- Architecture compliance (no subprocess in CLI, no hardcoded URLs)
|
- Architecture compliance (no subprocess in CLI, no hardcoded URLs)
|
||||||
@@ -216,7 +217,7 @@ Once all checklist items are verified and comments are addressed, approve
|
|||||||
the PR. Then add the `ready-to-merge` label. The auto-merge workflow will:
|
the PR. Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||||
1. **Validate** PR title format (`DEVX-N: <vikunja task title>`) and match against Vikunja task title
|
1. **Validate** PR title format (`DEVX-N: <vikunja task title>`) and match against Vikunja task title
|
||||||
2. **Check** that at least one substantive APPROVE review exists
|
2. **Check** that at least one substantive APPROVE review exists
|
||||||
3. Wait for all CI checks to pass (including the `pr-review` job)
|
3. Wait for all CI checks to pass (including the `validate` job)
|
||||||
4. Squash-merge with title: `DEVX-N: <conventional commit message>`
|
4. Squash-merge with title: `DEVX-N: <conventional commit message>`
|
||||||
5. The post-merge workflow marks the Vikunja task as done
|
5. The post-merge workflow marks the Vikunja task as done
|
||||||
6. The release workflow automatically versions, tags, and publishes
|
6. The release workflow automatically versions, tags, and publishes
|
||||||
@@ -227,36 +228,27 @@ the PR. Then add the `ready-to-merge` label. The auto-merge workflow will:
|
|||||||
### Automated Release Pipeline
|
### Automated Release Pipeline
|
||||||
|
|
||||||
After a PR is merged to master, the **post-merge workflow**
|
After a PR is merged to master, the **post-merge workflow**
|
||||||
(`.gitea/workflows/post-merge.yml`) runs automatically:
|
(`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
|
||||||
|
into 2 jobs (from 7) to reduce runner overhead:
|
||||||
|
|
||||||
1. **detect-type** — Checks if the commit is a regular merge or a
|
1. **detect-and-configure** — Configures repo (branch protection, labels),
|
||||||
release commit (`release: vX.Y.Z`). All subsequent jobs skip for
|
detects release commit, validates commit message. Outputs `is-release`
|
||||||
release commits (except badges).
|
and `is-automated` for the next job.
|
||||||
|
|
||||||
2. **release** — Runs `python -m devx.ci.release` which:
|
2. **release-and-maintain** — Runs all post-merge maintenance as
|
||||||
- Checks for user-facing changes via `python -m devx.ci.classify_changes`
|
conditional steps:
|
||||||
- Uses **git-cliff** to calculate the next semver version from conventional commits
|
- **release** (if not a release commit) — Runs `python -m devx.ci.release`
|
||||||
- Updates `__version__` in `src/devx/__init__.py` (single source of truth)
|
which checks for user-facing changes via `classify_changes`, uses
|
||||||
- Updates `CHANGELOG.md` with the new version section
|
git-cliff for semver, updates `__version__`, updates `CHANGELOG.md`,
|
||||||
- Runs `make lint-ruff` and `make pytest-cov` to verify the release is healthy
|
runs lint+tests, commits with `release: vX.Y.Z [skip ci]`, creates
|
||||||
- Commits with `release: vX.Y.Z [skip ci]` prefix
|
annotated tag, pushes to master.
|
||||||
- Creates an annotated tag `vX.Y.Z` on the release commit
|
- **publish** (if release created a tag) — Builds and publishes the
|
||||||
- Pushes both the commit and tag to master
|
package to the Gitea PyPI registry. Checks out the release tag
|
||||||
|
within the same job.
|
||||||
3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL
|
- **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
|
||||||
non-release commits (not only when release succeeds), so docs-only
|
- **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
|
||||||
changes still update the wiki.
|
- **badges** (always) — Generates and pushes quality badge SVGs to the
|
||||||
|
`badges` branch. Fetches latest master first to pick up release commits.
|
||||||
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
|
|
||||||
Uses `if: always()` so it runs on every push, including release commits.
|
|
||||||
|
|
||||||
5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL
|
|
||||||
non-release commits (not only when release succeeds), so infrastructure-only
|
|
||||||
changes still update the task tracker.
|
|
||||||
|
|
||||||
6. **publish** — Runs after release succeeds (needs: release). Builds and
|
|
||||||
publishes the package to the Gitea PyPI registry. Gets the tag from the
|
|
||||||
release job's `tag` output (written via `GITHUB_OUTPUT`).
|
|
||||||
|
|
||||||
### Smart CI: User-Facing vs Workflow-Only Changes
|
### Smart CI: User-Facing vs Workflow-Only Changes
|
||||||
|
|
||||||
@@ -362,12 +354,11 @@ dependency is skipped, even if the condition explicitly allows
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
auto-merge:
|
auto-merge:
|
||||||
needs: [quality, detect-changes, pr-review, molecule-tests]
|
needs: [validate, molecule-tests]
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
github.event_name == 'pull_request' &&
|
github.event_name == 'pull_request' &&
|
||||||
needs.quality.result == 'success' &&
|
needs.validate.result == 'success' &&
|
||||||
needs.pr-review.result == 'success' &&
|
|
||||||
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
|
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -505,9 +496,9 @@ to eliminate the 40-120s setup tax on every CI job:
|
|||||||
|
|
||||||
| Image | Contains | Used by jobs |
|
| Image | Contains | Used by jobs |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| `ci-base-latest` | Python 3.12 + devx[ci] + tea | detect-changes, detect-type, validate-commit-msg, pr-review, auto-merge, sync-wiki, vikunja, configure-repo |
|
| `ci-base-latest` | Python 3.12 + devx[ci] + tea | auto-merge, detect-and-configure |
|
||||||
| `ci-quality-latest` | ci-base + devx[lint] + actionlint + checkmake + hadolint | quality, badges |
|
| `ci-quality-latest` | ci-base + devx[lint] + actionlint + checkmake + hadolint | (badges in release-and-maintain uses ci-full) |
|
||||||
| `ci-full-latest` | ci-quality + devx[release,molecule,deploy] + git-cliff + OpenTofu | release, publish, release-dry-run, molecule-tests, deploy jobs |
|
| `ci-full-latest` | ci-quality + devx[release,molecule,deploy] + git-cliff + OpenTofu | validate, release-and-maintain, molecule-tests, build-and-push |
|
||||||
|
|
||||||
**Build process** (in `build-images.yml` workflow):
|
**Build process** (in `build-images.yml` workflow):
|
||||||
1. `ci-base` builds FROM `gitea/runner-images:ubuntu-latest`
|
1. `ci-base` builds FROM `gitea/runner-images:ubuntu-latest`
|
||||||
@@ -520,9 +511,9 @@ Each image is tagged `latest` and pushed to
|
|||||||
**Using images in workflows**:
|
**Using images in workflows**:
|
||||||
```yaml
|
```yaml
|
||||||
jobs:
|
jobs:
|
||||||
quality:
|
validate:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
@@ -604,7 +595,7 @@ the user should not need to specify which profile to use.
|
|||||||
|
|
||||||
| Profile | Purpose |
|
| Profile | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `ci-investigator` | Investigate CI failures (quality, release, publish, wiki sync, image build) |
|
| `ci-investigator` | Investigate CI failures (validate, release-and-maintain, build-images) |
|
||||||
| `dep-upgrader` | Python dependency upgrades in pyproject.toml with dep-doc validation |
|
| `dep-upgrader` | Python dependency upgrades in pyproject.toml with dep-doc validation |
|
||||||
| `docker-image-builder` | Build/push/cleanup 3-tier runner images (ci-base, ci-quality, ci-full) |
|
| `docker-image-builder` | Build/push/cleanup 3-tier runner images (ci-base, ci-quality, ci-full) |
|
||||||
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync integrity |
|
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync integrity |
|
||||||
@@ -614,7 +605,7 @@ the user should not need to specify which profile to use.
|
|||||||
|
|
||||||
| Trigger | Profile | Mode |
|
| Trigger | Profile | Mode |
|
||||||
|---------|---------|------|
|
|---------|---------|------|
|
||||||
| CI run failure (quality, release, publish, sync-wiki, build-images) | `ci-investigator` | Background |
|
| CI run failure (validate, release-and-maintain, build-images) | `ci-investigator` | Background |
|
||||||
| PR ready for review | `pr-reviewer` | Foreground |
|
| PR ready for review | `pr-reviewer` | Foreground |
|
||||||
| Dependency upgrade requested | `dep-upgrader` | Background |
|
| Dependency upgrade requested | `dep-upgrader` | Background |
|
||||||
| Docker image build/push needed | `docker-image-builder` | Background |
|
| Docker image build/push needed | `docker-image-builder` | Background |
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.40.1] - 2026-07-12
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fall back to CI token when reviewer self-approval is rejected
|
||||||
|
|
||||||
## [0.40.0] - 2026-07-11
|
## [0.40.0] - 2026-07-11
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ quality badges.
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Why devx?
|
## Why devx?
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.40.0",
|
"devx>=0.40.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
@@ -101,8 +101,8 @@ pip install -e .
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** If your project requires a specific devx version, pin it in
|
> **Note:** If your project requires a specific devx version, pin it in
|
||||||
> `dependencies` (for example, `"devx==0.40.0"`) or use a version constraint
|
> `dependencies` (for example, `"devx==0.40.1"`) or use a version constraint
|
||||||
> (for example, `"devx>=0.40.0,<0.41"`).
|
> (for example, `"devx>=0.40.1,<0.41"`).
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.40.0",
|
"devx>=0.40.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
||||||
```
|
```
|
||||||
|
|
||||||
Pin a specific version if needed: `"devx==0.40.0"` or `"devx>=0.40.0,<0.41"`.
|
Pin a specific version if needed: `"devx==0.40.1"` or `"devx>=0.40.1,<0.41"`.
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
+48
-43
@@ -337,7 +337,7 @@ Configures repository branch protection and labels via the Gitea REST API.
|
|||||||
Sets up master branch protection (required status checks, block on rejected
|
Sets up master branch protection (required status checks, block on rejected
|
||||||
reviews, block on outdated branch) and creates standard labels. Status check
|
reviews, block on outdated branch) and creates standard labels. Status check
|
||||||
contexts are read from `DEVX_STATUS_CHECKS` or default to
|
contexts are read from `DEVX_STATUS_CHECKS` or default to
|
||||||
`CI / quality (pull_request)`.
|
`CI / validate (pull_request)`.
|
||||||
|
|
||||||
### `generate_badges.py`
|
### `generate_badges.py`
|
||||||
|
|
||||||
@@ -456,13 +456,14 @@ Developer pushes and creates PR (title: "DEVX-N: <vikunja task title>")
|
|||||||
▼
|
▼
|
||||||
CI workflow (ci.yml) triggers:
|
CI workflow (ci.yml) triggers:
|
||||||
│
|
│
|
||||||
├── quality (lint, tests, coverage, test speed, doc coverage,
|
├── validate (single job: quality + detect-changes +
|
||||||
│ translation check, dependency scan, workflow dry-run)
|
│ release-dry-run + pr-review + pre-merge validation)
|
||||||
│
|
│ ├── quality steps (lint, tests, coverage, test speed, doc coverage,
|
||||||
├── detect-changes (classify_changes.py → user-facing or workflow-only)
|
│ │ translation check, dependency scan, workflow dry-run)
|
||||||
│ └── if user-facing → release-dry-run (release.py --dry-run)
|
│ ├── detect-changes (classify_changes.py → user-facing or workflow-only)
|
||||||
│
|
│ │ └── if user-facing → release-dry-run (release.py --dry-run)
|
||||||
├── pr-review (pr_review.py → posts COMMENT or REQUEST_CHANGES)
|
│ ├── pre-merge validation (check_auto_merge_ready.py)
|
||||||
|
│ └── pr-review (pr_review.py → posts COMMENT or REQUEST_CHANGES)
|
||||||
│
|
│
|
||||||
└── auto-merge (auto_merge.py)
|
└── auto-merge (auto_merge.py)
|
||||||
├── validate PR title format
|
├── validate PR title format
|
||||||
@@ -483,50 +484,54 @@ Push to master (squash-merge commit: "DEVX-N <conventional commit>")
|
|||||||
▼
|
▼
|
||||||
Post-merge workflow (post-merge.yml) triggers:
|
Post-merge workflow (post-merge.yml) triggers:
|
||||||
│
|
│
|
||||||
├── detect-type (detect_release_commit.py)
|
├── detect-and-configure (single job)
|
||||||
│ └── is-release? → skip all jobs except badges
|
│ ├── configure-repo (configure_repo.py)
|
||||||
|
│ ├── detect-type (detect_release_commit.py)
|
||||||
|
│ │ └── is-release? → skip all steps except badges
|
||||||
|
│ └── validate-commit-msg (validate_commit_msg.py --branch master)
|
||||||
│
|
│
|
||||||
├── validate-commit-msg (validate_commit_msg.py --branch master)
|
└── release-and-maintain (needs detect-and-configure)
|
||||||
│
|
├── release (release.py) [skip if release commit or workflow-only]
|
||||||
├── release (release.py)
|
│ ├── classify_changes.py → skip if workflow-only
|
||||||
│ ├── classify_changes.py → skip if workflow-only
|
│ ├── git-cliff → calculate next version
|
||||||
│ ├── git-cliff → calculate next version
|
│ ├── update __version__ in __init__.py
|
||||||
│ ├── update __version__ in __init__.py
|
│ ├── update CHANGELOG.md
|
||||||
│ ├── update CHANGELOG.md
|
│ ├── run make lint-ruff && make pytest-cov
|
||||||
│ ├── run make lint-ruff && make pytest-cov
|
│ ├── commit "release: vX.Y.Z [skip ci]"
|
||||||
│ ├── commit "release: vX.Y.Z [skip ci]"
|
│ ├── create annotated tag vX.Y.Z
|
||||||
│ ├── create annotated tag vX.Y.Z
|
│ └── push commit + tag to master
|
||||||
│ └── push commit + tag to master
|
│ │
|
||||||
│ │
|
│ ▼
|
||||||
│ ▼
|
│ publish (publish.py) [if release created a tag]
|
||||||
│ Tag push triggers publish workflow (see below)
|
│ ├── build package (python -m build)
|
||||||
│
|
│ ├── publish to Gitea PyPI registry (twine upload)
|
||||||
├── sync-wiki (sync_wiki.py --strict)
|
│ │ OR publish to standard PyPI (if PYPI_TOKEN set)
|
||||||
│ └── sync docs/ to Gitea wiki with integrity check
|
│ │ OR skip publish (if --skip-build)
|
||||||
│
|
│ └── create Gitea release with git-cliff notes
|
||||||
├── badges (push_badges.py) [ALWAYS runs, even on release commits]
|
│
|
||||||
│ ├── fetch latest master
|
├── sync-wiki (sync_wiki.py --strict) [skip if automated]
|
||||||
│ ├── generate_badges.py → SVG files
|
│ └── sync docs/ to Gitea wiki with integrity check
|
||||||
│ ├── push to orphan badges branch
|
│
|
||||||
│ └── update README.md + docs/index.md with cache-busting URLs
|
├── vikunja (post_merge.py) [skip if automated]
|
||||||
│
|
│ ├── extract task ID from commit message
|
||||||
├── vikunja (post_merge.py)
|
│ ├── mark Vikunja task as done
|
||||||
│ ├── extract task ID from commit message
|
│ └── post comment with merge SHA
|
||||||
│ ├── mark Vikunja task as done
|
│
|
||||||
│ └── post comment with merge SHA
|
└── badges (push_badges.py) [ALWAYS runs, even on release commits]
|
||||||
│
|
├── fetch latest master
|
||||||
└── configure-repo (configure_repo.py)
|
├── generate_badges.py → SVG files
|
||||||
└── ensure branch protection and labels
|
├── push to orphan badges branch
|
||||||
|
└── update README.md + docs/index.md with cache-busting URLs
|
||||||
```
|
```
|
||||||
|
|
||||||
### Publish flow
|
### Publish flow
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Tag push (vX.Y.Z) triggers publish workflow (publish.yml):
|
Within release-and-maintain job (after release step creates a tag):
|
||||||
│
|
│
|
||||||
▼
|
|
||||||
├── install build, twine, git-cliff, tea
|
├── install build, twine, git-cliff, tea
|
||||||
├── configure tea login
|
├── configure tea login
|
||||||
|
├── checkout release tag
|
||||||
│
|
│
|
||||||
└── publish (publish.py)
|
└── publish (publish.py)
|
||||||
├── build package (python -m build)
|
├── build package (python -m build)
|
||||||
|
|||||||
+146
-108
@@ -1,32 +1,29 @@
|
|||||||
# CI/CD Workflow
|
# CI/CD Workflow
|
||||||
|
|
||||||
devx uses Gitea Actions for CI/CD automation. Three workflows implement a
|
devx uses Gitea Actions for CI/CD automation. Two workflows implement a
|
||||||
complete pipeline: pull request validation, post-merge release automation, and
|
complete pipeline: pull request validation and post-merge release
|
||||||
tag-triggered publishing.
|
automation (including publishing).
|
||||||
|
|
||||||
## Workflow overview
|
## Workflow overview
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PR opened/synchronized ──► CI (ci.yml)
|
PR opened/synchronized ──► CI (ci.yml)
|
||||||
│ ├── quality
|
│ ├── validate (quality + detect-changes +
|
||||||
│ ├── detect-changes
|
│ │ release-dry-run + pr-review +
|
||||||
│ ├── release-dry-run (if user-facing)
|
│ │ pre-merge validation)
|
||||||
│ ├── pr-review
|
|
||||||
│ └── auto-merge ──► squash-merge to master
|
│ └── auto-merge ──► squash-merge to master
|
||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
Push to master ──► Post-merge (post-merge.yml)
|
Push to master ──► Post-merge (post-merge.yml)
|
||||||
├── detect-type
|
├── detect-and-configure (detect-type +
|
||||||
├── validate-commit-msg
|
│ validate-commit-msg +
|
||||||
├── release ──► tag vX.Y.Z
|
│ configure-repo)
|
||||||
├── sync-wiki │
|
└── release-and-maintain
|
||||||
├── badges │
|
├── release ──► tag vX.Y.Z
|
||||||
├── vikunja │
|
├── publish ──► Gitea PyPI registry + Gitea release
|
||||||
└── configure-repo │
|
├── sync-wiki
|
||||||
│
|
├── vikunja
|
||||||
▼
|
└── badges (always runs)
|
||||||
Tag push (v*) ──► Publish (publish.yml)
|
|
||||||
└── publish ──► Gitea PyPI registry + Gitea release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI workflow (`ci.yml`)
|
## CI workflow (`ci.yml`)
|
||||||
@@ -35,9 +32,15 @@ Runs on pull requests (opened and synchronize) and manual dispatch.
|
|||||||
|
|
||||||
### Jobs
|
### Jobs
|
||||||
|
|
||||||
#### `quality`
|
#### `validate`
|
||||||
|
|
||||||
The main quality gate. Runs on every PR:
|
The single validation job. Consolidates the former `quality`,
|
||||||
|
`detect-changes`, `release-dry-run`, `pr-review`, and `pre-merge-check`
|
||||||
|
jobs into one job to save checkout+setup overhead. Runs on every PR.
|
||||||
|
|
||||||
|
**Quality steps**
|
||||||
|
|
||||||
|
The main quality gate:
|
||||||
|
|
||||||
1. **Lint all** — ruff check, ruff format check, pyright, bandit, actionlint
|
1. **Lint all** — ruff check, ruff format check, pyright, bandit, actionlint
|
||||||
(via `make lint-all`)
|
(via `make lint-all`)
|
||||||
@@ -52,21 +55,21 @@ The main quality gate. Runs on every PR:
|
|||||||
7. **Workflow dry-run validation** — `make workflow-dryrun` via act_runner
|
7. **Workflow dry-run validation** — `make workflow-dryrun` via act_runner
|
||||||
(best-effort, skipped if act_runner is not installed)
|
(best-effort, skipped if act_runner is not installed)
|
||||||
|
|
||||||
#### `detect-changes`
|
**`detect-changes` step**
|
||||||
|
|
||||||
Classifies changes between `origin/master` and the PR head as user-facing or
|
Classifies changes between `origin/master` and the PR head as user-facing or
|
||||||
workflow-only using `python -m devx.ci.classify_changes --github-output`.
|
workflow-only using `python -m devx.ci.classify_changes --github-output`.
|
||||||
Writes `user-facing-changed=true|false` to the job output for use by
|
Writes `user-facing-changed=true|false` to the job output for use by
|
||||||
downstream jobs.
|
downstream steps.
|
||||||
|
|
||||||
#### `release-dry-run`
|
**`release-dry-run` step**
|
||||||
|
|
||||||
Depends on `quality` and `detect-changes`. Only runs if user-facing changes
|
Only runs if the detect-changes step detected user-facing changes. Runs
|
||||||
are detected. Runs `python -m devx.ci.release --dry-run` to validate that
|
`python -m devx.ci.release --dry-run` to validate that the release script
|
||||||
the release script can calculate the next version and generate the changelog
|
can calculate the next version and generate the changelog without making
|
||||||
without making changes. Non-blocking (uses `|| true`).
|
changes. Non-blocking (uses `|| true`).
|
||||||
|
|
||||||
#### `pr-review`
|
**`pr-review` step**
|
||||||
|
|
||||||
Runs on every pull request. Executes `python -m devx.ci.pr_review` with the
|
Runs on every pull request. Executes `python -m devx.ci.pr_review` with the
|
||||||
PR number and repository. Fetches the PR diff via the Gitea API and runs
|
PR number and repository. Fetches the PR diff via the Gitea API and runs
|
||||||
@@ -87,11 +90,24 @@ Checks performed:
|
|||||||
7. Test coverage — source changes must include test updates
|
7. Test coverage — source changes must include test updates
|
||||||
8. Commit conventions — conventional commit format on PR commits
|
8. Commit conventions — conventional commit format on PR commits
|
||||||
|
|
||||||
|
**Pre-merge validation step**
|
||||||
|
|
||||||
|
Runs on every pull request. Executes
|
||||||
|
`python -m devx.ci.check_auto_merge_ready` with the branch name, PR title,
|
||||||
|
repository, and PR number. Validates auto-merge preconditions before the
|
||||||
|
`auto-merge` job runs:
|
||||||
|
|
||||||
|
1. **Branch name** — must contain a valid task ID (for example,
|
||||||
|
`DEVX-12-fix-foo` → `DEVX-12`)
|
||||||
|
2. **PR title format** — must be `{PREFIX}-N: <vikunja task title>`
|
||||||
|
3. **Vikunja task** — must exist and the title must match the PR title
|
||||||
|
4. **Branch state** — must not be behind master
|
||||||
|
|
||||||
#### `auto-merge`
|
#### `auto-merge`
|
||||||
|
|
||||||
Depends on `quality`, `detect-changes`, and `pr-review`. The final job in the
|
Depends on `validate`. The final job in the CI workflow. Runs
|
||||||
CI workflow. Runs `python -m devx.ci.auto_merge` with the branch name, PR
|
`python -m devx.ci.auto_merge` with the branch name, PR title, repository,
|
||||||
title, repository, and PR number:
|
and PR number:
|
||||||
|
|
||||||
1. **Read task ID** from branch name (for example, `DEVX-12-fix-foo` → `DEVX-12`)
|
1. **Read task ID** from branch name (for example, `DEVX-12-fix-foo` → `DEVX-12`)
|
||||||
2. **Validate PR title format** — must be `{PREFIX}-N: <vikunja task title>`
|
2. **Validate PR title format** — must be `{PREFIX}-N: <vikunja task title>`
|
||||||
@@ -107,8 +123,9 @@ The merge commit push to master triggers the post-merge workflow.
|
|||||||
|
|
||||||
### Smart CI: user-facing vs workflow-only changes
|
### Smart CI: user-facing vs workflow-only changes
|
||||||
|
|
||||||
Not all changes require a new release. The `detect-changes` job classifies
|
Not all changes require a new release. The `detect-changes` step in the
|
||||||
changes using `python -m devx.ci.classify_changes`:
|
`validate` job classifies changes using
|
||||||
|
`python -m devx.ci.classify_changes`:
|
||||||
|
|
||||||
**Workflow-only paths** (infrastructure — no release needed):
|
**Workflow-only paths** (infrastructure — no release needed):
|
||||||
- `.gitea/**` — Gitea Actions workflows
|
- `.gitea/**` — Gitea Actions workflows
|
||||||
@@ -137,55 +154,90 @@ Rule priority (first match wins):
|
|||||||
|
|
||||||
## Post-merge workflow (`post-merge.yml`)
|
## Post-merge workflow (`post-merge.yml`)
|
||||||
|
|
||||||
Runs on every push to master. A single workflow with conditional jobs
|
Runs on every push to master. Consolidated into 2 jobs (from 7) to reduce
|
||||||
replaces separate workflows for release, wiki sync, badges, and Vikunja task
|
runner overhead: `detect-and-configure` (detect-type + validate-commit-msg +
|
||||||
updates.
|
configure-repo) and `release-and-maintain` (release + publish + sync-wiki +
|
||||||
|
badges + vikunja). Individual steps within `release-and-maintain` are
|
||||||
|
conditional on the `detect-and-configure` job's outputs.
|
||||||
|
|
||||||
### Job dependency graph
|
### Job dependency graph
|
||||||
|
|
||||||
```text
|
```text
|
||||||
detect-type ──┬── validate-commit-msg (skip if release commit)
|
detect-and-configure
|
||||||
├── release (skip if release commit)
|
├── configure-repo (independent, skip if release commit)
|
||||||
│ │
|
├── detect-type → is-release? is-automated?
|
||||||
│ ├── sync-wiki (needs release)
|
└── validate-commit-msg (skip if release commit)
|
||||||
│ ├── badges (needs release, ALWAYS runs)
|
│
|
||||||
│ └── vikunja (needs release)
|
▼
|
||||||
└── configure-repo (independent, skip if release commit)
|
release-and-maintain (needs detect-and-configure)
|
||||||
|
├── release (skip if release commit or workflow-only)
|
||||||
|
│ └── publish (if release created a tag)
|
||||||
|
├── sync-wiki (skip if automated)
|
||||||
|
├── vikunja (skip if automated)
|
||||||
|
└── badges (always runs)
|
||||||
```
|
```
|
||||||
|
|
||||||
`sync-wiki` and `vikunja` depend on `release` succeeding so that the wiki and
|
`sync-wiki` and `vikunja` run only on non-automated commits (that is, real PR
|
||||||
task tracker are only updated when the code is actually released. If release
|
merges) so that the wiki and task tracker are only updated when a human
|
||||||
fails, they are skipped to avoid leaving the wiki or Vikunja in an
|
change lands. They skip on release commits and automated commits.
|
||||||
inconsistent state.
|
|
||||||
|
|
||||||
The `badges` job uses `if: always()` with no is-release condition so it runs
|
The `badges` step always runs (even on release commits) so badges (tests,
|
||||||
on every push to master, including release commits. This ensures badges
|
coverage, version, etc.) are always current. It runs last so it picks up
|
||||||
(tests, coverage, version, etc.) are always current.
|
any version bump the release step created.
|
||||||
|
|
||||||
When `release` creates a `release: vX.Y.Z` commit, the release commit's
|
When `release` creates a `release: vX.Y.Z` commit, the release commit's
|
||||||
post-merge run still updates badges (the version badge picks up the new
|
post-merge run still updates badges (the version badge picks up the new
|
||||||
version). Other jobs skip. The tag push triggers `publish.yml`.
|
version). Other steps skip. The `publish` step builds and publishes the
|
||||||
|
package to the Gitea PyPI registry within the same `release-and-maintain`
|
||||||
|
job (it checks out the release tag).
|
||||||
|
|
||||||
### Post-merge jobs
|
### Post-merge jobs
|
||||||
|
|
||||||
#### `detect-type`
|
#### `detect-and-configure`
|
||||||
|
|
||||||
|
The first post-merge job. Consolidates the former `detect-type`,
|
||||||
|
`validate-commit-msg`, and `configure-repo` jobs. Outputs `is-release`,
|
||||||
|
`is-automated`, and `user-facing-changed` for the `release-and-maintain`
|
||||||
|
job.
|
||||||
|
|
||||||
|
**`detect-type` step**
|
||||||
|
|
||||||
Checks if the latest commit is a release commit (`release: vX.Y.Z [skip ci]`)
|
Checks if the latest commit is a release commit (`release: vX.Y.Z [skip ci]`)
|
||||||
using `python -m devx.ci.detect_release_commit`. Writes `is-release=true` or
|
using `python -m devx.ci.detect_release_commit`. Writes `is-release=true` or
|
||||||
`is-release=false` to the job output. All subsequent jobs use this to
|
`is-release=false` (and `is-automated`) to the job output. The
|
||||||
conditionally skip for release commits.
|
`release-and-maintain` job uses these to conditionally skip steps for
|
||||||
|
release commits.
|
||||||
|
|
||||||
#### `validate-commit-msg`
|
**`validate-commit-msg` step**
|
||||||
|
|
||||||
Depends on `detect-type`. Skips for release commits. Validates the latest
|
Skips for release/automated commits. Validates the latest commit message
|
||||||
commit message using `python -m devx.ci.validate_commit_msg --branch master`.
|
using `python -m devx.ci.validate_commit_msg --branch master`. On master,
|
||||||
On master, commits must follow `{PREFIX}-N: <conventional commit>` format
|
commits must follow `{PREFIX}-N: <conventional commit>` format (added by
|
||||||
(added by auto-merge).
|
auto-merge).
|
||||||
|
|
||||||
#### `release`
|
**`configure-repo` step**
|
||||||
|
|
||||||
Depends on `detect-type`. Skips for release commits. The core release
|
Ensures branch protection and labels are configured using
|
||||||
automation job. Runs `python -m devx.ci.release`:
|
`python -m devx.tools.configure_repo --repo <name> --owner <owner>`:
|
||||||
|
|
||||||
|
- Sets up master branch protection (required status checks, block on rejected
|
||||||
|
reviews, block on outdated branch)
|
||||||
|
- Creates standard labels
|
||||||
|
- Status check contexts read from `DEVX_STATUS_CHECKS` or default to
|
||||||
|
`CI / validate (pull_request)`
|
||||||
|
|
||||||
|
On failure, the `notify_failure` step creates a Gitea issue.
|
||||||
|
|
||||||
|
#### `release-and-maintain`
|
||||||
|
|
||||||
|
Depends on `detect-and-configure`. The second post-merge job. Consolidates
|
||||||
|
the former `release`, `publish`, `sync-wiki`, `badges`, and `vikunja` jobs.
|
||||||
|
Individual steps are conditional on the `detect-and-configure` job's outputs.
|
||||||
|
|
||||||
|
**`release` step**
|
||||||
|
|
||||||
|
Skips for release commits and workflow-only changes. The core release
|
||||||
|
automation step. Runs `python -m devx.ci.release`:
|
||||||
|
|
||||||
1. **Classify changes** — calls `classify_changes.py` to check for user-facing
|
1. **Classify changes** — calls `classify_changes.py` to check for user-facing
|
||||||
changes. If only infrastructure files changed, exits without releasing.
|
changes. If only infrastructure files changed, exits without releasing.
|
||||||
@@ -225,11 +277,10 @@ tag/version/commit alignment.
|
|||||||
On failure, the `notify_failure` step creates a Gitea issue via
|
On failure, the `notify_failure` step creates a Gitea issue via
|
||||||
`python -m devx.ci.notify_failure`.
|
`python -m devx.ci.notify_failure`.
|
||||||
|
|
||||||
#### `sync-wiki`
|
**`sync-wiki` step**
|
||||||
|
|
||||||
Depends on `detect-type` and `release`. Skips for release commits. Syncs
|
Skips for automated commits. Syncs documentation from `docs/` to the Gitea
|
||||||
documentation from `docs/` to the Gitea wiki using
|
wiki using `python -m devx.ci.sync_wiki --repo <owner/repo> --strict`:
|
||||||
`python -m devx.ci.sync_wiki --repo <owner/repo> --strict`:
|
|
||||||
|
|
||||||
1. Reads `docs/mapping.json` to map file paths to wiki page titles
|
1. Reads `docs/mapping.json` to map file paths to wiki page titles
|
||||||
2. Lists existing wiki pages via the Gitea API
|
2. Lists existing wiki pages via the Gitea API
|
||||||
@@ -243,15 +294,14 @@ deleted).
|
|||||||
|
|
||||||
On failure, the `notify_failure` step creates a Gitea issue.
|
On failure, the `notify_failure` step creates a Gitea issue.
|
||||||
|
|
||||||
#### `badges`
|
**`badges` step**
|
||||||
|
|
||||||
Depends on `detect-type` and `release`. Uses `if: always()` so it runs on
|
Always runs (even on release commits). Generates and pushes quality badges
|
||||||
every push to master, including release commits. Generates and pushes quality
|
using `python -m devx.ci.push_badges`:
|
||||||
badges using `python -m devx.ci.push_badges`:
|
|
||||||
|
|
||||||
1. **Fetch latest master** — `git fetch origin master && git reset --hard
|
1. **Fetch latest master** — `git fetch origin master && git reset --hard
|
||||||
origin/master` (ensures the version badge reflects the current state,
|
origin/master` (ensures the version badge reflects the current state,
|
||||||
even if the release job recently pushed a new version)
|
even if the release step recently pushed a new version)
|
||||||
2. **Generate badges** — calls `devx.tools.generate_badges` which runs
|
2. **Generate badges** — calls `devx.tools.generate_badges` which runs
|
||||||
pytest-cov, doc-coverage, lint checks, and version extraction, then writes
|
pytest-cov, doc-coverage, lint checks, and version extraction, then writes
|
||||||
SVG files: `coverage.svg`, `tests.svg`, `docs.svg`, `quality.svg`,
|
SVG files: `coverage.svg`, `tests.svg`, `docs.svg`, `quality.svg`,
|
||||||
@@ -268,11 +318,10 @@ and waits 10s between attempts).
|
|||||||
|
|
||||||
On failure, the `notify_failure` step creates a Gitea issue.
|
On failure, the `notify_failure` step creates a Gitea issue.
|
||||||
|
|
||||||
#### `vikunja`
|
**`vikunja` step**
|
||||||
|
|
||||||
Depends on `detect-type` and `release`. Skips for release commits. Updates
|
Skips for automated commits. Updates the Vikunja task after a merge using
|
||||||
the Vikunja task after a merge using `python -m devx.ci.post_merge --git-sha
|
`python -m devx.ci.post_merge --git-sha <sha>`:
|
||||||
<sha>`:
|
|
||||||
|
|
||||||
1. Extracts the task ID from the first line of the commit message
|
1. Extracts the task ID from the first line of the commit message
|
||||||
2. Marks the corresponding Vikunja task as done
|
2. Marks the corresponding Vikunja task as done
|
||||||
@@ -280,26 +329,11 @@ the Vikunja task after a merge using `python -m devx.ci.post_merge --git-sha
|
|||||||
|
|
||||||
On failure, the `notify_failure` step creates a Gitea issue.
|
On failure, the `notify_failure` step creates a Gitea issue.
|
||||||
|
|
||||||
#### `configure-repo`
|
**`publish` step**
|
||||||
|
|
||||||
Depends on `detect-type`. Skips for release commits. Ensures branch
|
Only runs if the `release` step created a tag. Builds and publishes the
|
||||||
protection and labels are configured using
|
package within the same `release-and-maintain` job (checks out the release
|
||||||
`python -m devx.tools.configure_repo --repo <name> --owner <owner>`:
|
tag). Runs `python -m devx.ci.publish <tag> <owner/repo>`:
|
||||||
|
|
||||||
- Sets up master branch protection (required status checks, block on rejected
|
|
||||||
reviews, block on outdated branch)
|
|
||||||
- Creates standard labels
|
|
||||||
- Status check contexts read from `DEVX_STATUS_CHECKS` or default to
|
|
||||||
`CI / quality (pull_request)`
|
|
||||||
|
|
||||||
On failure, the `notify_failure` step creates a Gitea issue.
|
|
||||||
|
|
||||||
## Publish workflow (`publish.yml`)
|
|
||||||
|
|
||||||
Runs on tag pushes matching `v*`. Triggered by the `release` job in the
|
|
||||||
post-merge workflow when it creates and pushes a new version tag.
|
|
||||||
|
|
||||||
### Job: `publish`
|
|
||||||
|
|
||||||
1. **Install dependencies** — build, twine, requests, python-dotenv, click,
|
1. **Install dependencies** — build, twine, requests, python-dotenv, click,
|
||||||
and the project itself
|
and the project itself
|
||||||
@@ -518,25 +552,29 @@ The complete release process from PR to published package:
|
|||||||
1. **PR merged** — `auto-merge` squash-merges the PR to master with
|
1. **PR merged** — `auto-merge` squash-merges the PR to master with
|
||||||
`{PREFIX}-N <conventional commit>` title
|
`{PREFIX}-N <conventional commit>` title
|
||||||
2. **Post-merge triggers** — the merge push triggers `post-merge.yml`
|
2. **Post-merge triggers** — the merge push triggers `post-merge.yml`
|
||||||
3. **detect-type** — confirms the commit is not a release commit
|
3. **detect-and-configure** — detects release commit, validates commit
|
||||||
4. **release** — `release.py` calculates the next version, updates files,
|
message, and ensures branch protection/labels
|
||||||
runs tests, commits `release: vX.Y.Z [skip ci]`, creates tag `vX.Y.Z`,
|
4. **release** (step in `release-and-maintain`) — `release.py` calculates
|
||||||
and pushes to master
|
the next version, updates files, runs tests, commits
|
||||||
5. **Tag push triggers publish** — the tag push triggers `publish.yml`
|
`release: vX.Y.Z [skip ci]`, creates tag `vX.Y.Z`, and pushes to master
|
||||||
6. **publish** — `publish.py` builds the package, publishes to the Gitea PyPI
|
5. **publish** (step in `release-and-maintain`) — `publish.py` builds the
|
||||||
registry, and creates a Gitea release with git-cliff notes
|
package, publishes to the Gitea PyPI registry, and creates a Gitea
|
||||||
7. **sync-wiki** — documentation is synced to the Gitea wiki
|
release with git-cliff notes (checks out the release tag within the
|
||||||
8. **badges** — quality badges are regenerated and pushed to the `badges`
|
same job)
|
||||||
branch; README and docs/index.md are updated with cache-busting URLs
|
6. **sync-wiki** (step in `release-and-maintain`) — documentation is synced
|
||||||
9. **vikunja** — the corresponding Vikunja task is marked as done
|
to the Gitea wiki
|
||||||
10. **configure-repo** — branch protection and labels are ensured
|
7. **vikunja** (step in `release-and-maintain`) — the corresponding Vikunja
|
||||||
|
task is marked as done
|
||||||
|
8. **badges** (step in `release-and-maintain`) — quality badges are
|
||||||
|
regenerated and pushed to the `badges` branch; README and docs/index.md
|
||||||
|
are updated with cache-busting URLs
|
||||||
|
|
||||||
The release commit's post-merge run skips all jobs except `badges` (which
|
The release commit's post-merge run skips all steps except `badges` (which
|
||||||
picks up the new version number). This prevents infinite loops.
|
picks up the new version number). This prevents infinite loops.
|
||||||
|
|
||||||
## Failure handling
|
## Failure handling
|
||||||
|
|
||||||
Every job in the post-merge and publish workflows has a `notify_failure` step
|
Every job in the CI and post-merge workflows has a `notify_failure` step
|
||||||
that runs `if: failure()`. This creates a Gitea issue with the workflow name,
|
that runs `if: failure()`. This creates a Gitea issue with the workflow name,
|
||||||
run ID, and commit SHA, ensuring failures that would otherwise go unnoticed
|
run ID, and commit SHA, ensuring failures that would otherwise go unnoticed
|
||||||
in the Actions tab are surfaced as issues. The issue is created via the tea
|
in the Actions tab are surfaced as issues. The issue is created via the tea
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.40.0",
|
"devx>=0.40.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"devx>=0.40.0",
|
"devx>=0.40.1",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||||
|
|
||||||
__version__ = "0.40.0"
|
__version__ = "0.40.1"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ Usage:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -548,8 +549,14 @@ def _post_manual_review(
|
|||||||
checklist_confirmed: bool,
|
checklist_confirmed: bool,
|
||||||
checklist_categories: str | None,
|
checklist_categories: str | None,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
owner: str | None = None,
|
||||||
|
repo_name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Post a manual review with validation for APPROVE events."""
|
"""Post a manual review with validation for APPROVE events.
|
||||||
|
|
||||||
|
When self-approval is rejected (reviewer token belongs to PR author),
|
||||||
|
falls back to the CI token (different user) if available.
|
||||||
|
"""
|
||||||
if not body or len(body) < 50:
|
if not body or len(body) < 50:
|
||||||
raise click.ClickException(_("Review body must be at least 50 characters."))
|
raise click.ClickException(_("Review body must be at least 50 characters."))
|
||||||
|
|
||||||
@@ -585,8 +592,20 @@ def _post_manual_review(
|
|||||||
review = client.create_review(pr_number, event=event, body=body)
|
review = client.create_review(pr_number, event=event, body=body)
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
if "approve" in e.message.lower() or "422" in str(e.status):
|
if "approve" in e.message.lower() or "422" in str(e.status):
|
||||||
click.echo(_("Note: Self-approval not allowed. Posting COMMENT instead."))
|
# Self-approval not allowed (reviewer token belongs to PR author).
|
||||||
review = client.create_review(pr_number, event="COMMENT", body=body)
|
# Fall back to CI token (different user) if available.
|
||||||
|
ci_token = os.environ.get("CI_GITEA_API_TOKEN", "").strip()
|
||||||
|
if ci_token and owner and repo_name:
|
||||||
|
click.echo(_("Note: Self-approval not allowed with reviewer token. Retrying with CI token."))
|
||||||
|
ci_client = GiteaClient(GITEA_API_URL, ci_token, owner, repo_name)
|
||||||
|
try:
|
||||||
|
review = ci_client.create_review(pr_number, event=event, body=body)
|
||||||
|
except APIError:
|
||||||
|
click.echo(_("Note: CI token also cannot approve. Posting COMMENT instead."))
|
||||||
|
review = client.create_review(pr_number, event="COMMENT", body=body)
|
||||||
|
else:
|
||||||
|
click.echo(_("Note: Self-approval not allowed. Posting COMMENT instead."))
|
||||||
|
review = client.create_review(pr_number, event="COMMENT", body=body)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
review_id = review.get("id", "?")
|
review_id = review.get("id", "?")
|
||||||
@@ -645,7 +664,17 @@ def main(
|
|||||||
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
|
||||||
|
|
||||||
if event is not None:
|
if event is not None:
|
||||||
_post_manual_review(client, pr_number, event.upper(), body, checklist_confirmed, checklist_categories, dry_run)
|
_post_manual_review(
|
||||||
|
client,
|
||||||
|
pr_number,
|
||||||
|
event.upper(),
|
||||||
|
body,
|
||||||
|
checklist_confirmed,
|
||||||
|
checklist_categories,
|
||||||
|
dry_run,
|
||||||
|
owner=owner,
|
||||||
|
repo_name=repo_name,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
result = run_review(client, pr_number)
|
result = run_review(client, pr_number)
|
||||||
|
|||||||
@@ -2104,12 +2104,28 @@
|
|||||||
"zh": "No workflow runs found for SHA {sha}."
|
"zh": "No workflow runs found for SHA {sha}."
|
||||||
},
|
},
|
||||||
"Note: Self-approval not allowed. Posting COMMENT instead.": {
|
"Note: Self-approval not allowed. Posting COMMENT instead.": {
|
||||||
"bg": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"bg": "Забележка: Само-одобрението не е разрешено. Публикуване на COMMENT вместо това.",
|
||||||
"de": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"de": "Hinweis: Selbstgenehmigung nicht erlaubt. COMMENT wird stattdessen gesendet.",
|
||||||
"en": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"en": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
||||||
"pl": "Uwaga: Samo-zatwierdzenie niedozwolone. Publikowanie COMMENT zamiast tego.",
|
"pl": "Uwaga: Samo-zatwierdzenie niedozwolone. Publikowanie COMMENT zamiast tego.",
|
||||||
"ru": "Note: Self-approval not allowed. Posting COMMENT instead.",
|
"ru": "Примечание: Самоодобрение не разрешено. Публикация COMMENT вместо этого.",
|
||||||
"zh": "Note: Self-approval not allowed. Posting COMMENT instead."
|
"zh": "注意:不允许自我批准。改为发布 COMMENT。"
|
||||||
|
},
|
||||||
|
"Note: Self-approval not allowed with reviewer token. Retrying with CI token.": {
|
||||||
|
"bg": "Забележка: Само-одобрението не е разрешено с тоукън на рецензента. Повторен опит с CI тоукън.",
|
||||||
|
"de": "Hinweis: Selbstgenehmigung mit Reviewer-Token nicht erlaubt. Wiederholung mit CI-Token.",
|
||||||
|
"en": "Note: Self-approval not allowed with reviewer token. Retrying with CI token.",
|
||||||
|
"pl": "Uwaga: Samo-zatwierdzenie niedozwolone tokenem recenzenta. Ponawianie tokenem CI.",
|
||||||
|
"ru": "Примечание: Самоодобрение токеном ревьюера не разрешено. Повторная попытка с CI токеном.",
|
||||||
|
"zh": "注意:不允许使用审阅者令牌进行自我批准。正在使用 CI 令牌重试。"
|
||||||
|
},
|
||||||
|
"Note: CI token also cannot approve. Posting COMMENT instead.": {
|
||||||
|
"bg": "Забележка: CI тоукънът също не може да одобри. Публикуване на COMMENT вместо това.",
|
||||||
|
"de": "Hinweis: CI-Token kann ebenfalls nicht genehmigen. COMMENT wird stattdessen gesendet.",
|
||||||
|
"en": "Note: CI token also cannot approve. Posting COMMENT instead.",
|
||||||
|
"pl": "Uwaga: Token CI również nie może zatwierdzić. Publikowanie COMMENT zamiast tego.",
|
||||||
|
"ru": "Примечание: CI токен также не может одобрить. Публикация COMMENT вместо этого.",
|
||||||
|
"zh": "注意:CI 令牌也无法批准。改为发布 COMMENT。"
|
||||||
},
|
},
|
||||||
"Nothing to push.": {
|
"Nothing to push.": {
|
||||||
"bg": "Nothing to push.",
|
"bg": "Nothing to push.",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from devx.ci.pr_review import (
|
from devx.ci.pr_review import (
|
||||||
@@ -902,7 +903,12 @@ class TestManualReview:
|
|||||||
mock_client_class.return_value.create_review.assert_not_called()
|
mock_client_class.return_value.create_review.assert_not_called()
|
||||||
|
|
||||||
@patch("devx.ci.pr_review.GiteaClient")
|
@patch("devx.ci.pr_review.GiteaClient")
|
||||||
def test_manual_review_self_approval_fallback(self, mock_client_class: MagicMock) -> None:
|
def test_manual_review_self_approval_fallback_to_comment(
|
||||||
|
self, mock_client_class: MagicMock, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Self-approval with no CI token available → fall back to COMMENT."""
|
||||||
|
monkeypatch.delenv("CI_GITEA_API_TOKEN", raising=False)
|
||||||
|
monkeypatch.delenv("CI_GITEA_TOKEN", raising=False)
|
||||||
client = mock_client_class.return_value
|
client = mock_client_class.return_value
|
||||||
client.create_review.side_effect = [
|
client.create_review.side_effect = [
|
||||||
APIError(422, "approve your own pull is not allowed"),
|
APIError(422, "approve your own pull is not allowed"),
|
||||||
@@ -922,10 +928,77 @@ class TestManualReview:
|
|||||||
"--checklist-categories",
|
"--checklist-categories",
|
||||||
"1,2,3,4,5,6,7,8",
|
"1,2,3,4,5,6,7,8",
|
||||||
],
|
],
|
||||||
env={"CI_GITEA_TOKEN": "fake"},
|
env={"REVIEWER_GITEA_API_TOKEN": "fake-reviewer"},
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Review #202" in result.output
|
assert "Review #202" in result.output
|
||||||
|
# Without CI_GITEA_API_TOKEN, the fallback is COMMENT
|
||||||
|
assert "Self-approval not allowed. Posting COMMENT instead." in result.output
|
||||||
|
assert client.create_review.call_count == 2
|
||||||
|
assert client.create_review.call_args_list[1].kwargs.get("event") == "COMMENT"
|
||||||
|
|
||||||
|
@patch("devx.ci.pr_review.GiteaClient")
|
||||||
|
def test_manual_review_self_approval_falls_back_to_ci_token(self, mock_client_class: MagicMock) -> None:
|
||||||
|
"""Self-approval with CI token available → retry APPROVE with CI token (different user)."""
|
||||||
|
client = mock_client_class.return_value
|
||||||
|
client.create_review.side_effect = [
|
||||||
|
APIError(422, "approve your own pull is not allowed"),
|
||||||
|
{"id": 303},
|
||||||
|
]
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
main,
|
||||||
|
[
|
||||||
|
"42",
|
||||||
|
"oblachno-oss/devx",
|
||||||
|
"--event",
|
||||||
|
"APPROVE",
|
||||||
|
"--body",
|
||||||
|
"x" * 60,
|
||||||
|
"--checklist-confirmed",
|
||||||
|
"--checklist-categories",
|
||||||
|
"1,2,3,4,5,6,7,8",
|
||||||
|
],
|
||||||
|
env={"REVIEWER_GITEA_API_TOKEN": "fake-reviewer", "CI_GITEA_API_TOKEN": "fake-ci"},
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Review #303" in result.output
|
||||||
|
assert "Retrying with CI token" in result.output
|
||||||
|
# Second call should still be APPROVE (CI token retry)
|
||||||
|
assert client.create_review.call_count == 2
|
||||||
|
assert client.create_review.call_args_list[1].kwargs.get("event") == "APPROVE"
|
||||||
|
|
||||||
|
@patch("devx.ci.pr_review.GiteaClient")
|
||||||
|
def test_manual_review_ci_token_also_fails_falls_back_to_comment(self, mock_client_class: MagicMock) -> None:
|
||||||
|
"""Self-approval + CI token retry also fails → fall back to COMMENT."""
|
||||||
|
client = mock_client_class.return_value
|
||||||
|
client.create_review.side_effect = [
|
||||||
|
APIError(422, "approve your own pull is not allowed"),
|
||||||
|
APIError(422, "approve your own pull is not allowed"),
|
||||||
|
{"id": 404},
|
||||||
|
]
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
main,
|
||||||
|
[
|
||||||
|
"42",
|
||||||
|
"oblachno-oss/devx",
|
||||||
|
"--event",
|
||||||
|
"APPROVE",
|
||||||
|
"--body",
|
||||||
|
"x" * 60,
|
||||||
|
"--checklist-confirmed",
|
||||||
|
"--checklist-categories",
|
||||||
|
"1,2,3,4,5,6,7,8",
|
||||||
|
],
|
||||||
|
env={"REVIEWER_GITEA_API_TOKEN": "fake-reviewer", "CI_GITEA_API_TOKEN": "fake-ci"},
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Review #404" in result.output
|
||||||
|
assert "CI token also cannot approve" in result.output
|
||||||
|
# Third call should be COMMENT (final fallback)
|
||||||
|
assert client.create_review.call_count == 3
|
||||||
|
assert client.create_review.call_args_list[2].kwargs.get("event") == "COMMENT"
|
||||||
|
|
||||||
@patch("devx.ci.pr_review.GiteaClient")
|
@patch("devx.ci.pr_review.GiteaClient")
|
||||||
def test_manual_review_other_error_re_raises(self, mock_client_class: MagicMock) -> None:
|
def test_manual_review_other_error_re_raises(self, mock_client_class: MagicMock) -> None:
|
||||||
|
|||||||
@@ -34,19 +34,25 @@ class TestRun:
|
|||||||
|
|
||||||
class TestInstallPythonDeps:
|
class TestInstallPythonDeps:
|
||||||
@patch("devx.tools.setup.subprocess.run")
|
@patch("devx.tools.setup.subprocess.run")
|
||||||
|
@patch.dict(os.environ, {}, clear=False)
|
||||||
def test_install_dev(self, mock_run: MagicMock) -> None:
|
def test_install_dev(self, mock_run: MagicMock) -> None:
|
||||||
|
os.environ.pop("PIP_BREAK_SYSTEM_PACKAGES", None)
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
_install_python_deps(".venv/bin", "dev")
|
_install_python_deps(".venv/bin", "dev")
|
||||||
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"], check=False)
|
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[dev]"], check=False)
|
||||||
|
|
||||||
@patch("devx.tools.setup.subprocess.run")
|
@patch("devx.tools.setup.subprocess.run")
|
||||||
|
@patch.dict(os.environ, {}, clear=False)
|
||||||
def test_install_ci(self, mock_run: MagicMock) -> None:
|
def test_install_ci(self, mock_run: MagicMock) -> None:
|
||||||
|
os.environ.pop("PIP_BREAK_SYSTEM_PACKAGES", None)
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
_install_python_deps(".venv/bin", "ci")
|
_install_python_deps(".venv/bin", "ci")
|
||||||
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"], check=False)
|
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci]"], check=False)
|
||||||
|
|
||||||
@patch("devx.tools.setup.subprocess.run")
|
@patch("devx.tools.setup.subprocess.run")
|
||||||
|
@patch.dict(os.environ, {}, clear=False)
|
||||||
def test_install_custom_extras(self, mock_run: MagicMock) -> None:
|
def test_install_custom_extras(self, mock_run: MagicMock) -> None:
|
||||||
|
os.environ.pop("PIP_BREAK_SYSTEM_PACKAGES", None)
|
||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
_install_python_deps(".venv/bin", "ci,lint")
|
_install_python_deps(".venv/bin", "ci,lint")
|
||||||
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"], check=False)
|
mock_run.assert_called_once_with([".venv/bin/pip", "install", "-e", ".[ci,lint]"], check=False)
|
||||||
@@ -71,7 +77,9 @@ class TestInstallPythonDeps:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@patch("devx.tools.setup.subprocess.run")
|
@patch("devx.tools.setup.subprocess.run")
|
||||||
|
@patch.dict(os.environ, {}, clear=False)
|
||||||
def test_install_failure_without_break_system(self, mock_run: MagicMock) -> None:
|
def test_install_failure_without_break_system(self, mock_run: MagicMock) -> None:
|
||||||
|
os.environ.pop("PIP_BREAK_SYSTEM_PACKAGES", None)
|
||||||
mock_run.return_value = MagicMock(returncode=1)
|
mock_run.return_value = MagicMock(returncode=1)
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
with pytest.raises(subprocess.CalledProcessError):
|
||||||
_install_python_deps(".venv/bin", "ci")
|
_install_python_deps(".venv/bin", "ci")
|
||||||
|
|||||||
Reference in New Issue
Block a user