chore(ci): centralize tool version management into a single source of truth
This commit is contained in:
+58
-19
@@ -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" ] || \
|
||||
|
||||
@@ -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
|
||||
@@ -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}'"
|
||||
)
|
||||
|
||||
Executable
+27
@@ -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/^/ /'
|
||||
Reference in New Issue
Block a user