merge: #1365 — security posture: lock past 32 CVEs with a 7-day cooldown, image scanning restored, Renovate actually running (refs #1365)
All checks were successful
CI / lint (push) Successful in 43s
Infra CI / zotero (push) Successful in 21s
CI / test (push) Successful in 2m22s
CI / notebooks-smoke (push) Successful in 1m41s
Deploy / changes (push) Successful in 15s
Infra CI / docs (push) Successful in 13s
Infra CI / notebooks (push) Successful in 56s
Infra CI / api (push) Successful in 13s
Infra CI / llm (push) Successful in 21s
Infra CI / mc (push) Successful in 17s
Package Supply Chain / pkg-supply-chain (push) Successful in 2m20s
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Successful in 1m2s
Deploy / api (push) Successful in 1m9s
Deploy / notebooks (push) Successful in 5m59s
Deploy / llm (push) Successful in 1m33s
Deploy / mc (push) Successful in 41s
Deploy / report (push) Successful in 15s
All checks were successful
CI / lint (push) Successful in 43s
Infra CI / zotero (push) Successful in 21s
CI / test (push) Successful in 2m22s
CI / notebooks-smoke (push) Successful in 1m41s
Deploy / changes (push) Successful in 15s
Infra CI / docs (push) Successful in 13s
Infra CI / notebooks (push) Successful in 56s
Infra CI / api (push) Successful in 13s
Infra CI / llm (push) Successful in 21s
Infra CI / mc (push) Successful in 17s
Package Supply Chain / pkg-supply-chain (push) Successful in 2m20s
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Successful in 1m2s
Deploy / api (push) Successful in 1m9s
Deploy / notebooks (push) Successful in 5m59s
Deploy / llm (push) Successful in 1m33s
Deploy / mc (push) Successful in 41s
Deploy / report (push) Successful in 15s
This commit is contained in:
@@ -71,7 +71,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -80,7 +86,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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
|
||||
@@ -123,7 +129,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -132,7 +144,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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
|
||||
@@ -175,7 +187,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -184,7 +202,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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
|
||||
@@ -227,7 +245,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -236,7 +260,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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
|
||||
@@ -279,7 +303,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -288,7 +318,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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
|
||||
@@ -331,7 +361,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -339,6 +375,12 @@ jobs:
|
||||
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"
|
||||
|
||||
|
||||
@@ -16,7 +16,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -24,7 +30,7 @@ jobs:
|
||||
CRANE_INSECURE: "true"
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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: Set up uv
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
@@ -32,86 +38,127 @@ jobs:
|
||||
UV_INSTALL_DIR: /usr/local/bin
|
||||
|
||||
- name: Build notebooks
|
||||
run: docker build --no-cache -f infra/images/notebooks.Dockerfile -t local/notebooks:build .
|
||||
id: build_notebooks
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f infra/images/notebooks.Dockerfile -t local/notebooks:build .
|
||||
|
||||
- name: Scan notebooks
|
||||
if: ${{ steps.build_notebooks.outcome == 'success' }}
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o notebooks-scan.json local/notebooks:build
|
||||
|
||||
- name: Push notebooks
|
||||
if: ${{ steps.build_notebooks.outcome == 'success' }}
|
||||
run: |
|
||||
docker save local/notebooks:build -o /tmp/notebooks.tar
|
||||
crane push /tmp/notebooks.tar git:3000/homelab/stack/notebooks:hardened --insecure
|
||||
crane push /tmp/notebooks.tar git:3000/homelab/stack/notebooks:latest --insecure
|
||||
|
||||
- name: Build zotero
|
||||
run: docker build --no-cache -f infra/images/zotero.Dockerfile -t local/zotero:build .
|
||||
id: build_zotero
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f infra/images/zotero.Dockerfile -t local/zotero:build .
|
||||
|
||||
- name: Scan zotero
|
||||
if: ${{ steps.build_zotero.outcome == 'success' }}
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o zotero-scan.json local/zotero:build
|
||||
|
||||
- name: Push zotero
|
||||
if: ${{ steps.build_zotero.outcome == 'success' }}
|
||||
run: |
|
||||
docker save local/zotero:build -o /tmp/zotero.tar
|
||||
crane push /tmp/zotero.tar git:3000/homelab/stack/zotero:hardened --insecure
|
||||
crane push /tmp/zotero.tar git:3000/homelab/stack/zotero:latest --insecure
|
||||
|
||||
- name: Build docs
|
||||
run: docker build --no-cache -f infra/images/docs.Dockerfile -t local/docs:build .
|
||||
id: build_docs
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f infra/images/docs.Dockerfile -t local/docs:build .
|
||||
|
||||
- name: Scan docs
|
||||
if: ${{ steps.build_docs.outcome == 'success' }}
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o docs-scan.json local/docs:build
|
||||
|
||||
- name: Push docs
|
||||
if: ${{ steps.build_docs.outcome == 'success' }}
|
||||
run: |
|
||||
docker save local/docs:build -o /tmp/docs.tar
|
||||
crane push /tmp/docs.tar git:3000/homelab/stack/docs:hardened --insecure
|
||||
crane push /tmp/docs.tar git:3000/homelab/stack/docs:latest --insecure
|
||||
|
||||
- name: Build api
|
||||
run: docker build --no-cache -f infra/images/api.Dockerfile -t local/api:build .
|
||||
id: build_api
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f infra/images/api.Dockerfile -t local/api:build .
|
||||
|
||||
- name: Scan api
|
||||
if: ${{ steps.build_api.outcome == 'success' }}
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o api-scan.json local/api:build
|
||||
|
||||
- name: Push api
|
||||
if: ${{ steps.build_api.outcome == 'success' }}
|
||||
run: |
|
||||
docker save local/api:build -o /tmp/api.tar
|
||||
crane push /tmp/api.tar git:3000/homelab/stack/api:hardened --insecure
|
||||
crane push /tmp/api.tar git:3000/homelab/stack/api:latest --insecure
|
||||
|
||||
- name: Build llm
|
||||
run: docker build --no-cache -f infra/images/llm.Dockerfile -t local/llm:build .
|
||||
id: build_llm
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f infra/images/llm.Dockerfile -t local/llm:build .
|
||||
|
||||
- name: Scan llm
|
||||
if: ${{ steps.build_llm.outcome == 'success' }}
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o llm-scan.json local/llm:build
|
||||
|
||||
- name: Push llm
|
||||
if: ${{ steps.build_llm.outcome == 'success' }}
|
||||
run: |
|
||||
docker save local/llm:build -o /tmp/llm.tar
|
||||
crane push /tmp/llm.tar git:3000/homelab/stack/llm:hardened --insecure
|
||||
crane push /tmp/llm.tar git:3000/homelab/stack/llm:latest --insecure
|
||||
|
||||
- name: Build mc
|
||||
run: docker build --no-cache -f infra/images/mc.Dockerfile -t local/mc:build infra/rustfs/
|
||||
id: build_mc
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f infra/images/mc.Dockerfile -t local/mc:build infra/rustfs/
|
||||
|
||||
- name: Scan mc
|
||||
if: ${{ steps.build_mc.outcome == 'success' }}
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o mc-scan.json local/mc:build
|
||||
|
||||
- name: Push mc
|
||||
if: ${{ steps.build_mc.outcome == 'success' }}
|
||||
run: |
|
||||
docker save local/mc:build -o /tmp/mc.tar
|
||||
crane push /tmp/mc.tar git:3000/homelab/stack/mc:hardened --insecure
|
||||
crane push /tmp/mc.tar git:3000/homelab/stack/mc:latest --insecure
|
||||
|
||||
- name: Scan notebooks
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o notebooks-scan.json local/notebooks:build
|
||||
|
||||
- name: Scan zotero
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o zotero-scan.json local/zotero:build
|
||||
|
||||
- name: Scan docs
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o docs-scan.json local/docs:build
|
||||
|
||||
- name: Scan api
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o api-scan.json local/api:build
|
||||
|
||||
- name: Scan llm
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o llm-scan.json local/llm:build
|
||||
|
||||
- name: Close resolved or file new vuln issues
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||
run: |
|
||||
uv sync --no-dev
|
||||
for f in notebooks-scan.json zotero-scan.json docs-scan.json api-scan.json llm-scan.json; do
|
||||
for f in notebooks-scan.json zotero-scan.json docs-scan.json api-scan.json llm-scan.json mc-scan.json; do
|
||||
if [ -f "$f" ]; then
|
||||
uv run python -m api.diag.vuln --close "$f" || \
|
||||
uv run python -m api.diag.vuln "$f" || true
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Fail if any image failed to build
|
||||
run: |
|
||||
failed=""
|
||||
[ "${{ steps.build_notebooks.outcome }}" = success ] || failed="$failed notebooks"
|
||||
[ "${{ steps.build_zotero.outcome }}" = success ] || failed="$failed zotero"
|
||||
[ "${{ steps.build_docs.outcome }}" = success ] || failed="$failed docs"
|
||||
[ "${{ steps.build_api.outcome }}" = success ] || failed="$failed api"
|
||||
[ "${{ steps.build_llm.outcome }}" = success ] || failed="$failed llm"
|
||||
[ "${{ steps.build_mc.outcome }}" = success ] || failed="$failed mc"
|
||||
if [ -n "$failed" ]; then
|
||||
echo "Images that failed to build (not scanned, not pushed):$failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: File failure issue
|
||||
if: failure()
|
||||
env:
|
||||
|
||||
@@ -45,13 +45,15 @@ jobs:
|
||||
run: uv run python dev/scripts/pkg_drift.py
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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: Vulnerability scan
|
||||
run: |
|
||||
set -euo pipefail
|
||||
trivy fs --scanners vuln --format json -o data/pkg-vulns.json uv.lock
|
||||
trivy fs --scanners vuln --format json -o data/pkg-vulns-pyproject.json pyproject.toml
|
||||
# The docs site's build toolchain — never scanned before (116 CVEs, 2026-10-27).
|
||||
trivy fs --scanners vuln --format json -o data/pkg-vulns-docs.json docs/pnpm-lock.yaml
|
||||
|
||||
- name: Auto-create issues for drift and vulns
|
||||
env:
|
||||
|
||||
@@ -14,7 +14,13 @@ jobs:
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin 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 }}"
|
||||
@@ -22,7 +28,7 @@ jobs:
|
||||
CRANE_INSECURE: "true"
|
||||
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
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: Set up uv
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
@@ -101,12 +107,15 @@ jobs:
|
||||
- name: Scan llm
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o llm-scan.json local/llm:build
|
||||
|
||||
- name: Scan mc
|
||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o mc-scan.json local/mc:build
|
||||
|
||||
- name: Report vulnerabilities
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||
run: |
|
||||
uv sync --no-dev
|
||||
for f in notebooks-scan.json zotero-scan.json docs-scan.json api-scan.json llm-scan.json; do
|
||||
for f in notebooks-scan.json zotero-scan.json docs-scan.json api-scan.json llm-scan.json mc-scan.json; do
|
||||
[ -f "$f" ] && uv run python -m api.diag.vuln "$f" || true
|
||||
done
|
||||
|
||||
|
||||
@@ -8,14 +8,21 @@ on:
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:latest
|
||||
# Pinned: a floating `latest` let a Renovate major change behaviour
|
||||
# unreviewed. Renovate bumps this pin itself (github-actions manager).
|
||||
container: ghcr.io/renovatebot/renovate:44.171.1
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Run Renovate
|
||||
run: renovate
|
||||
env:
|
||||
RENOVATE_CONFIG_FILE: renovate.json
|
||||
# Self-hosted (global) settings live here; renovate.json is the
|
||||
# repo config. Without RENOVATE_REPOSITORIES every run was a silent
|
||||
# no-op ("No repositories found") — 28 green runs, zero PRs.
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: http://git:3000/api/v1
|
||||
RENOVATE_REPOSITORIES: homelab/stack
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@fhirworx.io>"
|
||||
LOG_LEVEL: "info"
|
||||
|
||||
@@ -121,11 +121,12 @@ services:
|
||||
networks:
|
||||
- storage
|
||||
environment:
|
||||
- MC_HOST_local=http://${RUSTFS_ACCESS_KEY}:${RUSTFS_SECRET_KEY}@rustfs:9000
|
||||
- RC_HOST_local=http://${RUSTFS_ACCESS_KEY}:${RUSTFS_SECRET_KEY}@rustfs:9000
|
||||
- RC_CONFIG_DIR=/home/mc/.config/rc
|
||||
user: "10002:10002"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /home/mc/.mc:uid=10002,gid=10002
|
||||
- /home/mc/.config/rc:uid=10002,gid=10002
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
},
|
||||
{
|
||||
"name": "pymupdf",
|
||||
"version": ">=1.24",
|
||||
"version": ">=1.27.2,<2",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"version": ">=1.5.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"version": ">=1.5.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -303,7 +303,7 @@
|
||||
},
|
||||
{
|
||||
"name": "pyjwt",
|
||||
"version": ">=2.13.0",
|
||||
"version": ">=2.15.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -318,7 +318,7 @@
|
||||
},
|
||||
{
|
||||
"name": "fsspec",
|
||||
"version": ">=2024.1.0",
|
||||
"version": ">=2026.6.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -386,6 +386,11 @@
|
||||
"version": ">=2.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pyyaml",
|
||||
"version": ">=6.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pydantic",
|
||||
"version": ">=2.0.0",
|
||||
@@ -428,7 +433,7 @@
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"version": ">=1.5.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -438,7 +443,7 @@
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"version": ">=1.5.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -446,9 +451,14 @@
|
||||
"version": ">=2.17.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pydantic",
|
||||
"version": ">=2.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"version": ">=1.5.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -473,7 +483,7 @@
|
||||
},
|
||||
{
|
||||
"name": "fsspec",
|
||||
"version": ">=2024.1.0",
|
||||
"version": ">=2026.6.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -508,12 +518,12 @@
|
||||
},
|
||||
{
|
||||
"name": "opentelemetry-api",
|
||||
"version": ">=1.25.0",
|
||||
"version": ">=1.45.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "opentelemetry-sdk",
|
||||
"version": ">=1.25.0",
|
||||
"version": ">=1.45.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -599,47 +609,47 @@
|
||||
"npm": [
|
||||
{
|
||||
"name": "@docusaurus/core",
|
||||
"version": "^3.7.0"
|
||||
"version": "^3.10.2"
|
||||
},
|
||||
{
|
||||
"name": "@docusaurus/preset-classic",
|
||||
"version": "^3.7.0"
|
||||
"version": "^3.10.2"
|
||||
},
|
||||
{
|
||||
"name": "@easyops-cn/docusaurus-search-local",
|
||||
"version": "^0.55.1"
|
||||
"version": "^0.55.3"
|
||||
},
|
||||
{
|
||||
"name": "@mdx-js/react",
|
||||
"version": "^3.0.0"
|
||||
"version": "^3.1.1"
|
||||
},
|
||||
{
|
||||
"name": "clsx",
|
||||
"version": "^2.0.0"
|
||||
"version": "^2.1.1"
|
||||
},
|
||||
{
|
||||
"name": "prism-react-renderer",
|
||||
"version": "^2.3.0"
|
||||
"version": "^2.4.1"
|
||||
},
|
||||
{
|
||||
"name": "react",
|
||||
"version": "^18.0.0"
|
||||
"version": "^18.3.1"
|
||||
},
|
||||
{
|
||||
"name": "react-dom",
|
||||
"version": "^18.0.0"
|
||||
"version": "^18.3.1"
|
||||
},
|
||||
{
|
||||
"name": "@docusaurus/module-type-aliases",
|
||||
"version": "^3.7.0"
|
||||
"version": "^3.10.2"
|
||||
},
|
||||
{
|
||||
"name": "@docusaurus/types",
|
||||
"version": "^3.7.0"
|
||||
"version": "^3.10.2"
|
||||
},
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "~5.6.0"
|
||||
"version": "~5.6.3"
|
||||
}
|
||||
],
|
||||
"ci_tools": [
|
||||
@@ -647,10 +657,6 @@
|
||||
"name": "uv",
|
||||
"version": "latest"
|
||||
},
|
||||
{
|
||||
"name": "crane",
|
||||
"version": "latest"
|
||||
},
|
||||
{
|
||||
"name": "trivy",
|
||||
"version": "latest"
|
||||
|
||||
@@ -14,6 +14,15 @@ from backends.github import (
|
||||
_HEADER,
|
||||
)
|
||||
|
||||
# Pinned scanner/registry tooling. A `latest`/`main` install would make the
|
||||
# tools that guard the supply chain an unpinned dependency themselves.
|
||||
# Renovate bumps these via the regex manager in renovate.json; re-run
|
||||
# gen_config.py after a bump.
|
||||
# renovate: datasource=github-releases depName=aquasecurity/trivy
|
||||
TRIVY_VERSION = "0.76.0"
|
||||
# renovate: datasource=github-releases depName=google/go-containerregistry
|
||||
CRANE_VERSION = "0.22.1"
|
||||
|
||||
# ── Override action refs with full GitHub URLs ───────────────────
|
||||
# Gitea Actions resolves short refs (e.g. actions/checkout@v4) against
|
||||
# the local Gitea instance. Use full URLs to pull from GitHub.
|
||||
@@ -57,7 +66,9 @@ def _build_push_step(
|
||||
name = img["name"]
|
||||
tags = [t.strip() for t in tags_expr.split(",") if t.strip()]
|
||||
local_tag = f"local/{name}:build"
|
||||
cache_flag = " --no-cache" if no_cache else ""
|
||||
# A from-scratch rebuild must also re-pull base images — --no-cache alone
|
||||
# reuses whatever stale base the runner already has.
|
||||
cache_flag = " --pull --no-cache" if no_cache else ""
|
||||
lines = f"""\
|
||||
- name: Build {name}
|
||||
run: docker build{cache_flag} -f {img["dockerfile"]} -t {local_tag} {img["context"]}"""
|
||||
@@ -76,10 +87,15 @@ def _build_push_step(
|
||||
|
||||
|
||||
def _install_trivy_step() -> str:
|
||||
"""Install trivy as a native binary — no Docker-in-Docker needed."""
|
||||
return """\
|
||||
"""Install a pinned trivy as a native binary — no Docker-in-Docker needed.
|
||||
|
||||
The installer from the release tag verifies the tarball's sha256 against
|
||||
the release checksums.
|
||||
"""
|
||||
v = TRIVY_VERSION
|
||||
return f"""\
|
||||
- name: Install trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin"""
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/v{v}/contrib/install.sh | sh -s -- -b /usr/local/bin v{v}"""
|
||||
|
||||
|
||||
def _trivy_step(
|
||||
@@ -129,7 +145,13 @@ def _docker_login_step(registry: str) -> str:
|
||||
"""Install crane and authenticate — daemonless push tool."""
|
||||
return f"""\
|
||||
- name: Install crane
|
||||
run: curl -sL https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin crane
|
||||
run: |
|
||||
cd /tmp
|
||||
base=https://github.com/google/go-containerregistry/releases/download/v{CRANE_VERSION}
|
||||
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 {registry} -u "${{{{ secrets.REGISTRY_USER }}}}" -p "${{{{ secrets.REGISTRY_TOKEN }}}}"
|
||||
@@ -412,21 +434,46 @@ def _gen_harden(
|
||||
uv_version: str,
|
||||
**_kw: object,
|
||||
) -> tuple[str, str]:
|
||||
build_steps = []
|
||||
# Each image is built, scanned and pushed on its own, and a failed build
|
||||
# only skips that image: one dead upstream base (the archived minio/mc,
|
||||
# Sept–Oct 2026) used to abort the job before any scan ran, leaving every
|
||||
# image unscanned for weeks. The gate step at the end still fails the job
|
||||
# — after the vuln report — so the breakage is filed, not swallowed.
|
||||
scan_names = {i["name"] for i in scans}
|
||||
image_steps = []
|
||||
for img in images:
|
||||
tags = (
|
||||
f"{registry}/{owner_repo}/{img['name']}:hardened,"
|
||||
f"{registry}/{owner_repo}/{img['name']}:latest"
|
||||
)
|
||||
build_steps.append(
|
||||
_build_push_step(img, tags, registry, owner_repo, no_cache=True)
|
||||
)
|
||||
build_block = "\n\n".join(build_steps)
|
||||
name = img["name"]
|
||||
step_id = f"build_{name.replace('-', '_')}"
|
||||
built = f"steps.{step_id}.outcome == 'success'"
|
||||
steps = [
|
||||
f"""\
|
||||
- name: Build {name}
|
||||
id: {step_id}
|
||||
continue-on-error: true
|
||||
run: docker build --pull --no-cache -f {img["dockerfile"]} -t local/{name}:build {img["context"]}"""
|
||||
]
|
||||
if name in scan_names:
|
||||
sev = img.get("trivy_severity", "HIGH,CRITICAL")
|
||||
ec = img.get("trivy_exit_code", 0)
|
||||
steps.append(f"""\
|
||||
- name: Scan {name}
|
||||
if: ${{{{ {built} }}}}
|
||||
run: trivy image --severity {sev} --exit-code {ec} --format json -o {name}-scan.json local/{name}:build""")
|
||||
steps.append(f"""\
|
||||
- name: Push {name}
|
||||
if: ${{{{ {built} }}}}
|
||||
run: |
|
||||
docker save local/{name}:build -o /tmp/{name}.tar
|
||||
crane push /tmp/{name}.tar {registry}/{owner_repo}/{name}:hardened --insecure
|
||||
crane push /tmp/{name}.tar {registry}/{owner_repo}/{name}:latest --insecure""")
|
||||
image_steps.append("\n\n".join(steps))
|
||||
image_block = "\n\n".join(image_steps)
|
||||
|
||||
scan_steps = []
|
||||
for img in scans:
|
||||
scan_steps.append(_trivy_step(img, "hardened", registry, owner_repo))
|
||||
scan_block = "\n\n".join(scan_steps)
|
||||
gate_lines = "\n ".join(
|
||||
f'[ "${{{{ steps.build_{i["name"].replace("-", "_")}.outcome }}}}" = success ]'
|
||||
f' || failed="$failed {i["name"]}"'
|
||||
for i in images
|
||||
)
|
||||
|
||||
vuln_files = " ".join(f"{i['name']}-scan.json" for i in scans)
|
||||
|
||||
@@ -451,9 +498,7 @@ jobs:
|
||||
|
||||
{_setup_uv_step(uv_version)}
|
||||
|
||||
{build_block}
|
||||
|
||||
{scan_block}
|
||||
{image_block}
|
||||
|
||||
- name: Close resolved or file new vuln issues
|
||||
env:
|
||||
@@ -467,6 +512,15 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Fail if any image failed to build
|
||||
run: |
|
||||
failed=""
|
||||
{gate_lines}
|
||||
if [ -n "$failed" ]; then
|
||||
echo "Images that failed to build (not scanned, not pushed):$failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{_failure_step("Harden", "build-scan-report")}
|
||||
"""
|
||||
return (".gitea/workflows/harden.yml", content)
|
||||
|
||||
@@ -20,7 +20,11 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MANIFEST_PATH = ROOT / "data" / "pkg-manifest.json"
|
||||
VULN_PATH = ROOT / "data" / "pkg-vulns.json"
|
||||
# trivy JSON reports, one per lockfile (uv.lock, docs/pnpm-lock.yaml)
|
||||
VULN_PATHS = (
|
||||
ROOT / "data" / "pkg-vulns.json",
|
||||
ROOT / "data" / "pkg-vulns-docs.json",
|
||||
)
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "http://localhost:3000")
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
||||
@@ -99,15 +103,41 @@ def _get_label_ids(names: list[str]) -> list[int]:
|
||||
return [name_to_id[n] for n in names if n in name_to_id]
|
||||
|
||||
|
||||
def _find_existing_issues(prefix: str) -> dict[str, dict]:
|
||||
"""Find open issues with titles starting with prefix."""
|
||||
issues = _api(
|
||||
"GET",
|
||||
f"repos/{REPO}/issues?state=open&type=issues&limit=50&labels={LABEL_CI}",
|
||||
)
|
||||
if not issues:
|
||||
return {}
|
||||
return {i["title"]: i for i in issues if i["title"].startswith(prefix)}
|
||||
def _find_existing_issues(prefix: str) -> dict[str, list[dict]]:
|
||||
"""Find open issues with titles starting with prefix, grouped by title.
|
||||
|
||||
Walks every page: a single 50-issue page hid most of the tracker once
|
||||
CI-failure issues piled up, so known CVEs were re-filed nightly (174
|
||||
issues for 91 titles) and resolved ones were never closed.
|
||||
"""
|
||||
found: dict[str, list[dict]] = {}
|
||||
page = 1
|
||||
while True:
|
||||
issues = _api(
|
||||
"GET",
|
||||
f"repos/{REPO}/issues?state=open&type=issues&limit=50"
|
||||
f"&page={page}&labels={LABEL_CI}",
|
||||
)
|
||||
if not issues:
|
||||
return found
|
||||
for i in issues:
|
||||
if i["title"].startswith(prefix):
|
||||
found.setdefault(i["title"], []).append(i)
|
||||
page += 1
|
||||
|
||||
|
||||
def _close_all(title: str, issues: list[dict], *, dry_run: bool) -> None:
|
||||
"""Close every open issue carrying ``title`` (duplicates included)."""
|
||||
for issue in issues:
|
||||
if dry_run:
|
||||
print(f" would close #{issue['number']}: {title}")
|
||||
else:
|
||||
_api(
|
||||
"PATCH",
|
||||
f"repos/{REPO}/issues/{issue['number']}",
|
||||
{"state": "closed"},
|
||||
)
|
||||
print(f" closed #{issue['number']}: {title}")
|
||||
|
||||
|
||||
def handle_drift(*, dry_run: bool = False) -> int:
|
||||
@@ -172,7 +202,7 @@ def handle_drift(*, dry_run: bool = False) -> int:
|
||||
created += 1
|
||||
|
||||
# Close issues for packages that are no longer missing
|
||||
for title, issue in existing.items():
|
||||
for title, issues in existing.items():
|
||||
# Extract package name from title
|
||||
m = re.match(r"\[pkg-missing\] (\w+)/(.+?)(?:\s|$)", title)
|
||||
if not m:
|
||||
@@ -186,40 +216,36 @@ def handle_drift(*, dry_run: bool = False) -> int:
|
||||
if pkg_name in sub_d.get("untracked", []):
|
||||
still_missing = True
|
||||
break
|
||||
if not still_missing:
|
||||
if dry_run:
|
||||
print(f" would close: {title}")
|
||||
else:
|
||||
_api(
|
||||
"PATCH",
|
||||
f"repos/{REPO}/issues/{issue['number']}",
|
||||
{"state": "closed"},
|
||||
)
|
||||
print(f" closed #{issue['number']}: {title}")
|
||||
issues = sorted(issues, key=lambda i: i.get("number", 0))
|
||||
_close_all(title, issues[1:] if still_missing else issues, dry_run=dry_run)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
def handle_vulns(*, dry_run: bool = False) -> int:
|
||||
"""Create issues for vulnerabilities found in package scans."""
|
||||
if not VULN_PATH.exists():
|
||||
print(" vulns: no scan results found (data/pkg-vulns.json)")
|
||||
return 0
|
||||
"""Create issues for vulnerabilities found in package scans.
|
||||
|
||||
data = json.loads(VULN_PATH.read_text())
|
||||
Also closes issues whose CVE no longer appears — including when a scan
|
||||
comes back clean, which is exactly when the most issues resolve.
|
||||
"""
|
||||
reports = [p for p in VULN_PATHS if p.exists()]
|
||||
if not reports:
|
||||
print(" vulns: no scan results found (data/pkg-vulns*.json)")
|
||||
return 0
|
||||
|
||||
# trivy JSON format: {"Results": [{"Vulnerabilities": [...]}]}
|
||||
vulns: list[dict] = []
|
||||
for result in data.get("Results", []):
|
||||
for v in result.get("Vulnerabilities", []):
|
||||
vulns.append(v)
|
||||
for path in reports:
|
||||
data = json.loads(path.read_text())
|
||||
for result in data.get("Results", []):
|
||||
for v in result.get("Vulnerabilities") or []:
|
||||
vulns.append(v)
|
||||
|
||||
if not vulns:
|
||||
print(" vulns: no vulnerabilities found")
|
||||
return 0
|
||||
|
||||
milestone_id = _get_milestone_id()
|
||||
label_ids = _get_label_ids([LABEL_CI, LABEL_QUALITY])
|
||||
milestone_id = _get_milestone_id() if vulns else None
|
||||
label_ids = _get_label_ids([LABEL_CI, LABEL_QUALITY]) if vulns else []
|
||||
existing = _find_existing_issues("[pkg-vuln]")
|
||||
created = 0
|
||||
|
||||
@@ -245,6 +271,7 @@ def handle_vulns(*, dry_run: bool = False) -> int:
|
||||
|
||||
if dry_run:
|
||||
print(f" would create: {title}")
|
||||
existing[title] = []
|
||||
else:
|
||||
issue_data = {"title": title, "body": body, "labels": label_ids}
|
||||
if milestone_id:
|
||||
@@ -253,24 +280,23 @@ def handle_vulns(*, dry_run: bool = False) -> int:
|
||||
if result:
|
||||
print(f" created #{result['number']}: {title}")
|
||||
created += 1
|
||||
# the same CVE can surface twice (two lockfiles, two paths)
|
||||
existing[title] = [result]
|
||||
|
||||
# Close issues for CVEs that are no longer present
|
||||
# Close issues for CVEs that are no longer present, and duplicates of
|
||||
# ones that are (keep the oldest open).
|
||||
active_cves = {
|
||||
f"[pkg-vuln] {v.get('VulnerabilityID', '')} in "
|
||||
f"{v.get('PkgName', '')}@{v.get('InstalledVersion', '')}"
|
||||
for v in vulns
|
||||
}
|
||||
for title, issue in existing.items():
|
||||
if title not in active_cves:
|
||||
if dry_run:
|
||||
print(f" would close: {title}")
|
||||
else:
|
||||
_api(
|
||||
"PATCH",
|
||||
f"repos/{REPO}/issues/{issue['number']}",
|
||||
{"state": "closed"},
|
||||
)
|
||||
print(f" closed #{issue['number']}: {title}")
|
||||
for title, issues in existing.items():
|
||||
issues = sorted(issues, key=lambda i: i.get("number", 0))
|
||||
_close_all(
|
||||
title,
|
||||
issues[1:] if title in active_cves else issues,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
"serve": "docusaurus serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/preset-classic": "^3.7.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.55.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"@docusaurus/core": "^3.10.2",
|
||||
"@docusaurus/preset-classic": "^3.10.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.55.3",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
||||
"@docusaurus/types": "^3.7.0",
|
||||
"typescript": "~5.6.0"
|
||||
"@docusaurus/module-type-aliases": "^3.10.2",
|
||||
"@docusaurus/types": "^3.10.2",
|
||||
"typescript": "~5.6.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -40,5 +40,5 @@
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1"
|
||||
"packageManager": "pnpm@10.34.7"
|
||||
}
|
||||
|
||||
5572
docs/pnpm-lock.yaml
generated
5572
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
FROM ghcr.io/astral-sh/uv:0.13.2-python3.13-trixie-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -11,6 +11,9 @@ ARG PYPI_INDEX_URL=""
|
||||
# healthcheck timeout, so Docker marks the container unhealthy even
|
||||
# though /health responds in milliseconds.)
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
# uv manages the venv; the base image's system pip is never used, and its
|
||||
# vendored certifi/urllib3/msgpack/setuptools copies lag and get flagged.
|
||||
RUN python3 -m pip uninstall -y -q pip
|
||||
|
||||
# Copy project files for install
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
@@ -26,7 +29,9 @@ ENV UV_PYTHON_PREFERENCE=only-system \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PROJECT_ENVIRONMENT=.venv \
|
||||
UV_INDEX_URL=${PYPI_INDEX_URL}
|
||||
RUN uv sync --no-dev --extra cli --extra perf && uv pip install -e .
|
||||
# --locked: a stale uv.lock fails the build instead of silently re-resolving
|
||||
# to packages nobody scanned (pkg-supply-chain scans uv.lock).
|
||||
RUN uv sync --locked --no-dev --extra cli --extra perf && uv pip install -e .
|
||||
|
||||
# Config
|
||||
COPY stack.toml ./
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Extract docs from Python source + export bib library
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS extract
|
||||
FROM ghcr.io/astral-sh/uv:0.13.2-python3.13-trixie-slim AS extract
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN uv run --with griffe python docs/scripts/extract_docs.py
|
||||
# Stage 2: Build Docusaurus static site
|
||||
FROM node:22-slim AS build
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.12.1 --activate
|
||||
RUN corepack enable && corepack prepare pnpm@10.34.7 --activate
|
||||
|
||||
WORKDIR /docs
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||
FROM ghcr.io/astral-sh/uv:0.13.2-python3.13-trixie-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -8,6 +8,9 @@ ARG PYPI_INDEX_URL=""
|
||||
|
||||
# Patch base image CVEs + install curl for the healthcheck.
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
# uv manages the venv; the base image's system pip is never used, and its
|
||||
# vendored certifi/urllib3/msgpack/setuptools copies lag and get flagged.
|
||||
RUN python3 -m pip uninstall -y -q pip
|
||||
|
||||
# Word → PDF for the document viewer (llm.convert): headless Writer plus
|
||||
# metric-compatible fonts so Calibri/Cambria/Times/Arial letters paginate
|
||||
@@ -25,7 +28,9 @@ ENV UV_PYTHON_PREFERENCE=only-system \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PROJECT_ENVIRONMENT=.venv \
|
||||
UV_INDEX_URL=${PYPI_INDEX_URL}
|
||||
RUN uv sync --no-dev --extra llm --extra perf && uv pip install -e .
|
||||
# --locked: a stale uv.lock fails the build instead of silently re-resolving
|
||||
# to packages nobody scanned (pkg-supply-chain scans uv.lock).
|
||||
RUN uv sync --locked --no-dev --extra llm --extra perf && uv pip install -e .
|
||||
|
||||
# Config (conf reads stack.toml by walking up from CWD)
|
||||
COPY stack.toml ./
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# MinIO Client (mc) sidecar — least-privilege admin helper for RustFS.
|
||||
# The image moved off Docker Hub (docker.io/minio/mc now 404s — pull access
|
||||
# denied) to quay.io; pinned to a release tag so a base-image change is a
|
||||
# reviewed diff, not a silent CI break.
|
||||
# Object-store admin sidecar for RustFS — least-privilege IAM/bucket helper.
|
||||
# The service keeps its `mc` name, but the binary is RustFS's own `rc`
|
||||
# (github.com/rustfs/cli): MinIO archived the open-source mc — no security
|
||||
# updates, quay.io/minio/mc now 401s and dl.min.io answers 410 Gone. rc speaks
|
||||
# the same alias grammar (`RC_HOST_local=http://key:secret@host:port`) and
|
||||
# admin verbs: `docker exec mc rc admin policy create local/ <name> <file>`.
|
||||
# Pinned tags so a base-image change is a reviewed diff (Renovate bumps both).
|
||||
# Runs as non-root, read-only filesystem, no capabilities.
|
||||
FROM quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z AS mc
|
||||
FROM rustfs/rc:v0.1.36 AS rc
|
||||
|
||||
FROM alpine:3
|
||||
RUN addgroup -g 10002 -S mc && adduser -u 10002 -S mc -G mc
|
||||
COPY --from=mc /usr/bin/mc /usr/bin/mc
|
||||
FROM alpine:3.24
|
||||
RUN apk upgrade --no-cache \
|
||||
&& addgroup -g 10002 -S mc && adduser -u 10002 -S mc -G mc
|
||||
COPY --from=rc /usr/bin/rc /usr/bin/rc
|
||||
USER mc:mc
|
||||
ENTRYPOINT ["mc"]
|
||||
ENTRYPOINT ["rc"]
|
||||
|
||||
@@ -61,7 +61,7 @@ RUN rm -rf marimo/_static marimo/_lsp \
|
||||
|
||||
# ---- stage 2: wheel ---------------------------------------------------------
|
||||
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS wheel
|
||||
FROM ghcr.io/astral-sh/uv:0.13.2-python3.13-trixie-slim AS wheel
|
||||
WORKDIR /src
|
||||
COPY --from=fe /src/ /src/
|
||||
RUN uv build --wheel
|
||||
@@ -92,14 +92,19 @@ RUN groupmod -n ${USERNAME} ubuntu \
|
||||
&& usermod -l ${USERNAME} -d /home/${USERNAME} -m -s /bin/bash ubuntu \
|
||||
&& chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uvx /usr/local/bin/uvx
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.13.2 /uv /usr/local/bin/uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.13.2 /uvx /usr/local/bin/uvx
|
||||
|
||||
WORKDIR /home/${USERNAME}
|
||||
|
||||
# Bring in the fhirworx marimo wheel from the wheel stage.
|
||||
COPY --from=wheel /src/dist/ /tmp/marimo-wheel/
|
||||
|
||||
# No release cooldown here (unlike pyproject.toml's exclude-newer): this
|
||||
# workspace pulls cudf-polars-cu12 from pypi.nvidia.com, which publishes no
|
||||
# upload dates, so any exclude-newer filters out every release and the build
|
||||
# fails. uv has no per-package opt-out for date-less indexes.
|
||||
|
||||
# Install python + init workspace venv. Use the local wheel for marimo so
|
||||
# the fhirworx frontend ships in the image; no upstream PyPI fetch for
|
||||
# marimo itself.
|
||||
|
||||
@@ -6,7 +6,11 @@ readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"pydantic>=2.0.0",
|
||||
"pymupdf>=1.24", # AGPL-3.0 — flag if shipping outside internal use
|
||||
# AGPL-3.0 — flag if shipping outside internal use. <2: pymupdf 2.x folds
|
||||
# in the former pymupdf_layout code (Polyform Noncommercial / Artifex
|
||||
# commercial) and pulls onnxruntime into every image — a licence call,
|
||||
# not a routine bump.
|
||||
"pymupdf>=1.27.2,<2",
|
||||
"python-docx>=1.1",
|
||||
"xlrd>=2.0.2",
|
||||
]
|
||||
@@ -32,13 +36,13 @@ api = [
|
||||
"fastapi>=0.139.0",
|
||||
"uvicorn>=0.41.0",
|
||||
"httpx>=0.28.1",
|
||||
"pyjwt>=2.13.0",
|
||||
"pyjwt>=2.15.1",
|
||||
"cryptography>=50.0.0",
|
||||
]
|
||||
bcda = [
|
||||
"stack[conf]",
|
||||
"httpx>=0.28.1",
|
||||
"fsspec>=2024.1.0",
|
||||
"fsspec>=2026.6.0",
|
||||
"narwhals>=2.17.0",
|
||||
"pyarrow>=23.0.1",
|
||||
]
|
||||
@@ -110,7 +114,7 @@ rex = [
|
||||
"stack[conf]",
|
||||
"narwhals>=2.17.0",
|
||||
"pyarrow>=23.0.1",
|
||||
"fsspec>=2024.1.0",
|
||||
"fsspec>=2026.6.0",
|
||||
]
|
||||
prisma = [
|
||||
"stack[conf]",
|
||||
@@ -124,8 +128,8 @@ prisma = [
|
||||
]
|
||||
perf = [
|
||||
"stack[conf]",
|
||||
"opentelemetry-api>=1.25.0",
|
||||
"opentelemetry-sdk>=1.25.0",
|
||||
"opentelemetry-api>=1.45.1",
|
||||
"opentelemetry-sdk>=1.45.1",
|
||||
"opentelemetry-exporter-otlp-proto-grpc>=1.25.0",
|
||||
"opentelemetry-exporter-prometheus>=0.46b0",
|
||||
"opentelemetry-instrumentation-fastapi>=0.46b0",
|
||||
@@ -222,6 +226,18 @@ parallel = true
|
||||
source = ["src"]
|
||||
concurrency = ["multiprocessing", "thread"]
|
||||
|
||||
[tool.uv]
|
||||
# Supply-chain cooldown: resolve only releases at least a week old, so a
|
||||
# hijacked or yanked-within-days upload never lands in the lock. Renovate's
|
||||
# minimumReleaseAge matches. Exempt one package with
|
||||
# `exclude-newer-package` when a CVE fix can't wait. Lives here, not in a
|
||||
# uv.toml: images copy only pyproject.toml + uv.lock, and the lock records
|
||||
# the span, so `uv sync --locked` fails wherever the setting isn't visible.
|
||||
exclude-newer = "7 days"
|
||||
# Prevents broken .venv symlinks when uv runs under different users.
|
||||
link-mode = "copy"
|
||||
python-preference = "managed"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
|
||||
namespace = true
|
||||
|
||||
@@ -1,34 +1,63 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"platform": "gitea",
|
||||
"endpoint": "http://git:3000/api/v1",
|
||||
"gitAuthor": "Renovate Bot <renovate@fhirworx.io>",
|
||||
"automerge": false,
|
||||
"labels": ["dependencies"],
|
||||
"automerge": false,
|
||||
"minimumReleaseAge": "7 days",
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["dependencies", "security"]
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["before 6am on monday"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchUpdateTypes": ["minor", "patch", "digest", "lockFileMaintenance"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"description": "A Python minor (3.13 -> 3.15) is a runtime migration, not a patch: never automerge it.",
|
||||
"matchDepNames": ["python"],
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"description": "pymupdf 2.x bundles the Polyform-Noncommercial layout code and onnxruntime — a licence decision (see pyproject.toml), not a routine bump.",
|
||||
"matchPackageNames": ["pymupdf"],
|
||||
"allowedVersions": "<2"
|
||||
},
|
||||
{
|
||||
"description": "Generated-workflow tool pins: gen_config.py must re-emit .gitea/workflows after the bump, so a human merges these.",
|
||||
"matchFileNames": ["dev/scripts/backends/gitea.py"],
|
||||
"automerge": false,
|
||||
"prBodyNotes": ["Run `uv run python dev/scripts/gen_config.py` on this branch and commit the regenerated workflows before merging."]
|
||||
}
|
||||
],
|
||||
"pip_requirements": {
|
||||
"fileMatch": ["pyproject.toml"]
|
||||
},
|
||||
"docker-compose": {
|
||||
"fileMatch": ["compose.yml"]
|
||||
},
|
||||
"dockerfile": {
|
||||
"fileMatch": ["infra/images/*.Dockerfile"]
|
||||
"managerFilePatterns": ["/(^|/)[^/]+\\.Dockerfile$/"]
|
||||
},
|
||||
"github-actions": {
|
||||
"description": "Only the hand-written Renovate workflow; the rest of .gitea/workflows is generated by gen_config.py.",
|
||||
"managerFilePatterns": ["/^\\.gitea/workflows/renovate\\.yml$/"]
|
||||
},
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Bump `ARG *_VERSION=` pins annotated with a `# renovate:` comment (marimo, yq, otel-cli, …) that the dockerfile manager can't see.",
|
||||
"fileMatch": ["(^|/)[^/]+\\.Dockerfile$"],
|
||||
"managerFilePatterns": ["/(^|/)[^/]+\\.Dockerfile$/"],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)(?: extractVersion=(?<extractVersion>\\S+))?\\s+ARG \\w+?_VERSION=(?<currentValue>\\S+)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Bump the trivy/crane pins the CI workflow generator installs.",
|
||||
"managerFilePatterns": ["/^dev/scripts/backends/gitea\\.py$/"],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)\\s+\\w+_VERSION = \"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"extractVersionTemplate": "^v?(?<version>.*)$"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,14 +15,16 @@ def int_brand_with_generic_available(
|
||||
Operations: filter, select, unique
|
||||
Dependencies: pharmacy.rxnorm_generic_available
|
||||
"""
|
||||
today = date.today().isoformat()
|
||||
# The table declares a date, but seeds can arrive as ISO strings; Polars 2
|
||||
# no longer casts String -> Date, so parse explicitly.
|
||||
start = nw.col("product_startmarketingdate")
|
||||
schema = pharmacy__rxnorm_generic_available.collect_schema()
|
||||
if schema["product_startmarketingdate"] == nw.String:
|
||||
start = start.str.to_date("%Y-%m-%d")
|
||||
return (
|
||||
pharmacy__rxnorm_generic_available.filter(
|
||||
(nw.col("ndc_product_tty").is_in(["SCD", "GPCK"]))
|
||||
& (
|
||||
nw.col("product_startmarketingdate").cast(nw.Date)
|
||||
<= nw.lit(today).cast(nw.Date)
|
||||
)
|
||||
& (start <= nw.lit(date.today(), nw.Date))
|
||||
)
|
||||
.select(
|
||||
nw.col("product_rxcui").alias("brand_with_generic_available"),
|
||||
|
||||
@@ -235,7 +235,7 @@ df = ctx.load("core.encounter")
|
||||
|
||||
# Columns are automatically renamed from Unity Catalog names
|
||||
assert "encounter_id" in df.columns # Mapped from encntr_id
|
||||
assert "person_id" in df.columns # Mapped from mbr_id
|
||||
assert "person_id" in df.columns # Mapped from mbr_id
|
||||
```
|
||||
|
||||
## Workspace Configuration
|
||||
@@ -310,6 +310,7 @@ encounters = ctx.load("core.encounter")
|
||||
|
||||
# Use in pipeline
|
||||
from aco.pipe import readmissions
|
||||
|
||||
results = readmissions.pipeline.run(ctx.load)
|
||||
```
|
||||
|
||||
@@ -474,11 +475,13 @@ Error: User does not have CREATE permission on catalog 'aco'
|
||||
```python
|
||||
# 1. Develop locally with DuckDB
|
||||
from aco.lake import DuckDBContext
|
||||
|
||||
ctx_local = DuckDBContext(database="notebooks/aco.duckdb")
|
||||
results = execute(readmissions.pipeline, ctx_local)
|
||||
|
||||
# 2. Deploy to Unity Catalog
|
||||
from aco.lake import IcebergContext
|
||||
|
||||
ctx_unity = IcebergContext(
|
||||
catalog_uri="https://dbc-7474655000864661.cloud.databricks.com/api/2.1/unity-catalog/iceberg",
|
||||
warehouse="aco",
|
||||
|
||||
@@ -233,6 +233,27 @@ def compute_test_targets(
|
||||
return deduped, ", ".join(reasons) if reasons else "changed files"
|
||||
|
||||
|
||||
# Variables git exports to hooks that pin every nested git call to *this*
|
||||
# repository. Tests that build scratch repos in tmp dirs inherited them and
|
||||
# committed into the repo being committed to (and wrote its shared config).
|
||||
_HOOK_GIT_VARS = (
|
||||
"GIT_DIR",
|
||||
"GIT_INDEX_FILE",
|
||||
"GIT_WORK_TREE",
|
||||
"GIT_PREFIX",
|
||||
"GIT_COMMON_DIR",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
)
|
||||
|
||||
|
||||
def _test_env() -> dict[str, str]:
|
||||
"""The hook's environment minus git's repo-locating variables."""
|
||||
import os
|
||||
|
||||
return {k: v for k, v in os.environ.items() if k not in _HOOK_GIT_VARS}
|
||||
|
||||
|
||||
def run_step(label: str, cmd: list[str], **kwargs) -> int:
|
||||
"""Run a subprocess step, printing the label."""
|
||||
print(f"==> pre-commit: {label}")
|
||||
@@ -318,6 +339,7 @@ def main() -> int:
|
||||
["uv", "run", "python", "-m", "pytest"]
|
||||
+ targets
|
||||
+ ["--no-cov", "--tb=short", "-q", "-n", "auto"],
|
||||
env=_test_env(),
|
||||
)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
@@ -320,4 +320,3 @@ path_filter = ["infra/images/llm.Dockerfile", "src/**", "pyproject.toml"]
|
||||
dockerfile = "infra/images/mc.Dockerfile"
|
||||
context = "infra/rustfs/"
|
||||
path_filter = ["infra/rustfs/**", "infra/images/mc.Dockerfile"]
|
||||
scan = false
|
||||
|
||||
@@ -104,9 +104,16 @@ def test_image_jobs_gate_on_changes_outputs_not_head_commit():
|
||||
# ── Behavioural: run the detector script against real git history ──────
|
||||
|
||||
|
||||
def _clean_env() -> dict[str, str]:
|
||||
"""os.environ without GIT_* — under a pre-commit hook GIT_DIR and
|
||||
GIT_INDEX_FILE point at the real repo, and every scratch-repo command
|
||||
here would commit into it instead."""
|
||||
return {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
||||
|
||||
|
||||
def _git(*args: str, cwd: Path) -> str:
|
||||
env = {
|
||||
**os.environ,
|
||||
**_clean_env(),
|
||||
"GIT_AUTHOR_NAME": "t",
|
||||
"GIT_AUTHOR_EMAIL": "t@example.invalid",
|
||||
"GIT_COMMITTER_NAME": "t",
|
||||
@@ -170,7 +177,7 @@ def _run_detector(checkout: Path, before: str, sha: str, tmp_path: Path) -> dict
|
||||
["bash", "-e", "-c", step["run"]],
|
||||
cwd=checkout,
|
||||
env={
|
||||
**os.environ,
|
||||
**_clean_env(),
|
||||
"BEFORE": before,
|
||||
"GITHUB_SHA": sha,
|
||||
"GITHUB_OUTPUT": str(out_file),
|
||||
|
||||
@@ -9,6 +9,7 @@ from sem.hooks import (
|
||||
_changed_test_dirs,
|
||||
_classify,
|
||||
_staged_files,
|
||||
_test_env,
|
||||
_top_level_tests,
|
||||
check_syntax,
|
||||
compute_test_targets,
|
||||
@@ -195,6 +196,28 @@ class TestRunStep:
|
||||
assert rc == 1
|
||||
|
||||
|
||||
class TestTestEnv:
|
||||
def test_strips_repo_locating_git_vars(self, monkeypatch):
|
||||
monkeypatch.setenv("GIT_DIR", "/repo/.git")
|
||||
monkeypatch.setenv("GIT_INDEX_FILE", "/repo/.git/index")
|
||||
monkeypatch.setenv("GIT_AUTHOR_NAME", "kept")
|
||||
env = _test_env()
|
||||
assert "GIT_DIR" not in env
|
||||
assert "GIT_INDEX_FILE" not in env
|
||||
assert env["GIT_AUTHOR_NAME"] == "kept"
|
||||
|
||||
def test_nested_git_sees_its_own_repo(self, tmp_path, monkeypatch):
|
||||
"""Under a hook, `git init` in a tmp dir must not land in GIT_DIR."""
|
||||
import subprocess
|
||||
|
||||
monkeypatch.setenv("GIT_DIR", str(tmp_path / "hooked.git"))
|
||||
scratch = tmp_path / "scratch"
|
||||
scratch.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=scratch, env=_test_env(), check=True)
|
||||
assert (scratch / ".git").is_dir()
|
||||
assert not (tmp_path / "hooked.git").exists()
|
||||
|
||||
|
||||
class TestMain:
|
||||
@patch("sem.hooks._staged_files", return_value=[])
|
||||
def test_no_staged_files_returns_zero(self, mock_staged):
|
||||
|
||||
@@ -45,9 +45,10 @@ class TestBindMountSourcesExist:
|
||||
|
||||
missing = []
|
||||
for svc, host_path, full in mounts:
|
||||
# ./data/ is gitignored runtime state — populated by service
|
||||
# bootstrap on first run, not present in a fresh checkout (CI).
|
||||
if host_path.startswith("./data/"):
|
||||
# ./data/ and ./.state/ are gitignored runtime state — populated
|
||||
# by service bootstrap on first run, not present in a fresh
|
||||
# checkout (CI).
|
||||
if host_path.startswith(("./data/", "./.state/")):
|
||||
continue
|
||||
resolved = ROOT / host_path
|
||||
if not resolved.exists():
|
||||
|
||||
@@ -1470,7 +1470,7 @@ class TestSemHooksMainEntry:
|
||||
):
|
||||
call_count = [0]
|
||||
|
||||
def step_side_effect(label, cmd):
|
||||
def step_side_effect(label, cmd, **_kw):
|
||||
call_count[0] += 1
|
||||
if "notebook run" in label:
|
||||
return 1 # line 348
|
||||
|
||||
@@ -100,7 +100,7 @@ class TestSemHooksFormatCheckFails:
|
||||
|
||||
call_count = [0]
|
||||
|
||||
def step_side_effect(label, cmd):
|
||||
def step_side_effect(label, cmd, **_kw):
|
||||
call_count[0] += 1
|
||||
if "format" in label:
|
||||
return 1
|
||||
@@ -144,7 +144,7 @@ class TestSemHooksPytestFails:
|
||||
|
||||
call_count = [0]
|
||||
|
||||
def step_side_effect(label, cmd):
|
||||
def step_side_effect(label, cmd, **_kw):
|
||||
call_count[0] += 1
|
||||
if "pytest" in label:
|
||||
return 1
|
||||
@@ -170,7 +170,7 @@ class TestSemHooksNotebookFail:
|
||||
|
||||
call_count = [0]
|
||||
|
||||
def step_side_effect(label, cmd):
|
||||
def step_side_effect(label, cmd, **_kw):
|
||||
call_count[0] += 1
|
||||
if "marimo" in label:
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user