Files
stack/.gitea/workflows/deploy.yml
kert e8ac5143f4
All checks were successful
CI / lint (push) Successful in 1m1s
CI / test (push) Successful in 3m6s
CI / notebooks-smoke (push) Successful in 1m49s
Deploy / changes (push) Successful in 19s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / bcda (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Deploy / report (push) Successful in 25s
fix(deploy): diff from the last successful deploy, not the previous push
The changes job diffed github.event.before..SHA, so a re-push that
cancelled the run in flight dropped that push's paths for good: the
notebooks image never rebuilt for 9cba4fd (Dockerfile) or 409fc6f
(notebooks/), and the registry stayed on the a4b4110 Harden build.

A new base step asks Gitea for the newest successful deploy.yml run on
main and the detector diffs from its head_sha. Cancelled or failed runs
stay in the next run's range until they land. No successful run or an
API error leaves the base empty, which builds everything. The runner
image has no python or jq, so the run JSON is paired with grep + awk;
awk reads to EOF so grep never takes a SIGPIPE under pipefail.

workflow_dispatch added so a catch-up run needs no push.
2026-10-27 15:57:47 -04:00

528 lines
21 KiB
YAML

# DO NOT EDIT — generated by gen_config.py from stack.toml
# Re-generate: uv run python dev/scripts/gen_config.py
name: Deploy
on:
workflow_dispatch:
push:
branches: [main]
jobs:
changes:
runs-on: ubuntu-latest
outputs:
notebooks: ${{ steps.filter.outputs.notebooks }}
zotero: ${{ steps.filter.outputs.zotero }}
docs: ${{ steps.filter.outputs.docs }}
api: ${{ steps.filter.outputs.api }}
bcda: ${{ steps.filter.outputs.bcda }}
llm: ${{ steps.filter.outputs.llm }}
mc: ${{ steps.filter.outputs.mc }}
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Find the last successful deploy
id: base
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
API: http://git:3000/api/v1
REPO: ${{ github.repository }}
run: |
# Diff from the last *successful* Deploy, not from the previous
# push: a re-push cancels the run in flight, and diffing only
# github.event.before then dropped the cancelled push's paths for
# good (2026-10-27: the notebooks image never rebuilt for 9cba4fd
# or 409fc6f). A failed run likewise stays in the next run's range
# until it lands. The runner image has no python or jq; Gitea
# writes each run's "path" before its "head_sha", so grep + awk
# pair them (awk reads to the end, so grep never takes a SIGPIPE
# under pipefail). Nothing found leaves sha empty: build everything.
sha="$(curl -sf -H "Authorization: token $GITEA_TOKEN" \
"$API/repos/$REPO/actions/runs?status=success&branch=main&limit=50" \
| grep -o '"path":"[^"]*"\|"head_sha":"[0-9a-f]*"' \
| awk -F'"' '$2 == "path" {p = $4}
$2 == "head_sha" && !sha && index(p, "deploy.yml@") == 1 {sha = $4}
END {print sha}')" \
|| sha=""
echo "last successful deploy: ${sha:-none}"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Detect changed paths
id: filter
env:
BEFORE: ${{ steps.base.outputs.sha }}
run: |
# `github.event.head_commit.modified` only describes the tip commit
# and is empty for merge commits, so a PR merged with a merge commit
# never matched any path filter. Diff a range instead: from the
# last successfully deployed commit (the base step) to the new tip.
# When that commit is unknown or unfetchable (no successful run,
# API unreachable, history rewrite) build everything rather than
# nothing.
changed=__all__
if [ -n "$BEFORE" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ] \
&& git fetch --quiet --depth=1 origin "$BEFORE"; then
changed="$(git diff --name-only "$BEFORE" "$GITHUB_SHA")" || changed=__all__
fi
echo "changed since $BEFORE:"
printf '%s\n' "$changed"
match() {
name="$1"; shift
hit=false
if [ "$changed" = __all__ ]; then
hit=true
else
while IFS= read -r f; do
[ -n "$f" ] || continue
for g in "$@"; do
case "$f" in $g) hit=true ;; esac
done
done <<< "$changed"
fi
echo "$name=$hit" >> "$GITHUB_OUTPUT"
echo "$name: $hit"
}
match notebooks 'notebooks/*' 'infra/marimo/*' 'infra/images/notebooks.Dockerfile'
match zotero 'infra/images/zotero.Dockerfile' 'infra/images/zotero/*'
match docs 'docs/*' 'infra/images/docs.Dockerfile'
match api 'infra/images/api.Dockerfile' 'src/*' 'pyproject.toml'
match bcda 'infra/images/bcda.Dockerfile' 'infra/images/bcda/*' 'src/*' 'pyproject.toml'
match llm 'infra/images/llm.Dockerfile' 'src/*' 'pyproject.toml'
match mc 'infra/rustfs/*' 'infra/images/mc.Dockerfile'
notebooks:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.notebooks == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan notebooks
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o notebooks-scan.json local/notebooks:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build notebooks
run: docker build -f infra/images/notebooks.Dockerfile -t local/notebooks:build .
- name: Push notebooks
run: |
docker save local/notebooks:build -o /tmp/notebooks.tar
crane push /tmp/notebooks.tar git:3000/homelab/stack/notebooks:${{ env.SHORT_SHA }} --insecure
crane push /tmp/notebooks.tar git:3000/homelab/stack/notebooks:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "notebooks" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
zotero:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.zotero == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan zotero
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o zotero-scan.json local/zotero:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build zotero
run: docker build -f infra/images/zotero.Dockerfile -t local/zotero:build .
- name: Push zotero
run: |
docker save local/zotero:build -o /tmp/zotero.tar
crane push /tmp/zotero.tar git:3000/homelab/stack/zotero:${{ env.SHORT_SHA }} --insecure
crane push /tmp/zotero.tar git:3000/homelab/stack/zotero:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "zotero" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
docs:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.docs == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan docs
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o docs-scan.json local/docs:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build docs
run: docker build -f infra/images/docs.Dockerfile -t local/docs:build .
- name: Push docs
run: |
docker save local/docs:build -o /tmp/docs.tar
crane push /tmp/docs.tar git:3000/homelab/stack/docs:${{ env.SHORT_SHA }} --insecure
crane push /tmp/docs.tar git:3000/homelab/stack/docs:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "docs" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
api:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.api == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan api
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o api-scan.json local/api:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build api
run: docker build -f infra/images/api.Dockerfile -t local/api:build .
- name: Push api
run: |
docker save local/api:build -o /tmp/api.tar
crane push /tmp/api.tar git:3000/homelab/stack/api:${{ env.SHORT_SHA }} --insecure
crane push /tmp/api.tar git:3000/homelab/stack/api:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "api" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
bcda:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.bcda == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan bcda
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o bcda-scan.json local/bcda:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build bcda
run: docker build -f infra/images/bcda.Dockerfile -t local/bcda:build .
- name: Push bcda
run: |
docker save local/bcda:build -o /tmp/bcda.tar
crane push /tmp/bcda.tar git:3000/homelab/stack/bcda:${{ env.SHORT_SHA }} --insecure
crane push /tmp/bcda.tar git:3000/homelab/stack/bcda:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "bcda" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
llm:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.llm == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan llm
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o llm-scan.json local/llm:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build llm
run: docker build -f infra/images/llm.Dockerfile -t local/llm:build .
- name: Push llm
run: |
docker save local/llm:build -o /tmp/llm.tar
crane push /tmp/llm.tar git:3000/homelab/stack/llm:${{ env.SHORT_SHA }} --insecure
crane push /tmp/llm.tar git:3000/homelab/stack/llm:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "llm" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
mc:
runs-on: ubuntu-latest
needs: [changes]
if: needs.changes.outputs.mc == 'true'
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install crane
run: |
cd /tmp
base=https://github.com/google/go-containerregistry/releases/download/v0.22.1
tgz=go-containerregistry_Linux_x86_64.tar.gz
curl -fsSLO "$base/$tgz"
curl -fsSL "$base/checksums.txt" | grep -F " $tgz" | sha256sum -c -
tar xzf "$tgz" -C /usr/local/bin crane
- name: Log in to registry
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
env:
CRANE_INSECURE: "true"
- name: Install trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v0.76.0/contrib/install.sh | sh -s -- -b /usr/local/bin v0.76.0
- name: Scan mc
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o mc-scan.json local/mc:build
- name: Compute short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
- name: Build mc
run: docker build -f infra/images/mc.Dockerfile -t local/mc:build infra/rustfs/
- name: Push mc
run: |
docker save local/mc:build -o /tmp/mc.tar
crane push /tmp/mc.tar git:3000/homelab/stack/mc:${{ env.SHORT_SHA }} --insecure
crane push /tmp/mc.tar git:3000/homelab/stack/mc:latest --insecure
- name: File failure issue
if: failure()
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
# Jobs that never install uv (image builds, infra checks) still
# need it here — otherwise the filer dies with "uv: command not
# found" and the failure is never filed.
command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "mc" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true
report:
runs-on: ubuntu-latest
needs: [changes, notebooks, zotero, docs, api, bcda, llm, mc]
if: always() && !cancelled()
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Set up uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
env:
UV_INSTALL_DIR: /usr/local/bin
- name: Report
# Only file an issue if at least one upstream build job failed.
# Without this gate api.diag.ci runs unconditionally and files
# a fake "failed" issue on every successful Deploy run.
if: contains(needs.*.result, 'failure')
env:
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
uv sync --no-dev --quiet 2>/dev/null || true
uv run python -m api.diag.ci \
--workflow "Deploy" --job "report" \
--run "${{ github.run_number }}" \
--sha "${{ github.sha }}" \
--ref "${{ github.ref }}" || true