Skip to content

feat(ci): add 08-maintenance_code-maturity workflow for analyze_code_maturity.py #1

feat(ci): add 08-maintenance_code-maturity workflow for analyze_code_maturity.py

feat(ci): add 08-maintenance_code-maturity workflow for analyze_code_maturity.py #1

name: Code Maturity Maintenance
# Runs .github/scripts/analyze_code_maturity.py in one of two modes:
#
# check-only (default)
# Executes the script with --no-headers so no source files are modified.
# Generates docs/code_maturity_report.md to /tmp and compares it against
# the committed version to surface drift. Badge and version-tracking
# updates are written to the workspace but never committed.
# Triggered by: pull_request on the script / workflow file, schedule, and
# workflow_dispatch with update_headers=false (the default).
#
# rewrite (manual only, opt-in)
# Executes the script without --no-headers, writing auto-generated headers
# into every supported source file and updating docs/code_maturity_report.md,
# .github/version_tracking.json, and .github/badges/*.json.
# All changed artefacts are uploaded as workflow artefacts so maintainers
# can review and commit them manually.
# Triggered by: workflow_dispatch with update_headers=true.
#
# Governance note:
# This workflow is classified as Maintenance (lane 08). It is NOT a required
# PR gate. It must never be widened to trigger on broad src/**, include/**,
# or tests/** path patterns. The script itself is responsible for scanning
# those directories; this workflow only monitors changes to the script or its
# direct outputs.
#
# Vorlage aus .github/no_workflows/:
# 05-quality_build_disabled-stub-policy-ci.yml (Python-Guard-Muster)
# Strukturell nächstverwandter deaktivierter Workflow; nicht reaktiviert,
# sondern als eigenständiger neuer Workflow nach dem 08-Maintenance-Schema
# der aktiven Workflows erstellt.
on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- develop
- main
paths:
- '.github/scripts/analyze_code_maturity.py'
- '.github/workflows/08-maintenance_code-maturity.yml'
- '.github/version_tracking.json'
- '.github/badges/**'
schedule:
- cron: '0 3 * * 1' # Weekly on Monday at 03:00 UTC (check-only)
workflow_dispatch:
inputs:
update_headers:
description: >
Write auto-generated headers into source files (rewrite mode).
When false (default), only the maturity report is generated and
no source files are modified.
type: boolean
default: false
fail_on_drift:
description: >
Fail when docs/code_maturity_report.md differs from the freshly
generated report (check-only mode only; ignored when update_headers
is true).
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
code-maturity:
name: Code Maturity Analysis
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# ── 1. Checkout ────────────────────────────────────────────────────────
# Full history (fetch-depth: 0) is required in rewrite mode so that
# get_file_commit_history() can retrieve per-file git log entries.
# In check-only mode the script skips history extraction, but fetching
# full history here is harmless and avoids conditional checkout logic.
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── 2. Python ──────────────────────────────────────────────────────────
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
# ── 3. Determine run mode ──────────────────────────────────────────────
- name: Determine run mode
id: mode
run: |
UPDATE_HEADERS="${{ inputs.update_headers }}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${UPDATE_HEADERS}" = "true" ]; then
echo "mode=rewrite" >> "$GITHUB_OUTPUT"
echo "no_headers_flag=" >> "$GITHUB_OUTPUT"
echo "report_path_flag=" >> "$GITHUB_OUTPUT"
else
echo "mode=check-only" >> "$GITHUB_OUTPUT"
echo "no_headers_flag=--no-headers" >> "$GITHUB_OUTPUT"
echo "report_path_flag=--report-path /tmp/code_maturity_report.md" >> "$GITHUB_OUTPUT"
fi
# ── 4. Run analysis ────────────────────────────────────────────────────
- name: Run code maturity analysis (${{ steps.mode.outputs.mode }})
id: analyze
run: |
python .github/scripts/analyze_code_maturity.py \
--root . \
${{ steps.mode.outputs.no_headers_flag }} \
${{ steps.mode.outputs.report_path_flag }} \
2>&1 | tee /tmp/maturity_run.log
# Surface analyzed file count for the step summary
FILE_COUNT=$(grep -oP 'Analyzed \K[0-9]+' /tmp/maturity_run.log | tail -1 || echo "n/a")
echo "file_count=${FILE_COUNT}" >> "$GITHUB_OUTPUT"
# ── 5. Drift detection (check-only mode) ──────────────────────────────
- name: Detect report drift (check-only)
if: steps.mode.outputs.mode == 'check-only'
run: |
REPORT_COMMITTED="docs/code_maturity_report.md"
REPORT_GENERATED="/tmp/code_maturity_report.md"
FAIL_ON_DRIFT="${{ inputs.fail_on_drift || 'false' }}"
DRIFT=0
if [ ! -f "${REPORT_COMMITTED}" ]; then
echo "::notice::docs/code_maturity_report.md does not exist in the repository yet."
echo " Run the workflow with update_headers=false (default) via workflow_dispatch"
echo " to generate the initial report, then commit it."
elif ! diff -q "${REPORT_COMMITTED}" "${REPORT_GENERATED}" > /dev/null 2>&1; then
echo "::warning::docs/code_maturity_report.md is out of date."
echo " Diff:"
diff "${REPORT_COMMITTED}" "${REPORT_GENERATED}" || true
DRIFT=1
else
echo "✅ docs/code_maturity_report.md is up to date."
fi
if [ "${FAIL_ON_DRIFT}" = "true" ] && [ "${DRIFT}" -ne 0 ]; then
echo "::error::Drift detected and fail_on_drift=true — aborting."
exit 1
fi
# ── 6. Upload report artefact ──────────────────────────────────────────
- name: Upload maturity report
if: always()
uses: actions/upload-artifact@v4
with:
name: code-maturity-report-${{ github.run_number }}
path: |
/tmp/code_maturity_report.md
docs/code_maturity_report.md
retention-days: 30
if-no-files-found: ignore
# ── 7. Upload tracking + badge artefacts (rewrite mode only) ──────────
- name: Upload version tracking and badges (rewrite)
if: steps.mode.outputs.mode == 'rewrite'
uses: actions/upload-artifact@v4
with:
name: code-maturity-tracking-${{ github.run_number }}
path: |
.github/version_tracking.json
.github/badges/*.json
retention-days: 30
if-no-files-found: ignore
# ── 8. Step summary ───────────────────────────────────────────────────
- name: Write job summary
if: always()
run: |
{
echo "## Code Maturity Maintenance"
echo ""
echo "| Parameter | Value |"
echo "|-----------|-------|"
echo "| **Script** | \`.github/scripts/analyze_code_maturity.py\` |"
echo "| **Mode** | \`${{ steps.mode.outputs.mode }}\` |"
echo "| **Event** | \`${{ github.event_name }}\` |"
echo "| **Branch** | \`${{ github.ref_name }}\` |"
echo "| **Analyzed files** | ${{ steps.analyze.outputs.file_count }} |"
if [ "${{ steps.mode.outputs.mode }}" = "check-only" ]; then
echo "| **Header rewrites** | disabled (\`--no-headers\`) |"
echo "| **Fail on drift** | \`${{ inputs.fail_on_drift || 'false' }}\` |"
else
echo "| **Header rewrites** | enabled |"
fi
echo ""
REPORT_FILE=""
if [ "${{ steps.mode.outputs.mode }}" = "check-only" ] && [ -f /tmp/code_maturity_report.md ]; then
REPORT_FILE=/tmp/code_maturity_report.md
elif [ -f docs/code_maturity_report.md ]; then
REPORT_FILE=docs/code_maturity_report.md
fi
if [ -n "${REPORT_FILE}" ]; then
echo "**Report excerpt:**"
echo ""
echo '```'
head -35 "${REPORT_FILE}"
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"