Repository navigation
chore(deps): lock file maintenance #671
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| site: | |
| name: Check Site | |
| # Every other job here is GitHub-hosted, and the self-hosted scale sets are | |
| # not available to this repository: pinning this one to them left it queued | |
| # forever, so the gate never ran at all. The prerender needs heap, not a | |
| # bigger machine, and NODE_OPTIONS below gives it that. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version: '24' | |
| cache: pnpm | |
| registry-url: https://npm.pkg.github.com | |
| scope: '@ferrlabs' | |
| - name: Install dependencies | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.FERRFLOW_PACKAGES_READ || secrets.GITHUB_TOKEN }} | |
| run: pnpm install --frozen-lockfile | |
| - name: Typecheck | |
| working-directory: site | |
| run: pnpm typecheck | |
| - name: Build | |
| working-directory: site | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=4096 | |
| run: pnpm build | |
| rust: | |
| name: Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Cache cargo | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo- | |
| - name: The image build sees every workspace member | |
| run: | | |
| set -euo pipefail | |
| members=$(grep '^members = ' Cargo.toml | sed 's/.*= *\[//; s/\].*//' | tr -d '" ' | tr ',' ' ') | |
| missing=0 | |
| for member in $members; do | |
| if ! grep -q "^COPY ${member} " Dockerfile; then | |
| echo "::error::${member} is a workspace member the Dockerfile never copies, so cargo cannot load the workspace" | |
| missing=1 | |
| fi | |
| done | |
| echo "checked: ${members}" | |
| exit "$missing" | |
| - run: cargo fmt --check | |
| - run: cargo clippy --all-targets -- -D warnings | |
| - run: cargo test --locked | |
| bucket: | |
| # The stub the unit tests drive agrees with whatever it is sent, so it | |
| # cannot catch what a real implementation refuses. It missed an upload with | |
| # no Content-Length, which every push to a real bucket answered with a 500. | |
| name: Objects in a bucket | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Cache cargo | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo- | |
| # Not a service container: MinIO needs a command, and services take an | |
| # image and an entrypoint override at best. | |
| - name: Start MinIO | |
| run: | | |
| set -euo pipefail | |
| docker run -d --name minio -p 9000:9000 -e MINIO_ROOT_USER=lfsxkey -e MINIO_ROOT_PASSWORD=lfsxsecret quay.io/minio/minio:latest server /data | |
| for _ in $(seq 60); do | |
| if curl -fsS http://127.0.0.1:9000/minio/health/live >/dev/null 2>&1; then | |
| docker exec minio mc alias set local http://127.0.0.1:9000 lfsxkey lfsxsecret | |
| docker exec minio mc mb --ignore-existing local/lfsx-test | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| docker logs minio | |
| echo "::error::MinIO never came up" | |
| exit 1 | |
| - run: cargo test --locked --test bucket | |
| env: | |
| LFSX_TEST_S3_ENDPOINT: http://127.0.0.1:9000 | |
| bucket-garage: | |
| # Same suite, second dialect. The startup probes ask the implementation | |
| # what it enforces instead of assuming, and MinIO alone cannot catch a | |
| # probe that trusts something MinIO happens to do. Garage is what the | |
| # homelab deployments this server courts actually run, and its answers | |
| # to conditional writes are the ones verify_locking has to classify. | |
| name: Objects in a Garage bucket | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Cache cargo | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo- | |
| # The version is pinned on purpose: the dialect is the thing under test, | |
| # so it must not drift underneath the suite on someone else's schedule. | |
| - name: Start Garage | |
| run: | | |
| set -euo pipefail | |
| cat > garage.toml <<EOF | |
| metadata_dir = "/var/lib/garage/meta" | |
| data_dir = "/var/lib/garage/data" | |
| db_engine = "sqlite" | |
| replication_factor = 1 | |
| rpc_bind_addr = "[::]:3901" | |
| rpc_public_addr = "127.0.0.1:3901" | |
| rpc_secret = "$(openssl rand -hex 32)" | |
| [s3_api] | |
| s3_region = "us-east-1" | |
| api_bind_addr = "[::]:3900" | |
| root_domain = ".s3.garage.localhost" | |
| EOF | |
| # Only the S3 port is mapped. The RPC port stays inside the | |
| # container on purpose: this is a single node talking to itself, and | |
| # every admin call below goes through docker exec. | |
| docker run -d --name garage -p 3900:3900 -v "$PWD/garage.toml:/etc/garage.toml" dxflrs/garage:v1.1.0 | |
| for _ in $(seq 60); do | |
| if docker exec garage /garage status >/dev/null 2>&1; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| docker exec garage /garage status >/dev/null || { docker logs garage; echo "::error::Garage never came up"; exit 1; } | |
| NODE=$(docker exec garage /garage node id -q | cut -d@ -f1) | |
| docker exec garage /garage layout assign -z ci -c 1G "$NODE" | |
| docker exec garage /garage layout apply --version 1 | |
| docker exec garage /garage key create ci-key > key.txt | |
| docker exec garage /garage bucket create lfsx-test | |
| docker exec garage /garage bucket allow --read --write lfsx-test --key ci-key | |
| { | |
| awk '/Key ID:/{print "LFSX_TEST_S3_ACCESS_KEY=" $3}' key.txt | |
| awk '/Secret key:/{print "LFSX_TEST_S3_SECRET_KEY=" $3}' key.txt | |
| } >> "$GITHUB_ENV" | |
| - run: cargo test --locked --test bucket | |
| env: | |
| LFSX_TEST_S3_ENDPOINT: http://127.0.0.1:3900 | |
| e2e: | |
| # The audience is a studio: artists on Windows, programmers on macOS, CI on | |
| # Linux, and whatever git-lfs their tooling happened to bundle. One job on | |
| # one runner proved none of that. | |
| name: git-lfs ${{ matrix.lfs && matrix.lfs || 'bundled' }} on ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| - os: windows-latest | |
| - os: macos-latest | |
| # The floor this project claims to support. Locking settled in 3.0, and | |
| # the clients that ship inside Git for Windows, Sourcetree or Unity are | |
| # rarely the newest. | |
| - os: ubuntu-latest | |
| lfs: v3.0.2 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Cache cargo | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-e2e-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo-e2e- | |
| - name: Pin git-lfs ${{ matrix.lfs }} | |
| if: matrix.lfs | |
| run: | | |
| set -euo pipefail | |
| version="${{ matrix.lfs }}" | |
| curl -fsSL "https://github.com/git-lfs/git-lfs/releases/download/${version}/git-lfs-linux-amd64-${version}.tar.gz" | tar xz -C "$RUNNER_TEMP" | |
| # 3.0 tarballs unpack flat; later ones nest under a directory. Take | |
| # whichever binary is there rather than guessing the shape. | |
| binary=$(find "$RUNNER_TEMP" -name git-lfs -type f | head -n1) | |
| chmod +x "$binary" | |
| mkdir -p "$RUNNER_TEMP/bin" | |
| mv "$binary" "$RUNNER_TEMP/bin/git-lfs" | |
| echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" | |
| - run: git lfs version | |
| - run: bash ci/e2e.sh | |
| helm: | |
| name: Helm chart | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 | |
| - run: helm lint chart --set config.publicUrl=https://lfs.example.com | |
| - name: An unpinned public URL leaves the variable out | |
| run: | | |
| set -euo pipefail | |
| helm template lfsx chart > unpinned.yaml | |
| if grep -q 'LFSX_PUBLIC_URL' unpinned.yaml; then | |
| echo "::error::LFSX_PUBLIC_URL was set with nothing to pin it to" | |
| exit 1 | |
| fi | |
| echo "no public URL pinned, the server answers on the requested host" | |
| - name: Render the shapes people actually deploy | |
| run: | | |
| set -euo pipefail | |
| helm template lfsx chart --set config.publicUrl=https://lfs.example.com >/dev/null | |
| helm template lfsx chart --set ingress.enabled=true --set ingress.className=nginx --set ingress.host=lfs.example.com > nginx.yaml | |
| grep -q 'proxy-body-size: "0"' nginx.yaml || { echo "::error::nginx ingress lost its body size annotation, uploads over 1MB would fail"; exit 1; } | |
| grep -q 'https://lfs.example.com' nginx.yaml || { echo "::error::LFSX_PUBLIC_URL was not derived from the ingress host"; exit 1; } | |
| helm template lfsx chart --set config.publicUrl=http://lfsx.internal --set auth.mode=disabled --set persistence.existingClaim=objects >/dev/null | |
| helm template lfsx chart --set config.publicUrl=https://lfs.example.com --set auth.mode=gitea --set auth.giteaApiUrl=https://git.example.com/api/v1 > gitea.yaml | |
| grep -q 'LFSX_GITEA_API_URL' gitea.yaml || { echo "::error::auth.mode=gitea did not reach the server"; exit 1; } | |
| grep -q 'https://git.example.com/api/v1' gitea.yaml || { echo "::error::auth.giteaApiUrl was not rendered"; exit 1; } | |
| if grep -q 'LFSX_GITHUB_API_URL' gitea.yaml; then | |
| echo "::error::a gitea deployment fell into the GitHub branch"; exit 1 | |
| fi | |
| - name: A bucket deployment carries its credentials by reference | |
| run: | | |
| set -euo pipefail | |
| helm template lfsx chart --set config.publicUrl=https://lfs.example.com --set storage.type=s3 --set storage.s3.endpoint=https://s3.example.com --set storage.s3.bucket=assets --set storage.s3.existingSecret=lfsx-s3 > bucket.yaml | |
| grep -A1 'name: LFSX_STORAGE$' bucket.yaml | grep -q 's3' || { echo "::error::storage.type=s3 did not reach the server"; exit 1; } | |
| grep -q 'secretKeyRef' bucket.yaml || { echo "::error::the bucket keys were not taken from a Secret"; exit 1; } | |
| grep -q 'type: RollingUpdate' bucket.yaml || { echo "::error::a bucket deployment cannot roll, so it has downtime it does not need"; exit 1; } | |
| if grep -A1 'name: LFSX_S3_ACCESS_KEY' bucket.yaml | grep -q 'value:'; then | |
| echo "::error::a key was rendered as a literal value"; exit 1 | |
| fi | |
| - name: A volume deployment stays one pod, and says so rather than breaking | |
| run: | | |
| set -euo pipefail | |
| helm template lfsx chart --set config.publicUrl=https://lfs.example.com > one.yaml | |
| grep -q 'replicas: 1' one.yaml || { echo "::error::the default is no longer a single replica"; exit 1; } | |
| grep -q 'type: Recreate' one.yaml || { echo "::error::a volume deployment must not roll: two pods would share one claim"; exit 1; } | |
| refuses() { | |
| if helm template lfsx chart "$@" >/dev/null 2>&1; then | |
| echo "::error::the chart rendered a topology it cannot serve: $*" | |
| exit 1 | |
| fi | |
| } | |
| refuses --set config.publicUrl=https://lfs.example.com --set replicaCount=2 | |
| refuses --set config.publicUrl=https://lfs.example.com --set storage.type=s3 --set storage.s3.endpoint=https://s3.example.com --set storage.s3.bucket=assets --set storage.s3.existingSecret=lfsx-s3 --set replicaCount=2 | |
| refuses --set config.publicUrl=https://lfs.example.com --set storage.type=s3 --set storage.s3.endpoint=https://s3.example.com --set storage.s3.bucket=assets | |
| refuses --set config.publicUrl=https://lfs.example.com --set storage.type=nfs | |
| refuses --set config.publicUrl=https://lfs.example.com --set auth.mode=gitea | |
| - name: Two pods over one bucket is the shape that carries HA | |
| run: | | |
| set -euo pipefail | |
| helm template lfsx chart --set config.publicUrl=https://lfs.example.com --set storage.type=s3 --set storage.s3.endpoint=https://s3.example.com --set storage.s3.bucket=assets --set storage.s3.existingSecret=lfsx-s3 --set persistence.enabled=false --set replicaCount=2 > ha.yaml | |
| grep -q 'replicas: 2' ha.yaml || { echo "::error::the replica count did not reach the Deployment"; exit 1; } | |
| grep -q 'emptyDir' ha.yaml || { echo "::error::each replica has to stage its own uploads"; exit 1; } | |
| if grep -q 'kind: PersistentVolumeClaim' ha.yaml; then | |
| echo "::error::a ReadWriteOnce claim cannot be shared by two pods"; exit 1 | |
| fi |