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
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -80,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Scan notebooks
|
||||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o notebooks-scan.json local/notebooks:build
|
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
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -132,7 +144,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Scan zotero
|
||||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o zotero-scan.json local/zotero:build
|
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
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -184,7 +202,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Scan docs
|
||||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o docs-scan.json local/docs:build
|
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
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -236,7 +260,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Scan api
|
||||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o api-scan.json local/api:build
|
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
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -288,7 +318,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Scan llm
|
||||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o llm-scan.json local/llm:build
|
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
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -339,6 +375,12 @@ jobs:
|
|||||||
CRANE_INSECURE: "true"
|
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
|
- name: Compute short SHA
|
||||||
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
|
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | head -c 8)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ jobs:
|
|||||||
uses: https://github.com/actions/checkout@v4
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -24,7 +30,7 @@ jobs:
|
|||||||
CRANE_INSECURE: "true"
|
CRANE_INSECURE: "true"
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Set up uv
|
||||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
@@ -32,86 +38,127 @@ jobs:
|
|||||||
UV_INSTALL_DIR: /usr/local/bin
|
UV_INSTALL_DIR: /usr/local/bin
|
||||||
|
|
||||||
- name: Build notebooks
|
- 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
|
- name: Push notebooks
|
||||||
|
if: ${{ steps.build_notebooks.outcome == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
docker save local/notebooks:build -o /tmp/notebooks.tar
|
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:hardened --insecure
|
||||||
crane push /tmp/notebooks.tar git:3000/homelab/stack/notebooks:latest --insecure
|
crane push /tmp/notebooks.tar git:3000/homelab/stack/notebooks:latest --insecure
|
||||||
|
|
||||||
- name: Build zotero
|
- 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
|
- name: Push zotero
|
||||||
|
if: ${{ steps.build_zotero.outcome == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
docker save local/zotero:build -o /tmp/zotero.tar
|
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:hardened --insecure
|
||||||
crane push /tmp/zotero.tar git:3000/homelab/stack/zotero:latest --insecure
|
crane push /tmp/zotero.tar git:3000/homelab/stack/zotero:latest --insecure
|
||||||
|
|
||||||
- name: Build docs
|
- 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
|
- name: Push docs
|
||||||
|
if: ${{ steps.build_docs.outcome == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
docker save local/docs:build -o /tmp/docs.tar
|
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:hardened --insecure
|
||||||
crane push /tmp/docs.tar git:3000/homelab/stack/docs:latest --insecure
|
crane push /tmp/docs.tar git:3000/homelab/stack/docs:latest --insecure
|
||||||
|
|
||||||
- name: Build api
|
- 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
|
- name: Push api
|
||||||
|
if: ${{ steps.build_api.outcome == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
docker save local/api:build -o /tmp/api.tar
|
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:hardened --insecure
|
||||||
crane push /tmp/api.tar git:3000/homelab/stack/api:latest --insecure
|
crane push /tmp/api.tar git:3000/homelab/stack/api:latest --insecure
|
||||||
|
|
||||||
- name: Build llm
|
- 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
|
- name: Push llm
|
||||||
|
if: ${{ steps.build_llm.outcome == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
docker save local/llm:build -o /tmp/llm.tar
|
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:hardened --insecure
|
||||||
crane push /tmp/llm.tar git:3000/homelab/stack/llm:latest --insecure
|
crane push /tmp/llm.tar git:3000/homelab/stack/llm:latest --insecure
|
||||||
|
|
||||||
- name: Build mc
|
- 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
|
- name: Push mc
|
||||||
|
if: ${{ steps.build_mc.outcome == 'success' }}
|
||||||
run: |
|
run: |
|
||||||
docker save local/mc:build -o /tmp/mc.tar
|
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:hardened --insecure
|
||||||
crane push /tmp/mc.tar git:3000/homelab/stack/mc:latest --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
|
- name: Close resolved or file new vuln issues
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
uv sync --no-dev
|
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
|
if [ -f "$f" ]; then
|
||||||
uv run python -m api.diag.vuln --close "$f" || \
|
uv run python -m api.diag.vuln --close "$f" || \
|
||||||
uv run python -m api.diag.vuln "$f" || true
|
uv run python -m api.diag.vuln "$f" || true
|
||||||
fi
|
fi
|
||||||
done
|
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
|
- name: File failure issue
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -45,13 +45,15 @@ jobs:
|
|||||||
run: uv run python dev/scripts/pkg_drift.py
|
run: uv run python dev/scripts/pkg_drift.py
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Vulnerability scan
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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.json uv.lock
|
||||||
trivy fs --scanners vuln --format json -o data/pkg-vulns-pyproject.json pyproject.toml
|
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
|
- name: Auto-create issues for drift and vulns
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ jobs:
|
|||||||
uses: https://github.com/actions/checkout@v4
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
run: crane auth login git:3000 -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}"
|
||||||
@@ -22,7 +28,7 @@ jobs:
|
|||||||
CRANE_INSECURE: "true"
|
CRANE_INSECURE: "true"
|
||||||
|
|
||||||
- name: Install trivy
|
- 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
|
- name: Set up uv
|
||||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
@@ -101,12 +107,15 @@ jobs:
|
|||||||
- name: Scan llm
|
- name: Scan llm
|
||||||
run: trivy image --severity HIGH,CRITICAL --exit-code 0 --format json -o llm-scan.json local/llm:build
|
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
|
- name: Report vulnerabilities
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
uv sync --no-dev
|
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
|
[ -f "$f" ] && uv run python -m api.diag.vuln "$f" || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,21 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
run: renovate
|
run: renovate
|
||||||
env:
|
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_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@fhirworx.io>"
|
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@fhirworx.io>"
|
||||||
LOG_LEVEL: "info"
|
LOG_LEVEL: "info"
|
||||||
|
|||||||
@@ -121,11 +121,12 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- storage
|
- storage
|
||||||
environment:
|
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"
|
user: "10002:10002"
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /home/mc/.mc:uid=10002,gid=10002
|
- /home/mc/.config/rc:uid=10002,gid=10002
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
cap_drop:
|
cap_drop:
|
||||||
|
|||||||
@@ -238,7 +238,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pymupdf",
|
"name": "pymupdf",
|
||||||
"version": ">=1.24",
|
"version": ">=1.27.2,<2",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "duckdb",
|
"name": "duckdb",
|
||||||
"version": ">=1.0.0",
|
"version": ">=1.5.4",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "duckdb",
|
"name": "duckdb",
|
||||||
"version": ">=1.0.0",
|
"version": ">=1.5.4",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -303,7 +303,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pyjwt",
|
"name": "pyjwt",
|
||||||
"version": ">=2.13.0",
|
"version": ">=2.15.1",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fsspec",
|
"name": "fsspec",
|
||||||
"version": ">=2024.1.0",
|
"version": ">=2026.6.0",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,6 +386,11 @@
|
|||||||
"version": ">=2.0.0",
|
"version": ">=2.0.0",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pyyaml",
|
||||||
|
"version": ">=6.0.0",
|
||||||
|
"extras": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pydantic",
|
"name": "pydantic",
|
||||||
"version": ">=2.0.0",
|
"version": ">=2.0.0",
|
||||||
@@ -428,7 +433,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "duckdb",
|
"name": "duckdb",
|
||||||
"version": ">=1.0.0",
|
"version": ">=1.5.4",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -438,7 +443,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "duckdb",
|
"name": "duckdb",
|
||||||
"version": ">=1.0.0",
|
"version": ">=1.5.4",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -446,9 +451,14 @@
|
|||||||
"version": ">=2.17.0",
|
"version": ">=2.17.0",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pydantic",
|
||||||
|
"version": ">=2.0.0",
|
||||||
|
"extras": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "duckdb",
|
"name": "duckdb",
|
||||||
"version": ">=1.0.0",
|
"version": ">=1.5.4",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -473,7 +483,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fsspec",
|
"name": "fsspec",
|
||||||
"version": ">=2024.1.0",
|
"version": ">=2026.6.0",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -508,12 +518,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "opentelemetry-api",
|
"name": "opentelemetry-api",
|
||||||
"version": ">=1.25.0",
|
"version": ">=1.45.1",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "opentelemetry-sdk",
|
"name": "opentelemetry-sdk",
|
||||||
"version": ">=1.25.0",
|
"version": ">=1.45.1",
|
||||||
"extras": ""
|
"extras": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -599,47 +609,47 @@
|
|||||||
"npm": [
|
"npm": [
|
||||||
{
|
{
|
||||||
"name": "@docusaurus/core",
|
"name": "@docusaurus/core",
|
||||||
"version": "^3.7.0"
|
"version": "^3.10.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@docusaurus/preset-classic",
|
"name": "@docusaurus/preset-classic",
|
||||||
"version": "^3.7.0"
|
"version": "^3.10.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@easyops-cn/docusaurus-search-local",
|
"name": "@easyops-cn/docusaurus-search-local",
|
||||||
"version": "^0.55.1"
|
"version": "^0.55.3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@mdx-js/react",
|
"name": "@mdx-js/react",
|
||||||
"version": "^3.0.0"
|
"version": "^3.1.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "clsx",
|
"name": "clsx",
|
||||||
"version": "^2.0.0"
|
"version": "^2.1.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "prism-react-renderer",
|
"name": "prism-react-renderer",
|
||||||
"version": "^2.3.0"
|
"version": "^2.4.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "react",
|
"name": "react",
|
||||||
"version": "^18.0.0"
|
"version": "^18.3.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "react-dom",
|
"name": "react-dom",
|
||||||
"version": "^18.0.0"
|
"version": "^18.3.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@docusaurus/module-type-aliases",
|
"name": "@docusaurus/module-type-aliases",
|
||||||
"version": "^3.7.0"
|
"version": "^3.10.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "@docusaurus/types",
|
"name": "@docusaurus/types",
|
||||||
"version": "^3.7.0"
|
"version": "^3.10.2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "typescript",
|
"name": "typescript",
|
||||||
"version": "~5.6.0"
|
"version": "~5.6.3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ci_tools": [
|
"ci_tools": [
|
||||||
@@ -647,10 +657,6 @@
|
|||||||
"name": "uv",
|
"name": "uv",
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "crane",
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "trivy",
|
"name": "trivy",
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ from backends.github import (
|
|||||||
_HEADER,
|
_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 ───────────────────
|
# ── Override action refs with full GitHub URLs ───────────────────
|
||||||
# Gitea Actions resolves short refs (e.g. actions/checkout@v4) against
|
# Gitea Actions resolves short refs (e.g. actions/checkout@v4) against
|
||||||
# the local Gitea instance. Use full URLs to pull from GitHub.
|
# the local Gitea instance. Use full URLs to pull from GitHub.
|
||||||
@@ -57,7 +66,9 @@ def _build_push_step(
|
|||||||
name = img["name"]
|
name = img["name"]
|
||||||
tags = [t.strip() for t in tags_expr.split(",") if t.strip()]
|
tags = [t.strip() for t in tags_expr.split(",") if t.strip()]
|
||||||
local_tag = f"local/{name}:build"
|
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"""\
|
lines = f"""\
|
||||||
- name: Build {name}
|
- name: Build {name}
|
||||||
run: docker build{cache_flag} -f {img["dockerfile"]} -t {local_tag} {img["context"]}"""
|
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:
|
def _install_trivy_step() -> str:
|
||||||
"""Install trivy as a native binary — no Docker-in-Docker needed."""
|
"""Install a pinned trivy as a native binary — no Docker-in-Docker needed.
|
||||||
return """\
|
|
||||||
|
The installer from the release tag verifies the tarball's sha256 against
|
||||||
|
the release checksums.
|
||||||
|
"""
|
||||||
|
v = TRIVY_VERSION
|
||||||
|
return f"""\
|
||||||
- name: Install trivy
|
- 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(
|
def _trivy_step(
|
||||||
@@ -129,7 +145,13 @@ def _docker_login_step(registry: str) -> str:
|
|||||||
"""Install crane and authenticate — daemonless push tool."""
|
"""Install crane and authenticate — daemonless push tool."""
|
||||||
return f"""\
|
return f"""\
|
||||||
- name: Install crane
|
- 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
|
- name: Log in to registry
|
||||||
run: crane auth login {registry} -u "${{{{ secrets.REGISTRY_USER }}}}" -p "${{{{ secrets.REGISTRY_TOKEN }}}}"
|
run: crane auth login {registry} -u "${{{{ secrets.REGISTRY_USER }}}}" -p "${{{{ secrets.REGISTRY_TOKEN }}}}"
|
||||||
@@ -412,21 +434,46 @@ def _gen_harden(
|
|||||||
uv_version: str,
|
uv_version: str,
|
||||||
**_kw: object,
|
**_kw: object,
|
||||||
) -> tuple[str, str]:
|
) -> 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:
|
for img in images:
|
||||||
tags = (
|
name = img["name"]
|
||||||
f"{registry}/{owner_repo}/{img['name']}:hardened,"
|
step_id = f"build_{name.replace('-', '_')}"
|
||||||
f"{registry}/{owner_repo}/{img['name']}:latest"
|
built = f"steps.{step_id}.outcome == 'success'"
|
||||||
)
|
steps = [
|
||||||
build_steps.append(
|
f"""\
|
||||||
_build_push_step(img, tags, registry, owner_repo, no_cache=True)
|
- name: Build {name}
|
||||||
)
|
id: {step_id}
|
||||||
build_block = "\n\n".join(build_steps)
|
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 = []
|
gate_lines = "\n ".join(
|
||||||
for img in scans:
|
f'[ "${{{{ steps.build_{i["name"].replace("-", "_")}.outcome }}}}" = success ]'
|
||||||
scan_steps.append(_trivy_step(img, "hardened", registry, owner_repo))
|
f' || failed="$failed {i["name"]}"'
|
||||||
scan_block = "\n\n".join(scan_steps)
|
for i in images
|
||||||
|
)
|
||||||
|
|
||||||
vuln_files = " ".join(f"{i['name']}-scan.json" for i in scans)
|
vuln_files = " ".join(f"{i['name']}-scan.json" for i in scans)
|
||||||
|
|
||||||
@@ -451,9 +498,7 @@ jobs:
|
|||||||
|
|
||||||
{_setup_uv_step(uv_version)}
|
{_setup_uv_step(uv_version)}
|
||||||
|
|
||||||
{build_block}
|
{image_block}
|
||||||
|
|
||||||
{scan_block}
|
|
||||||
|
|
||||||
- name: Close resolved or file new vuln issues
|
- name: Close resolved or file new vuln issues
|
||||||
env:
|
env:
|
||||||
@@ -467,6 +512,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
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")}
|
{_failure_step("Harden", "build-scan-report")}
|
||||||
"""
|
"""
|
||||||
return (".gitea/workflows/harden.yml", content)
|
return (".gitea/workflows/harden.yml", content)
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
MANIFEST_PATH = ROOT / "data" / "pkg-manifest.json"
|
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_URL = os.environ.get("GITEA_URL", "http://localhost:3000")
|
||||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
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]
|
return [name_to_id[n] for n in names if n in name_to_id]
|
||||||
|
|
||||||
|
|
||||||
def _find_existing_issues(prefix: str) -> dict[str, dict]:
|
def _find_existing_issues(prefix: str) -> dict[str, list[dict]]:
|
||||||
"""Find open issues with titles starting with prefix."""
|
"""Find open issues with titles starting with prefix, grouped by title.
|
||||||
issues = _api(
|
|
||||||
"GET",
|
Walks every page: a single 50-issue page hid most of the tracker once
|
||||||
f"repos/{REPO}/issues?state=open&type=issues&limit=50&labels={LABEL_CI}",
|
CI-failure issues piled up, so known CVEs were re-filed nightly (174
|
||||||
)
|
issues for 91 titles) and resolved ones were never closed.
|
||||||
if not issues:
|
"""
|
||||||
return {}
|
found: dict[str, list[dict]] = {}
|
||||||
return {i["title"]: i for i in issues if i["title"].startswith(prefix)}
|
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:
|
def handle_drift(*, dry_run: bool = False) -> int:
|
||||||
@@ -172,7 +202,7 @@ def handle_drift(*, dry_run: bool = False) -> int:
|
|||||||
created += 1
|
created += 1
|
||||||
|
|
||||||
# Close issues for packages that are no longer missing
|
# 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
|
# Extract package name from title
|
||||||
m = re.match(r"\[pkg-missing\] (\w+)/(.+?)(?:\s|$)", title)
|
m = re.match(r"\[pkg-missing\] (\w+)/(.+?)(?:\s|$)", title)
|
||||||
if not m:
|
if not m:
|
||||||
@@ -186,40 +216,36 @@ def handle_drift(*, dry_run: bool = False) -> int:
|
|||||||
if pkg_name in sub_d.get("untracked", []):
|
if pkg_name in sub_d.get("untracked", []):
|
||||||
still_missing = True
|
still_missing = True
|
||||||
break
|
break
|
||||||
if not still_missing:
|
issues = sorted(issues, key=lambda i: i.get("number", 0))
|
||||||
if dry_run:
|
_close_all(title, issues[1:] if still_missing else issues, dry_run=dry_run)
|
||||||
print(f" would close: {title}")
|
|
||||||
else:
|
|
||||||
_api(
|
|
||||||
"PATCH",
|
|
||||||
f"repos/{REPO}/issues/{issue['number']}",
|
|
||||||
{"state": "closed"},
|
|
||||||
)
|
|
||||||
print(f" closed #{issue['number']}: {title}")
|
|
||||||
|
|
||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
def handle_vulns(*, dry_run: bool = False) -> int:
|
def handle_vulns(*, dry_run: bool = False) -> int:
|
||||||
"""Create issues for vulnerabilities found in package scans."""
|
"""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
|
|
||||||
|
|
||||||
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": [...]}]}
|
# trivy JSON format: {"Results": [{"Vulnerabilities": [...]}]}
|
||||||
vulns: list[dict] = []
|
vulns: list[dict] = []
|
||||||
for result in data.get("Results", []):
|
for path in reports:
|
||||||
for v in result.get("Vulnerabilities", []):
|
data = json.loads(path.read_text())
|
||||||
vulns.append(v)
|
for result in data.get("Results", []):
|
||||||
|
for v in result.get("Vulnerabilities") or []:
|
||||||
|
vulns.append(v)
|
||||||
|
|
||||||
if not vulns:
|
if not vulns:
|
||||||
print(" vulns: no vulnerabilities found")
|
print(" vulns: no vulnerabilities found")
|
||||||
return 0
|
|
||||||
|
|
||||||
milestone_id = _get_milestone_id()
|
milestone_id = _get_milestone_id() if vulns else None
|
||||||
label_ids = _get_label_ids([LABEL_CI, LABEL_QUALITY])
|
label_ids = _get_label_ids([LABEL_CI, LABEL_QUALITY]) if vulns else []
|
||||||
existing = _find_existing_issues("[pkg-vuln]")
|
existing = _find_existing_issues("[pkg-vuln]")
|
||||||
created = 0
|
created = 0
|
||||||
|
|
||||||
@@ -245,6 +271,7 @@ def handle_vulns(*, dry_run: bool = False) -> int:
|
|||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print(f" would create: {title}")
|
print(f" would create: {title}")
|
||||||
|
existing[title] = []
|
||||||
else:
|
else:
|
||||||
issue_data = {"title": title, "body": body, "labels": label_ids}
|
issue_data = {"title": title, "body": body, "labels": label_ids}
|
||||||
if milestone_id:
|
if milestone_id:
|
||||||
@@ -253,24 +280,23 @@ def handle_vulns(*, dry_run: bool = False) -> int:
|
|||||||
if result:
|
if result:
|
||||||
print(f" created #{result['number']}: {title}")
|
print(f" created #{result['number']}: {title}")
|
||||||
created += 1
|
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 = {
|
active_cves = {
|
||||||
f"[pkg-vuln] {v.get('VulnerabilityID', '')} in "
|
f"[pkg-vuln] {v.get('VulnerabilityID', '')} in "
|
||||||
f"{v.get('PkgName', '')}@{v.get('InstalledVersion', '')}"
|
f"{v.get('PkgName', '')}@{v.get('InstalledVersion', '')}"
|
||||||
for v in vulns
|
for v in vulns
|
||||||
}
|
}
|
||||||
for title, issue in existing.items():
|
for title, issues in existing.items():
|
||||||
if title not in active_cves:
|
issues = sorted(issues, key=lambda i: i.get("number", 0))
|
||||||
if dry_run:
|
_close_all(
|
||||||
print(f" would close: {title}")
|
title,
|
||||||
else:
|
issues[1:] if title in active_cves else issues,
|
||||||
_api(
|
dry_run=dry_run,
|
||||||
"PATCH",
|
)
|
||||||
f"repos/{REPO}/issues/{issue['number']}",
|
|
||||||
{"state": "closed"},
|
|
||||||
)
|
|
||||||
print(f" closed #{issue['number']}: {title}")
|
|
||||||
|
|
||||||
return created
|
return created
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,19 @@
|
|||||||
"serve": "docusaurus serve"
|
"serve": "docusaurus serve"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^3.7.0",
|
"@docusaurus/core": "^3.10.2",
|
||||||
"@docusaurus/preset-classic": "^3.7.0",
|
"@docusaurus/preset-classic": "^3.10.2",
|
||||||
"@easyops-cn/docusaurus-search-local": "^0.55.1",
|
"@easyops-cn/docusaurus-search-local": "^0.55.3",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.1.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.1.1",
|
||||||
"prism-react-renderer": "^2.3.0",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"react": "^18.0.0",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
"@docusaurus/module-type-aliases": "^3.10.2",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.10.2",
|
||||||
"typescript": "~5.6.0"
|
"typescript": "~5.6.3"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -40,5 +40,5 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0"
|
"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
|
# 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
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -11,6 +11,9 @@ ARG PYPI_INDEX_URL=""
|
|||||||
# healthcheck timeout, so Docker marks the container unhealthy even
|
# healthcheck timeout, so Docker marks the container unhealthy even
|
||||||
# though /health responds in milliseconds.)
|
# 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/*
|
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 project files for install
|
||||||
COPY pyproject.toml uv.lock README.md ./
|
COPY pyproject.toml uv.lock README.md ./
|
||||||
@@ -26,7 +29,9 @@ ENV UV_PYTHON_PREFERENCE=only-system \
|
|||||||
UV_LINK_MODE=copy \
|
UV_LINK_MODE=copy \
|
||||||
UV_PROJECT_ENVIRONMENT=.venv \
|
UV_PROJECT_ENVIRONMENT=.venv \
|
||||||
UV_INDEX_URL=${PYPI_INDEX_URL}
|
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
|
# Config
|
||||||
COPY stack.toml ./
|
COPY stack.toml ./
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Extract docs from Python source + export bib library
|
# 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
|
WORKDIR /build
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ RUN uv run --with griffe python docs/scripts/extract_docs.py
|
|||||||
# Stage 2: Build Docusaurus static site
|
# Stage 2: Build Docusaurus static site
|
||||||
FROM node:22-slim AS build
|
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
|
WORKDIR /docs
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# 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
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -8,6 +8,9 @@ ARG PYPI_INDEX_URL=""
|
|||||||
|
|
||||||
# Patch base image CVEs + install curl for the healthcheck.
|
# 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/*
|
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
|
# Word → PDF for the document viewer (llm.convert): headless Writer plus
|
||||||
# metric-compatible fonts so Calibri/Cambria/Times/Arial letters paginate
|
# 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_LINK_MODE=copy \
|
||||||
UV_PROJECT_ENVIRONMENT=.venv \
|
UV_PROJECT_ENVIRONMENT=.venv \
|
||||||
UV_INDEX_URL=${PYPI_INDEX_URL}
|
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)
|
# Config (conf reads stack.toml by walking up from CWD)
|
||||||
COPY stack.toml ./
|
COPY stack.toml ./
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# MinIO Client (mc) sidecar — least-privilege admin helper for RustFS.
|
# Object-store admin sidecar for RustFS — least-privilege IAM/bucket helper.
|
||||||
# The image moved off Docker Hub (docker.io/minio/mc now 404s — pull access
|
# The service keeps its `mc` name, but the binary is RustFS's own `rc`
|
||||||
# denied) to quay.io; pinned to a release tag so a base-image change is a
|
# (github.com/rustfs/cli): MinIO archived the open-source mc — no security
|
||||||
# reviewed diff, not a silent CI break.
|
# 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.
|
# 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
|
FROM alpine:3.24
|
||||||
RUN addgroup -g 10002 -S mc && adduser -u 10002 -S mc -G mc
|
RUN apk upgrade --no-cache \
|
||||||
COPY --from=mc /usr/bin/mc /usr/bin/mc
|
&& addgroup -g 10002 -S mc && adduser -u 10002 -S mc -G mc
|
||||||
|
COPY --from=rc /usr/bin/rc /usr/bin/rc
|
||||||
USER mc:mc
|
USER mc:mc
|
||||||
ENTRYPOINT ["mc"]
|
ENTRYPOINT ["rc"]
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ RUN rm -rf marimo/_static marimo/_lsp \
|
|||||||
|
|
||||||
# ---- stage 2: wheel ---------------------------------------------------------
|
# ---- 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
|
WORKDIR /src
|
||||||
COPY --from=fe /src/ /src/
|
COPY --from=fe /src/ /src/
|
||||||
RUN uv build --wheel
|
RUN uv build --wheel
|
||||||
@@ -92,14 +92,19 @@ RUN groupmod -n ${USERNAME} ubuntu \
|
|||||||
&& usermod -l ${USERNAME} -d /home/${USERNAME} -m -s /bin/bash ubuntu \
|
&& usermod -l ${USERNAME} -d /home/${USERNAME} -m -s /bin/bash ubuntu \
|
||||||
&& chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}
|
&& 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:0.13.2 /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 /uvx /usr/local/bin/uvx
|
||||||
|
|
||||||
WORKDIR /home/${USERNAME}
|
WORKDIR /home/${USERNAME}
|
||||||
|
|
||||||
# Bring in the fhirworx marimo wheel from the wheel stage.
|
# Bring in the fhirworx marimo wheel from the wheel stage.
|
||||||
COPY --from=wheel /src/dist/ /tmp/marimo-wheel/
|
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
|
# Install python + init workspace venv. Use the local wheel for marimo so
|
||||||
# the fhirworx frontend ships in the image; no upstream PyPI fetch for
|
# the fhirworx frontend ships in the image; no upstream PyPI fetch for
|
||||||
# marimo itself.
|
# marimo itself.
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic>=2.0.0",
|
"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",
|
"python-docx>=1.1",
|
||||||
"xlrd>=2.0.2",
|
"xlrd>=2.0.2",
|
||||||
]
|
]
|
||||||
@@ -32,13 +36,13 @@ api = [
|
|||||||
"fastapi>=0.139.0",
|
"fastapi>=0.139.0",
|
||||||
"uvicorn>=0.41.0",
|
"uvicorn>=0.41.0",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"pyjwt>=2.13.0",
|
"pyjwt>=2.15.1",
|
||||||
"cryptography>=50.0.0",
|
"cryptography>=50.0.0",
|
||||||
]
|
]
|
||||||
bcda = [
|
bcda = [
|
||||||
"stack[conf]",
|
"stack[conf]",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"fsspec>=2024.1.0",
|
"fsspec>=2026.6.0",
|
||||||
"narwhals>=2.17.0",
|
"narwhals>=2.17.0",
|
||||||
"pyarrow>=23.0.1",
|
"pyarrow>=23.0.1",
|
||||||
]
|
]
|
||||||
@@ -110,7 +114,7 @@ rex = [
|
|||||||
"stack[conf]",
|
"stack[conf]",
|
||||||
"narwhals>=2.17.0",
|
"narwhals>=2.17.0",
|
||||||
"pyarrow>=23.0.1",
|
"pyarrow>=23.0.1",
|
||||||
"fsspec>=2024.1.0",
|
"fsspec>=2026.6.0",
|
||||||
]
|
]
|
||||||
prisma = [
|
prisma = [
|
||||||
"stack[conf]",
|
"stack[conf]",
|
||||||
@@ -124,8 +128,8 @@ prisma = [
|
|||||||
]
|
]
|
||||||
perf = [
|
perf = [
|
||||||
"stack[conf]",
|
"stack[conf]",
|
||||||
"opentelemetry-api>=1.25.0",
|
"opentelemetry-api>=1.45.1",
|
||||||
"opentelemetry-sdk>=1.25.0",
|
"opentelemetry-sdk>=1.45.1",
|
||||||
"opentelemetry-exporter-otlp-proto-grpc>=1.25.0",
|
"opentelemetry-exporter-otlp-proto-grpc>=1.25.0",
|
||||||
"opentelemetry-exporter-prometheus>=0.46b0",
|
"opentelemetry-exporter-prometheus>=0.46b0",
|
||||||
"opentelemetry-instrumentation-fastapi>=0.46b0",
|
"opentelemetry-instrumentation-fastapi>=0.46b0",
|
||||||
@@ -222,6 +226,18 @@ parallel = true
|
|||||||
source = ["src"]
|
source = ["src"]
|
||||||
concurrency = ["multiprocessing", "thread"]
|
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]
|
[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"]
|
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
|
||||||
namespace = true
|
namespace = true
|
||||||
|
|||||||
@@ -1,34 +1,63 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended"],
|
"extends": ["config:recommended"],
|
||||||
"platform": "gitea",
|
|
||||||
"endpoint": "http://git:3000/api/v1",
|
|
||||||
"gitAuthor": "Renovate Bot <renovate@fhirworx.io>",
|
|
||||||
"automerge": false,
|
|
||||||
"labels": ["dependencies"],
|
"labels": ["dependencies"],
|
||||||
|
"automerge": false,
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
|
"osvVulnerabilityAlerts": true,
|
||||||
|
"vulnerabilityAlerts": {
|
||||||
|
"labels": ["dependencies", "security"]
|
||||||
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"schedule": ["before 6am on monday"]
|
||||||
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch", "digest", "lockFileMaintenance"],
|
||||||
"automerge": true
|
"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": {
|
"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": [
|
"customManagers": [
|
||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"description": "Bump `ARG *_VERSION=` pins annotated with a `# renovate:` comment (marimo, yq, otel-cli, …) that the dockerfile manager can't see.",
|
"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": [
|
"matchStrings": [
|
||||||
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)(?: extractVersion=(?<extractVersion>\\S+))?\\s+ARG \\w+?_VERSION=(?<currentValue>\\S+)"
|
"# 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
|
Operations: filter, select, unique
|
||||||
Dependencies: pharmacy.rxnorm_generic_available
|
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 (
|
return (
|
||||||
pharmacy__rxnorm_generic_available.filter(
|
pharmacy__rxnorm_generic_available.filter(
|
||||||
(nw.col("ndc_product_tty").is_in(["SCD", "GPCK"]))
|
(nw.col("ndc_product_tty").is_in(["SCD", "GPCK"]))
|
||||||
& (
|
& (start <= nw.lit(date.today(), nw.Date))
|
||||||
nw.col("product_startmarketingdate").cast(nw.Date)
|
|
||||||
<= nw.lit(today).cast(nw.Date)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.select(
|
.select(
|
||||||
nw.col("product_rxcui").alias("brand_with_generic_available"),
|
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
|
# Columns are automatically renamed from Unity Catalog names
|
||||||
assert "encounter_id" in df.columns # Mapped from encntr_id
|
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
|
## Workspace Configuration
|
||||||
@@ -310,6 +310,7 @@ encounters = ctx.load("core.encounter")
|
|||||||
|
|
||||||
# Use in pipeline
|
# Use in pipeline
|
||||||
from aco.pipe import readmissions
|
from aco.pipe import readmissions
|
||||||
|
|
||||||
results = readmissions.pipeline.run(ctx.load)
|
results = readmissions.pipeline.run(ctx.load)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -474,11 +475,13 @@ Error: User does not have CREATE permission on catalog 'aco'
|
|||||||
```python
|
```python
|
||||||
# 1. Develop locally with DuckDB
|
# 1. Develop locally with DuckDB
|
||||||
from aco.lake import DuckDBContext
|
from aco.lake import DuckDBContext
|
||||||
|
|
||||||
ctx_local = DuckDBContext(database="notebooks/aco.duckdb")
|
ctx_local = DuckDBContext(database="notebooks/aco.duckdb")
|
||||||
results = execute(readmissions.pipeline, ctx_local)
|
results = execute(readmissions.pipeline, ctx_local)
|
||||||
|
|
||||||
# 2. Deploy to Unity Catalog
|
# 2. Deploy to Unity Catalog
|
||||||
from aco.lake import IcebergContext
|
from aco.lake import IcebergContext
|
||||||
|
|
||||||
ctx_unity = IcebergContext(
|
ctx_unity = IcebergContext(
|
||||||
catalog_uri="https://dbc-7474655000864661.cloud.databricks.com/api/2.1/unity-catalog/iceberg",
|
catalog_uri="https://dbc-7474655000864661.cloud.databricks.com/api/2.1/unity-catalog/iceberg",
|
||||||
warehouse="aco",
|
warehouse="aco",
|
||||||
|
|||||||
@@ -233,6 +233,27 @@ def compute_test_targets(
|
|||||||
return deduped, ", ".join(reasons) if reasons else "changed files"
|
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:
|
def run_step(label: str, cmd: list[str], **kwargs) -> int:
|
||||||
"""Run a subprocess step, printing the label."""
|
"""Run a subprocess step, printing the label."""
|
||||||
print(f"==> pre-commit: {label}")
|
print(f"==> pre-commit: {label}")
|
||||||
@@ -318,6 +339,7 @@ def main() -> int:
|
|||||||
["uv", "run", "python", "-m", "pytest"]
|
["uv", "run", "python", "-m", "pytest"]
|
||||||
+ targets
|
+ targets
|
||||||
+ ["--no-cov", "--tb=short", "-q", "-n", "auto"],
|
+ ["--no-cov", "--tb=short", "-q", "-n", "auto"],
|
||||||
|
env=_test_env(),
|
||||||
)
|
)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
return rc
|
return rc
|
||||||
|
|||||||
@@ -320,4 +320,3 @@ path_filter = ["infra/images/llm.Dockerfile", "src/**", "pyproject.toml"]
|
|||||||
dockerfile = "infra/images/mc.Dockerfile"
|
dockerfile = "infra/images/mc.Dockerfile"
|
||||||
context = "infra/rustfs/"
|
context = "infra/rustfs/"
|
||||||
path_filter = ["infra/rustfs/**", "infra/images/mc.Dockerfile"]
|
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 ──────
|
# ── 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:
|
def _git(*args: str, cwd: Path) -> str:
|
||||||
env = {
|
env = {
|
||||||
**os.environ,
|
**_clean_env(),
|
||||||
"GIT_AUTHOR_NAME": "t",
|
"GIT_AUTHOR_NAME": "t",
|
||||||
"GIT_AUTHOR_EMAIL": "t@example.invalid",
|
"GIT_AUTHOR_EMAIL": "t@example.invalid",
|
||||||
"GIT_COMMITTER_NAME": "t",
|
"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"]],
|
["bash", "-e", "-c", step["run"]],
|
||||||
cwd=checkout,
|
cwd=checkout,
|
||||||
env={
|
env={
|
||||||
**os.environ,
|
**_clean_env(),
|
||||||
"BEFORE": before,
|
"BEFORE": before,
|
||||||
"GITHUB_SHA": sha,
|
"GITHUB_SHA": sha,
|
||||||
"GITHUB_OUTPUT": str(out_file),
|
"GITHUB_OUTPUT": str(out_file),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from sem.hooks import (
|
|||||||
_changed_test_dirs,
|
_changed_test_dirs,
|
||||||
_classify,
|
_classify,
|
||||||
_staged_files,
|
_staged_files,
|
||||||
|
_test_env,
|
||||||
_top_level_tests,
|
_top_level_tests,
|
||||||
check_syntax,
|
check_syntax,
|
||||||
compute_test_targets,
|
compute_test_targets,
|
||||||
@@ -195,6 +196,28 @@ class TestRunStep:
|
|||||||
assert rc == 1
|
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:
|
class TestMain:
|
||||||
@patch("sem.hooks._staged_files", return_value=[])
|
@patch("sem.hooks._staged_files", return_value=[])
|
||||||
def test_no_staged_files_returns_zero(self, mock_staged):
|
def test_no_staged_files_returns_zero(self, mock_staged):
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ class TestBindMountSourcesExist:
|
|||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
for svc, host_path, full in mounts:
|
for svc, host_path, full in mounts:
|
||||||
# ./data/ is gitignored runtime state — populated by service
|
# ./data/ and ./.state/ are gitignored runtime state — populated
|
||||||
# bootstrap on first run, not present in a fresh checkout (CI).
|
# by service bootstrap on first run, not present in a fresh
|
||||||
if host_path.startswith("./data/"):
|
# checkout (CI).
|
||||||
|
if host_path.startswith(("./data/", "./.state/")):
|
||||||
continue
|
continue
|
||||||
resolved = ROOT / host_path
|
resolved = ROOT / host_path
|
||||||
if not resolved.exists():
|
if not resolved.exists():
|
||||||
|
|||||||
@@ -1470,7 +1470,7 @@ class TestSemHooksMainEntry:
|
|||||||
):
|
):
|
||||||
call_count = [0]
|
call_count = [0]
|
||||||
|
|
||||||
def step_side_effect(label, cmd):
|
def step_side_effect(label, cmd, **_kw):
|
||||||
call_count[0] += 1
|
call_count[0] += 1
|
||||||
if "notebook run" in label:
|
if "notebook run" in label:
|
||||||
return 1 # line 348
|
return 1 # line 348
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class TestSemHooksFormatCheckFails:
|
|||||||
|
|
||||||
call_count = [0]
|
call_count = [0]
|
||||||
|
|
||||||
def step_side_effect(label, cmd):
|
def step_side_effect(label, cmd, **_kw):
|
||||||
call_count[0] += 1
|
call_count[0] += 1
|
||||||
if "format" in label:
|
if "format" in label:
|
||||||
return 1
|
return 1
|
||||||
@@ -144,7 +144,7 @@ class TestSemHooksPytestFails:
|
|||||||
|
|
||||||
call_count = [0]
|
call_count = [0]
|
||||||
|
|
||||||
def step_side_effect(label, cmd):
|
def step_side_effect(label, cmd, **_kw):
|
||||||
call_count[0] += 1
|
call_count[0] += 1
|
||||||
if "pytest" in label:
|
if "pytest" in label:
|
||||||
return 1
|
return 1
|
||||||
@@ -170,7 +170,7 @@ class TestSemHooksNotebookFail:
|
|||||||
|
|
||||||
call_count = [0]
|
call_count = [0]
|
||||||
|
|
||||||
def step_side_effect(label, cmd):
|
def step_side_effect(label, cmd, **_kw):
|
||||||
call_count[0] += 1
|
call_count[0] += 1
|
||||||
if "marimo" in label:
|
if "marimo" in label:
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user