28 Commits

Author SHA1 Message Date
0cc1e2fb62 fix: install pnpm before setup-node 2026-03-23 15:52:20 +01:00
a347242a24 Fix pnpm cache setup for Node CI 2026-03-23 13:52:05 +01:00
estebanthi
583ae62c3b validate images 2026-01-18 11:25:34 +01:00
estebanthi
c5563d9dc1 Split python UV workflow with DB service 2026-01-05 14:43:07 +01:00
estebanthi
4ebffa8c94 Normalize dockerfile paths for build and bake 2026-01-05 13:41:20 +01:00
estebanthi
4f00812999 Allow ssh entitlement for bake 2026-01-05 13:02:11 +01:00
estebanthi
91ab1aaac0 Fix ssh usage for bake builds 2026-01-05 12:56:52 +01:00
estebanthi
65e2336237 Cleanup leftover postgres containers 2026-01-05 12:07:21 +01:00
estebanthi
4170b17ea6 Use unique db container and cleanup 2026-01-05 11:56:40 +01:00
estebanthi
133731d493 Improve db readiness and build args parsing 2026-01-05 11:47:40 +01:00
estebanthi
bfd6822394 Add optional database for tests 2026-01-05 11:34:51 +01:00
estebanthi
012da683f6 pre-commit workflow 2026-01-05 09:12:57 +01:00
estebanthi
50e0f562a3 Fix bake jq conditionals 2026-01-04 18:47:44 +01:00
estebanthi
cbb6163cc2 Optimize docker build workflow 2026-01-04 18:42:08 +01:00
estebanthi
982f09e161 Add env input to workflows 2026-01-04 17:46:06 +01:00
estebanthi
757fc65c40 Support env input as KEY=VALUE lines 2026-01-04 17:10:00 +01:00
estebanthi
6bf910dff1 Make docker build target optional 2026-01-04 17:03:17 +01:00
estebanthi
c001f91b5e Add SSH support to reusable workflows 2026-01-04 16:50:30 +01:00
estebanthi
74bd39fcd3 Add python uv ci workflow 2026-01-04 16:07:16 +01:00
estebanthi
f481260d7e Expand node ci workflow steps 2026-01-04 15:44:00 +01:00
estebanthi
bb980c9263 node ci 2026-01-04 13:16:54 +01:00
estebanthi
7bfe90d49b Add latest tag on release builds 2026-01-04 13:05:07 +01:00
estebanthi
c3aa42a612 Fix docker build workflow tagging and scanning 2026-01-04 13:01:56 +01:00
estebanthi
81a34e348c updated wf 2026-01-04 12:54:04 +01:00
estebanthi
1440e4796c tags 2026-01-04 12:50:37 +01:00
estebanthi
a8c9119010 wf 2026-01-04 12:48:40 +01:00
estebanthi
1053d1271c wf 2026-01-04 12:41:57 +01:00
estebanthi
0c21f9b27b updated gh 2026-01-04 12:32:25 +01:00
6 changed files with 796 additions and 114 deletions

View File

@@ -1,114 +0,0 @@
name: Docker Build, Scan & Publish
on:
workflow_call:
inputs:
images:
description: >
JSON array of images to build.
Each item: { name, context, dockerfile, target, cache_ref }
required: true
type: string
registry_host:
required: true
type: string
default_branch:
required: true
type: string
build_args:
required: false
type: string
default: ""
trivy_severity:
required: false
type: string
default: "CRITICAL"
secrets:
registry_user:
required: true
registry_password:
required: true
ci_token:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Determine branch context
id: branch
run: |
if [ "${{ github.ref_name }}" = "${{ inputs.default_branch }}" ]; then
echo "is_default=true" >> "$GITHUB_OUTPUT"
else
echo "is_default=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ inputs.registry_host }}
username: ${{ secrets.registry_user }}
password: ${{ secrets.registry_password }}
- name: Build, scan and push images
env:
IMAGES: ${{ inputs.images }}
BUILD_ARGS: ${{ inputs.build_args }}
CI_TOKEN: ${{ secrets.ci_token }}
TRIVY_SEVERITY: ${{ inputs.trivy_severity }}
run: |
set -euo pipefail
echo "$IMAGES" | jq -c '.[]' | while read -r img; do
NAME=$(echo "$img" | jq -r '.name')
CONTEXT=$(echo "$img" | jq -r '.context')
DOCKERFILE=$(echo "$img" | jq -r '.dockerfile')
TARGET=$(echo "$img" | jq -r '.target')
CACHE_REF=$(echo "$img" | jq -r '.cache_ref')
echo "==== Building $NAME ===="
TAGS=()
TAGS+=("$NAME:${{ github.ref_name }}")
TAGS+=("$NAME:${{ github.sha }}")
if [ "${{ steps.branch.outputs.is_default }}" = "true" ]; then
TAGS+=("$NAME:latest")
fi
TAG_ARGS=$(printf -- "--tag %s " "${TAGS[@]}")
docker buildx build \
--file "$DOCKERFILE" \
--target "$TARGET" \
--cache-from "type=registry,ref=$CACHE_REF" \
--cache-to "type=registry,ref=$CACHE_REF,mode=max" \
--load \
$TAG_ARGS \
$(printf -- "--build-arg %s " $BUILD_ARGS) \
"$CONTEXT"
echo "==== Trivy scan for $NAME ===="
trivy image \
--severity "$TRIVY_SEVERITY" \
--exit-code 1 \
"${TAGS[0]}"
echo "==== Pushing $NAME ===="
for tag in "${TAGS[@]}"; do
docker push "$tag"
done
done

View File

@@ -0,0 +1,314 @@
name: Docker Build, Scan & Publish
on:
workflow_call:
inputs:
images:
description: >
JSON array of images to build.
Each item: { name, context, dockerfile, target, cache_ref }
required: true
type: string
registry_host:
required: true
type: string
build_args:
required: false
type: string
default: ""
description: >
Multiline build args, one per line: KEY=VALUE (values may include spaces)
env:
description: >
Multiline env vars, one per line: KEY=VALUE
required: false
type: string
default: ""
trivy_severity:
required: false
type: string
default: "CRITICAL"
secrets:
registry_user:
required: true
registry_password:
required: true
ci_token:
required: true
ssh_private_key:
required: false
ssh_known_hosts:
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load env vars
if: ${{ inputs.env != '' }}
run: |
while IFS= read -r line; do
[ -z "$line" ] && continue
case "$line" in \#*) continue;; esac
if [[ "$line" != *=* ]]; then
echo "Invalid env line: $line" >&2
exit 1
fi
echo "$line" >> "$GITHUB_ENV"
done <<< "${{ inputs.env }}"
- name: Start ssh-agent
if: ${{ secrets.ssh_private_key != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ssh_private_key }}
- name: Add SSH known hosts
if: ${{ secrets.ssh_known_hosts != '' }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.ssh_known_hosts }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
trivy --version
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ inputs.registry_host }}
username: ${{ secrets.registry_user }}
password: ${{ secrets.registry_password }}
- name: Build, scan and push images
env:
IMAGES: ${{ inputs.images }}
BUILD_ARGS: ${{ inputs.build_args }}
CI_TOKEN: ${{ secrets.ci_token }}
TRIVY_SEVERITY: ${{ inputs.trivy_severity }}
run: |
set -euo pipefail
if ! echo "$IMAGES" | jq -e . >/dev/null; then
echo "inputs.images must be valid JSON" >&2
exit 1
fi
if ! echo "$IMAGES" | jq -e 'type == "array"' >/dev/null; then
echo "inputs.images must be a JSON array" >&2
exit 1
fi
SSH_FLAGS=()
if [ -n "${SSH_AUTH_SOCK:-}" ]; then
SSH_FLAGS+=(--ssh default)
fi
SSH_BAKE_JSON="null"
if [ -n "${SSH_AUTH_SOCK:-}" ]; then
SSH_BAKE_JSON='["default"]'
fi
BAKE_ALLOW_FLAGS=()
if [ -n "${SSH_AUTH_SOCK:-}" ]; then
BAKE_ALLOW_FLAGS+=(--allow=ssh)
fi
RAW_REF="${{ github.ref }}"
SHA_FULL="${{ github.sha }}"
SHA_SHORT="${SHA_FULL:0:12}"
VERSION_TAGS=()
if [[ "$RAW_REF" =~ ^refs/tags/v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
VERSION_TAGS+=("v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}")
VERSION_TAGS+=("v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}")
VERSION_TAGS+=("v${BASH_REMATCH[1]}")
VERSION_TAGS+=("latest")
fi
BUILD_ARG_FLAGS=()
BUILD_ARGS_JSON="{}"
if [ -n "$BUILD_ARGS" ]; then
while IFS= read -r line; do
trimmed="${line#"${line%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
[ -z "$trimmed" ] && continue
case "$trimmed" in \#*) continue;; esac
if [[ "$trimmed" != *=* ]]; then
echo "Invalid build arg: $trimmed" >&2
exit 1
fi
BUILD_ARG_FLAGS+=(--build-arg "$trimmed")
key="${trimmed%%=*}"
val="${trimmed#*=}"
BUILD_ARGS_JSON=$(jq --arg k "$key" --arg v "$val" '. + {($k): $v}' <<<"$BUILD_ARGS_JSON")
done <<< "$BUILD_ARGS"
fi
normalize_path() {
local p="$1"
while [[ "$p" == ./* ]]; do
p="${p#./}"
done
if [ "$p" != "/" ]; then
p="${p%/}"
fi
printf '%s' "$p"
}
while read -r group; do
GROUP_COUNT=$(echo "$group" | jq 'length')
CONTEXT=$(echo "$group" | jq -r '.[0].context')
DOCKERFILE=$(echo "$group" | jq -r '.[0].dockerfile')
context_norm="$(normalize_path "$CONTEXT")"
dockerfile_norm="$(normalize_path "$DOCKERFILE")"
DOCKERFILE_FOR_BAKE="$DOCKERFILE"
DOCKERFILE_FOR_BUILD="$DOCKERFILE"
if [ -n "$context_norm" ] && [ "$context_norm" != "." ]; then
if [[ "$dockerfile_norm" == "$context_norm/"* ]]; then
DOCKERFILE_FOR_BAKE="${dockerfile_norm#$context_norm/}"
DOCKERFILE_FOR_BUILD="$dockerfile_norm"
elif [[ "$dockerfile_norm" != /* ]]; then
DOCKERFILE_FOR_BUILD="${context_norm}/${dockerfile_norm}"
fi
fi
if [ "$GROUP_COUNT" -gt 1 ]; then
echo "==== Building $GROUP_COUNT images from $DOCKERFILE (bake) ===="
CACHE_REF_SUFFIXES=$(echo "$group" | jq -c 'map(.cache_ref // empty) | map(select(length > 0)) | unique')
CACHE_REF_SUFFIX=$(echo "$CACHE_REF_SUFFIXES" | jq -r '.[0] // empty')
UNIQUE_CACHE_COUNT=$(echo "$CACHE_REF_SUFFIXES" | jq -r 'length')
if [ "$UNIQUE_CACHE_COUNT" -gt 1 ]; then
echo "Warning: multiple cache_ref values for the same dockerfile. Using $CACHE_REF_SUFFIX." >&2
fi
CACHE_REF=""
if [ -n "$CACHE_REF_SUFFIX" ]; then
CACHE_REF="${{ inputs.registry_host }}/$CACHE_REF_SUFFIX"
fi
TARGETS_JSON="{}"
TARGET_NAMES=()
while read -r entry; do
IDX=$(echo "$entry" | jq -r '.key')
IMG=$(echo "$entry" | jq -c '.value')
IMAGE_NAME=$(echo "$IMG" | jq -r '.name')
TARGET=$(echo "$IMG" | jq -r '.target // empty')
FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}"
TAGS=()
TAGS+=("$FULL_IMAGE:sha-$SHA_SHORT")
for ver in "${VERSION_TAGS[@]}"; do
TAGS+=("$FULL_IMAGE:$ver")
done
TAGS_JSON=$(printf '%s\n' "${TAGS[@]}" | jq -R . | jq -s .)
TARGET_NAME="img_${IDX}"
TARGET_NAMES+=("$TARGET_NAME")
TARGET_OBJ=$(jq -n \
--arg context "$CONTEXT" \
--arg dockerfile "$DOCKERFILE_FOR_BAKE" \
--arg target "$TARGET" \
--argjson tags "$TAGS_JSON" \
--argjson args "$BUILD_ARGS_JSON" \
--argjson ssh "$SSH_BAKE_JSON" \
--arg cache_ref "$CACHE_REF" \
'{
context: $context,
dockerfile: $dockerfile,
tags: $tags,
args: $args
}
+ (if ($ssh != null) then {ssh: $ssh} else {} end)
+ (if ($target != "" and $target != "null") then {target: $target} else {} end)
+ (if ($cache_ref != "") then {"cache-from": ["type=registry,ref=" + $cache_ref], "cache-to": ["type=registry,ref=" + $cache_ref + ",mode=max"]} else {} end)')
TARGETS_JSON=$(jq -n --arg name "$TARGET_NAME" --argjson target "$TARGET_OBJ" --argjson targets "$TARGETS_JSON" '$targets + {($name): $target}')
done < <(echo "$group" | jq -c 'to_entries[]')
GROUP_TARGETS_JSON=$(printf '%s\n' "${TARGET_NAMES[@]}" | jq -R . | jq -s .)
BAKE_JSON=$(jq -n --argjson targets "$TARGETS_JSON" --argjson group_targets "$GROUP_TARGETS_JSON" '{target:$targets, group:{default:{targets:$group_targets}}}')
BAKE_FILE=$(mktemp)
echo "$BAKE_JSON" > "$BAKE_FILE"
docker buildx bake --file "$BAKE_FILE" --push "${BAKE_ALLOW_FLAGS[@]}"
rm -f "$BAKE_FILE"
while read -r img; do
IMAGE_NAME=$(echo "$img" | jq -r '.name')
FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}"
echo "==== Trivy scan for $FULL_IMAGE ===="
trivy image \
--severity "$TRIVY_SEVERITY" \
--exit-code 1 \
"$FULL_IMAGE:sha-$SHA_SHORT"
done < <(echo "$group" | jq -c '.[]')
else
img=$(echo "$group" | jq -c '.[0]')
IMAGE_NAME=$(echo "$img" | jq -r '.name')
FULL_IMAGE="${{ inputs.registry_host }}/${IMAGE_NAME}"
CACHE_REF_SUFFIX=$(echo "$img" | jq -r '.cache_ref // empty')
CONTEXT=$(echo "$img" | jq -r '.context')
DOCKERFILE=$(echo "$img" | jq -r '.dockerfile')
TARGET=$(echo "$img" | jq -r '.target // empty')
TAGS=()
TAGS+=("$FULL_IMAGE:sha-$SHA_SHORT")
for ver in "${VERSION_TAGS[@]}"; do
TAGS+=("$FULL_IMAGE:$ver")
done
TAG_ARGS=()
for tag in "${TAGS[@]}"; do
TAG_ARGS+=(--tag "$tag")
done
TARGET_FLAGS=()
if [ -n "$TARGET" ] && [ "$TARGET" != "null" ]; then
TARGET_FLAGS+=(--target "$TARGET")
fi
CACHE_FLAGS=()
if [ -n "$CACHE_REF_SUFFIX" ]; then
CACHE_REF="${{ inputs.registry_host }}/${CACHE_REF_SUFFIX}"
CACHE_FLAGS+=(--cache-from "type=registry,ref=$CACHE_REF")
CACHE_FLAGS+=(--cache-to "type=registry,ref=$CACHE_REF,mode=max")
fi
echo "==== Building $FULL_IMAGE ===="
docker buildx build \
--file "$DOCKERFILE_FOR_BUILD" \
"${TARGET_FLAGS[@]}" \
"${CACHE_FLAGS[@]}" \
"${SSH_FLAGS[@]}" \
--push \
"${TAG_ARGS[@]}" \
"${BUILD_ARG_FLAGS[@]}" \
"$CONTEXT"
echo "==== Trivy scan for $FULL_IMAGE ===="
trivy image \
--severity "$TRIVY_SEVERITY" \
--exit-code 1 \
"${TAGS[0]}"
fi
done < <(echo "$IMAGES" | jq -c 'sort_by(.context, .dockerfile) | group_by([.context, .dockerfile])[]')

123
.github/workflows/node-ci.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Node CI
on:
workflow_call:
inputs:
node_version:
type: string
default: "20"
pnpm_version:
type: string
default: "10.23.0"
working_directory:
type: string
default: "."
cache_dependency_path:
type: string
default: "pnpm-lock.yaml"
env:
description: >
Multiline env vars, one per line: KEY=VALUE
required: false
type: string
default: ""
format_command:
type: string
default: "pnpm format:check"
lint_command:
type: string
default: "pnpm lint"
typecheck_command:
type: string
default: "pnpm typecheck"
test_command:
type: string
default: "pnpm test"
secrets:
ssh_private_key:
required: false
ssh_known_hosts:
required: false
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Load env vars
if: ${{ inputs.env != '' }}
run: |
while IFS= read -r line; do
[ -z "$line" ] && continue
case "$line" in \#*) continue;; esac
if [[ "$line" != *=* ]]; then
echo "Invalid env line: $line" >&2
exit 1
fi
echo "$line" >> "$GITHUB_ENV"
done <<< "${{ inputs.env }}"
- name: Start ssh-agent
if: ${{ secrets.ssh_private_key != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ssh_private_key }}
- name: Add SSH known hosts
if: ${{ secrets.ssh_known_hosts != '' }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.ssh_known_hosts }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ inputs.pnpm_version }}
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Get pnpm store path
id: pnpm-store
run: echo "store_path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
working-directory: ${{ inputs.working_directory }}
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.store_path }}
key: ${{ runner.os }}-pnpm-${{ inputs.pnpm_version }}-${{ hashFiles(inputs.cache_dependency_path) }}
restore-keys: |
${{ runner.os }}-pnpm-${{ inputs.pnpm_version }}-
${{ runner.os }}-pnpm-
- name: Install dependencies
run: pnpm install --frozen-lockfile
working-directory: ${{ inputs.working_directory }}
- name: Run format check
if: ${{ inputs.format_command != '' }}
run: ${{ inputs.format_command }}
working-directory: ${{ inputs.working_directory }}
- name: Run lint
if: ${{ inputs.lint_command != '' }}
run: ${{ inputs.lint_command }}
working-directory: ${{ inputs.working_directory }}
- name: Run typecheck
if: ${{ inputs.typecheck_command != '' }}
run: ${{ inputs.typecheck_command }}
working-directory: ${{ inputs.working_directory }}
- name: Run tests
if: ${{ inputs.test_command != '' }}
run: ${{ inputs.test_command }}
working-directory: ${{ inputs.working_directory }}

97
.github/workflows/pre-commit.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: Pre-commit
on:
workflow_call:
inputs:
python_version:
type: string
default: "3.13"
pre_commit_version:
type: string
default: "latest"
working_directory:
type: string
default: "."
cache_dependency_path:
type: string
default: ".pre-commit-config.yaml"
env:
description: >
Multiline env vars, one per line: KEY=VALUE
required: false
type: string
default: ""
pre_commit_command:
type: string
default: "pre-commit run --all-files --show-diff-on-failure"
secrets:
ssh_private_key:
required: false
ssh_known_hosts:
required: false
jobs:
pre-commit:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load env vars
if: ${{ inputs.env != '' }}
run: |
while IFS= read -r line; do
[ -z "$line" ] && continue
case "$line" in \#*) continue;; esac
if [[ "$line" != *=* ]]; then
echo "Invalid env line: $line" >&2
exit 1
fi
echo "$line" >> "$GITHUB_ENV"
done <<< "${{ inputs.env }}"
- name: Start ssh-agent
if: ${{ secrets.ssh_private_key != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ssh_private_key }}
- name: Add SSH known hosts
if: ${{ secrets.ssh_known_hosts != '' }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.ssh_known_hosts }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python_version }}
- name: Install pre-commit
env:
PRE_COMMIT_VERSION: ${{ inputs.pre_commit_version }}
run: |
python -m pip install --upgrade pip
if [ -z "$PRE_COMMIT_VERSION" ] || [ "$PRE_COMMIT_VERSION" = "latest" ]; then
python -m pip install pre-commit
else
python -m pip install "pre-commit==$PRE_COMMIT_VERSION"
fi
- name: Cache pre-commit
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles(inputs.cache_dependency_path) }}
restore-keys: |
pre-commit-${{ runner.os }}-
- name: Run pre-commit
if: ${{ inputs.pre_commit_command != '' }}
run: ${{ inputs.pre_commit_command }}

View File

@@ -0,0 +1,138 @@
name: Python UV Quality (with DB)
on:
workflow_call:
inputs:
python_version:
type: string
default: "3.13"
uv_version:
type: string
default: "latest"
working_directory:
type: string
default: "."
cache_dependency_path:
type: string
default: "uv.lock"
env:
description: >
Multiline env vars, one per line: KEY=VALUE
required: false
type: string
default: ""
uv_sync_args:
type: string
default: "--frozen --dev"
format_command:
type: string
default: "uv run ruff format --check ."
lint_command:
type: string
default: "uv run ruff check ."
typecheck_command:
type: string
default: "uv run mypy ."
test_command:
type: string
default: "uv run pytest"
secrets:
ssh_private_key:
required: false
ssh_known_hosts:
required: false
jobs:
quality:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load env vars
if: ${{ inputs.env != '' }}
run: |
while IFS= read -r line; do
[ -z "$line" ] && continue
case "$line" in \#*) continue;; esac
if [[ "$line" != *=* ]]; then
echo "Invalid env line: $line" >&2
exit 1
fi
echo "$line" >> "$GITHUB_ENV"
done <<< "${{ inputs.env }}"
- name: Start ssh-agent
if: ${{ secrets.ssh_private_key != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ssh_private_key }}
- name: Add SSH known hosts
if: ${{ secrets.ssh_known_hosts != '' }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.ssh_known_hosts }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python_version }}
- name: Install uv
env:
UV_VERSION: ${{ inputs.uv_version }}
run: |
python -m pip install --upgrade pip
if [ -z "$UV_VERSION" ] || [ "$UV_VERSION" = "latest" ]; then
python -m pip install uv
else
python -m pip install "uv==$UV_VERSION"
fi
- name: Cache uv downloads
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles(inputs.cache_dependency_path) }}
restore-keys: |
uv-${{ runner.os }}-
- name: Sync dependencies
run: uv sync ${{ inputs.uv_sync_args }}
- name: Run format check
if: ${{ inputs.format_command != '' }}
run: ${{ inputs.format_command }}
- name: Run lint
if: ${{ inputs.lint_command != '' }}
run: ${{ inputs.lint_command }}
- name: Run typecheck
if: ${{ inputs.typecheck_command != '' }}
run: ${{ inputs.typecheck_command }}
- name: Run tests
if: ${{ inputs.test_command != '' }}
run: ${{ inputs.test_command }}

124
.github/workflows/python-uv-ci.yml vendored Normal file
View File

@@ -0,0 +1,124 @@
name: Python UV Quality
on:
workflow_call:
inputs:
python_version:
type: string
default: "3.13"
uv_version:
type: string
default: "latest"
working_directory:
type: string
default: "."
cache_dependency_path:
type: string
default: "uv.lock"
env:
description: >
Multiline env vars, one per line: KEY=VALUE
required: false
type: string
default: ""
uv_sync_args:
type: string
default: "--frozen --dev"
format_command:
type: string
default: "uv run ruff format --check ."
lint_command:
type: string
default: "uv run ruff check ."
typecheck_command:
type: string
default: "uv run mypy ."
test_command:
type: string
default: "uv run pytest"
secrets:
ssh_private_key:
required: false
ssh_known_hosts:
required: false
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load env vars
if: ${{ inputs.env != '' }}
run: |
while IFS= read -r line; do
[ -z "$line" ] && continue
case "$line" in \#*) continue;; esac
if [[ "$line" != *=* ]]; then
echo "Invalid env line: $line" >&2
exit 1
fi
echo "$line" >> "$GITHUB_ENV"
done <<< "${{ inputs.env }}"
- name: Start ssh-agent
if: ${{ secrets.ssh_private_key != '' }}
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ssh_private_key }}
- name: Add SSH known hosts
if: ${{ secrets.ssh_known_hosts != '' }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.ssh_known_hosts }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python_version }}
- name: Install uv
env:
UV_VERSION: ${{ inputs.uv_version }}
run: |
python -m pip install --upgrade pip
if [ -z "$UV_VERSION" ] || [ "$UV_VERSION" = "latest" ]; then
python -m pip install uv
else
python -m pip install "uv==$UV_VERSION"
fi
- name: Cache uv downloads
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles(inputs.cache_dependency_path) }}
restore-keys: |
uv-${{ runner.os }}-
- name: Sync dependencies
run: uv sync ${{ inputs.uv_sync_args }}
- name: Run format check
if: ${{ inputs.format_command != '' }}
run: ${{ inputs.format_command }}
- name: Run lint
if: ${{ inputs.lint_command != '' }}
run: ${{ inputs.lint_command }}
- name: Run typecheck
if: ${{ inputs.typecheck_command != '' }}
run: ${{ inputs.typecheck_command }}
- name: Run tests
if: ${{ inputs.test_command != '' }}
run: ${{ inputs.test_command }}