chore(ci): centralize tool version management into a single source of truth

This commit is contained in:
2026-05-03 00:50:57 +00:00
committed by Forgejo
parent ac74edd175
commit eba947bd0f
5 changed files with 239 additions and 19 deletions
+58 -19
View File
@@ -6,12 +6,6 @@ on:
pull_request:
branches: [master, develop*]
env:
UV_VERSION: "0.8.0"
PYTHON_VERSION: "3.13"
NOX_DEFAULT_VENV_BACKEND: "uv"
HELM_VERSION: "v3.16.4"
# Logging policy (server-load reduction):
# The Forgejo server stores every line a step streams to its console as the
# live job log, and that aggregate volume across all controller-driven CI
@@ -27,7 +21,40 @@ env:
# always streamed — the pipeline greps for them.
jobs:
load-versions:
runs-on: docker
container:
image: python:3.13-slim
outputs:
uv-version: ${{ steps.versions.outputs.uv-version }}
python-version: ${{ steps.versions.outputs.python-version }}
helm-version: ${{ steps.versions.outputs.helm-version }}
kubeconform-version: ${{ steps.versions.outputs.kubeconform-version }}
steps:
- name: Install Node.js (required by actions/checkout)
run: |
apt-get update && apt-get install -y -qq nodejs && rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v4
- name: Load tool versions from .tool-versions
id: versions
run: |
# Source the tool versions file and export as GitHub Actions outputs
while IFS='=' read -r key value; do
# Skip empty lines and comments
[[ -z "$key" || "$key" =~ ^# ]] && continue
# Trim whitespace
key=$(echo "$key" | xargs)
value=$(echo "$value" | xargs)
# Convert to lowercase with hyphens for output names
output_key=$(echo "$key" | tr '[:upper:]_' '[:lower:]-')
echo "${output_key}=${value}" >> $GITHUB_OUTPUT
echo "Loaded: ${output_key}=${value}"
done < .tool-versions
lint:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -40,7 +67,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -74,6 +101,7 @@ jobs:
retention-days: 30
typecheck:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -86,7 +114,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -119,6 +147,7 @@ jobs:
retention-days: 30
security:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -131,7 +160,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -165,6 +194,7 @@ jobs:
retention-days: 30
quality:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -177,7 +207,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -211,6 +241,7 @@ jobs:
timeout-minutes: 30
unit_tests:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -231,6 +262,7 @@ jobs:
- name: Install Helm CLI
if: steps.helm-cache.outputs.cache-hit != 'true'
run: |
HELM_VERSION="${{ needs.load-versions.outputs.helm-version }}"
ARCH="amd64"
HELM_TARBALL="helm-${HELM_VERSION}-linux-${ARCH}.tar.gz"
curl -fsSL "https://get.helm.sh/${HELM_TARBALL}" -o "/tmp/${HELM_TARBALL}"
@@ -246,7 +278,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -285,6 +317,7 @@ jobs:
timeout-minutes: 45
integration_tests:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -305,6 +338,7 @@ jobs:
- name: Install Helm CLI
if: steps.helm-cache.outputs.cache-hit != 'true'
run: |
HELM_VERSION="${{ needs.load-versions.outputs.helm-version }}"
ARCH="amd64"
HELM_TARBALL="helm-${HELM_VERSION}-linux-${ARCH}.tar.gz"
curl -fsSL "https://get.helm.sh/${HELM_TARBALL}" -o "/tmp/${HELM_TARBALL}"
@@ -320,7 +354,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -364,10 +398,10 @@ jobs:
retention-days: 30
coverage:
needs: [load-versions, lint, typecheck, security, quality, unit_tests]
runs-on: docker
container:
image: python:3.13-slim
needs: [lint, typecheck, security, quality, unit_tests]
# Bound the job so a hung run fails cleanly with diagnostics instead of
# being externally reaped to "no data". The parallel engine runs in
# ~4min; this leaves generous headroom for install + chunk tail.
@@ -401,7 +435,7 @@ jobs:
- name: Install uv and nox
if: steps.gate.outputs.run == 'true'
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
if: steps.gate.outputs.run == 'true'
@@ -460,6 +494,7 @@ jobs:
retention-days: 30
build:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -472,7 +507,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
pip install -q uv==${{ needs.load-versions.outputs.uv-version }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -508,7 +543,7 @@ jobs:
# continue-on-error: true allows the status-check gate to pass even
# when the docker:dind runner is unavailable (infrastructure issue).
# The docker job still runs; only infrastructure failures are tolerated.
needs: [lint, typecheck, security, quality, unit_tests]
needs: [load-versions, lint, typecheck, security, quality, unit_tests]
continue-on-error: true
runs-on: docker
container:
@@ -580,6 +615,7 @@ jobs:
retention-days: 30
helm:
needs: load-versions
runs-on: docker
container:
image: python:3.13-slim
@@ -602,6 +638,7 @@ jobs:
- name: Install Helm CLI
if: steps.helm-cache.outputs.cache-hit != 'true'
run: |
HELM_VERSION="${{ needs.load-versions.outputs.helm-version }}"
ARCH="amd64"
HELM_TARBALL="helm-${HELM_VERSION}-linux-${ARCH}.tar.gz"
curl -fsSL "https://get.helm.sh/${HELM_TARBALL}" -o "/tmp/${HELM_TARBALL}"
@@ -615,7 +652,7 @@ jobs:
- name: Install kubeconform
run: |
KUBECONFORM_VERSION="v0.7.0"
KUBECONFORM_VERSION="${{ needs.load-versions.outputs.kubeconform-version }}"
ARCH="amd64"
KUBECONFORM_TARBALL="kubeconform-linux-${ARCH}.tar.gz"
curl -fsSL "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/${KUBECONFORM_TARBALL}" \
@@ -792,13 +829,14 @@ jobs:
retention-days: 30
status-check:
if: always()
needs: [lint, typecheck, security, quality, unit_tests, integration_tests, coverage, build, docker, helm, push-validation]
needs: [load-versions, lint, typecheck, security, quality, unit_tests, integration_tests, coverage, build, docker, helm, push-validation]
runs-on: docker
container:
image: python:3.13-slim
steps:
- name: Check required job results
run: |
echo "load-versions: ${{ needs.load-versions.result }}"
echo "lint: ${{ needs.lint.result }}"
echo "typecheck: ${{ needs.typecheck.result }}"
echo "security: ${{ needs.security.result }}"
@@ -811,7 +849,8 @@ jobs:
echo "helm: ${{ needs.helm.result }}"
echo "push-validation: ${{ needs.push-validation.result }}"
if [ "${{ needs.lint.result }}" != "success" ] || \
if [ "${{ needs.load-versions.result }}" != "success" ] || \
[ "${{ needs.lint.result }}" != "success" ] || \
[ "${{ needs.typecheck.result }}" != "success" ] || \
[ "${{ needs.security.result }}" != "success" ] || \
[ "${{ needs.quality.result }}" != "success" ] || \
+8
View File
@@ -0,0 +1,8 @@
# Tool versions for CI pipeline
# This file is the single source of truth for tool versions used in the CI pipeline.
# Format: TOOL_NAME=version
UV_VERSION=0.8.0
PYTHON_VERSION=3.13
HELM_VERSION=v3.16.4
KUBECONFORM_VERSION=v0.7.0
+70
View File
@@ -186,3 +186,73 @@ Feature: CI workflow validation
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "status-check" should depend on "helm"
# --- Tool version centralization ---
Scenario: Tool versions file exists
Given the tool versions file at ".tool-versions"
Then the tool versions file should exist
Scenario: Tool versions file contains UV_VERSION
Given the tool versions file at ".tool-versions"
When I parse the tool versions file
Then the tool versions should contain "UV_VERSION"
Scenario: Tool versions file contains PYTHON_VERSION
Given the tool versions file at ".tool-versions"
When I parse the tool versions file
Then the tool versions should contain "PYTHON_VERSION"
Scenario: Tool versions file contains HELM_VERSION
Given the tool versions file at ".tool-versions"
When I parse the tool versions file
Then the tool versions should contain "HELM_VERSION"
Scenario: Tool versions file contains KUBECONFORM_VERSION
Given the tool versions file at ".tool-versions"
When I parse the tool versions file
Then the tool versions should contain "KUBECONFORM_VERSION"
Scenario: CI workflow reads UV_VERSION from tool versions file
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the workflow should have a job named "load-versions"
And the job "load-versions" should run "load tool versions from .tool-versions"
Scenario: CI workflow load-versions job outputs uv-version
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "load-versions" should have output "uv-version"
Scenario: CI workflow load-versions job outputs python-version
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "load-versions" should have output "python-version"
Scenario: CI workflow load-versions job outputs helm-version
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "load-versions" should have output "helm-version"
Scenario: CI workflow load-versions job outputs kubeconform-version
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "load-versions" should have output "kubeconform-version"
Scenario: CI workflow jobs depend on load-versions
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "lint" should depend on "load-versions"
And the job "typecheck" should depend on "load-versions"
And the job "unit_tests" should depend on "load-versions"
And the job "integration_tests" should depend on "load-versions"
And the job "e2e_tests" should depend on "load-versions"
And the job "helm" should depend on "load-versions"
And the job "build" should depend on "load-versions"
Scenario: CI workflow uses load-versions outputs for uv installation
Given the CI workflow file at ".forgejo/workflows/ci.yml"
When I parse the CI workflow YAML
Then the job "lint" should reference "needs.load-versions.outputs.uv-version"
And the job "typecheck" should reference "needs.load-versions.outputs.uv-version"
And the job "unit_tests" should reference "needs.load-versions.outputs.uv-version"
@@ -189,3 +189,79 @@ def step_then_at_least_one_job_uses_cache(context):
raise AssertionError(
"No job in the CI workflow uses actions/cache for dependency caching"
)
@given('the tool versions file at "{path}"')
def step_given_tool_versions_file(context, path):
"""Store the tool versions file path."""
context.tool_versions_path = Path(path)
@then("the tool versions file should exist")
def step_then_tool_versions_exists(context):
"""Verify the tool versions file exists."""
if not context.tool_versions_path.exists():
raise AssertionError(
f"Tool versions file not found at {context.tool_versions_path}"
)
@when("I parse the tool versions file")
def step_when_parse_tool_versions(context):
"""Parse the tool versions file."""
tool_versions_path = context.tool_versions_path
if not tool_versions_path.exists():
raise FileNotFoundError(f"Tool versions file not found at {tool_versions_path}")
context.tool_versions = {}
with tool_versions_path.open("r") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith("#"):
continue
if "=" in line:
key, value = line.split("=", 1)
context.tool_versions[key.strip()] = value.strip()
@then('the tool versions should contain "{key}"')
def step_then_tool_versions_contain(context, key):
"""Verify the tool versions file contains a specific key."""
if key not in context.tool_versions:
raise AssertionError(
f"Tool version '{key}' not found in tool versions file. "
f"Available keys: {list(context.tool_versions.keys())}"
)
@then('the job "{job_name}" should have output "{output_name}"')
def step_then_job_has_output(context, job_name, output_name):
"""Verify a job has a specific output."""
jobs = context.ci_workflow.get("jobs", {})
job = jobs.get(job_name)
if job is None:
raise AssertionError(f"Job '{job_name}' not found in workflow")
outputs = job.get("outputs", {})
if output_name not in outputs:
raise AssertionError(
f"Output '{output_name}' not found in job '{job_name}'. "
f"Available outputs: {list(outputs.keys())}"
)
@then('the job "{job_name}" should reference "{reference}"')
def step_then_job_references(context, job_name, reference):
"""Verify a job references a specific value (e.g., needs.load-versions.outputs.uv-version)."""
jobs = context.ci_workflow.get("jobs", {})
job = jobs.get(job_name)
if job is None:
raise AssertionError(f"Job '{job_name}' not found in workflow")
# Convert the job to a string and search for the reference
job_str = str(job)
if reference not in job_str:
raise AssertionError(
f"Reference '{reference}' not found in job '{job_name}'"
)
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Load tool versions from .tool-versions file
# This script sources the .tool-versions file and exports the variables
set -e
TOOL_VERSIONS_FILE=".tool-versions"
if [ ! -f "$TOOL_VERSIONS_FILE" ]; then
echo "ERROR: $TOOL_VERSIONS_FILE not found"
exit 1
fi
# Source the tool versions file
# This exports all TOOL_NAME=version pairs as environment variables
while IFS='=' read -r key value; do
# Skip empty lines and comments
[[ -z "$key" || "$key" =~ ^# ]] && continue
# Trim whitespace
key=$(echo "$key" | xargs)
value=$(echo "$value" | xargs)
export "$key=$value"
done < "$TOOL_VERSIONS_FILE"
# Output the loaded versions for verification
echo "Loaded tool versions from $TOOL_VERSIONS_FILE:"
grep -v '^#' "$TOOL_VERSIONS_FILE" | grep -v '^$' | sed 's/^/ /'