From d9c162f44aa5d15c45c7c46d3b93ce84dccbe8f8 Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:02:17 +0200 Subject: [PATCH 1/3] ci: run jobs on gcrunner GitHub-hosted runners don't start jobs for this repository while it's private ("recent account payments have failed or your spending limit needs to be increased"), so nothing has run since 2026-10-12. Lint, Workflow lint, Install, Nightly end-to-end, Security scan, Renovate generate and Upstream drift now run on Appwrite's gcrunner runners (an ephemeral Compute Engine VM per job), as appwrite/appwrite does: c3d-standard-8 for the nightly, c3d-standard-4 for everything else, so the install jobs get the 4 vCPUs and 16 GB of a public GitHub runner. Pull requests from forks fall back to ubuntu-latest, as in appwrite's ci.yml, so code from outside the repository never runs on Appwrite's machines. Release and Scorecard stay on GitHub-hosted runners: Scorecard only publishes results from them, and the release's provenance should name GitHub's runners. Both only run once the repository is public. --- .github/workflows/actions-lint.yaml | 12 +++++-- .github/workflows/e2e-nightly.yaml | 2 +- .github/workflows/lint.yaml | 42 ++++++++++++++++++++---- .github/workflows/renovate-generate.yaml | 6 +++- .github/workflows/security-scan.yaml | 6 ++-- .github/workflows/test-install.yaml | 12 +++++-- .github/workflows/upstream-drift.yaml | 2 +- CONTRIBUTING.md | 9 +++++ 8 files changed, 74 insertions(+), 17 deletions(-) diff --git a/.github/workflows/actions-lint.yaml b/.github/workflows/actions-lint.yaml index 391d8bf..f30a1c6 100644 --- a/.github/workflows/actions-lint.yaml +++ b/.github/workflows/actions-lint.yaml @@ -31,7 +31,11 @@ env: jobs: actionlint: name: actionlint and shellcheck - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-actionlint-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: @@ -60,7 +64,11 @@ jobs: zizmor: name: zizmor - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-zizmor-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: diff --git a/.github/workflows/e2e-nightly.yaml b/.github/workflows/e2e-nightly.yaml index a8e5ae4..bb51be8 100644 --- a/.github/workflows/e2e-nightly.yaml +++ b/.github/workflows/e2e-nightly.yaml @@ -18,7 +18,7 @@ env: jobs: functions: name: Functions and optional components - runs-on: ubuntu-latest + runs-on: ${{ format('gcrunner={0}-functions-{1}/machine=c3d-standard-8', github.run_id, strategy.job-index) }} timeout-minutes: 60 steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7aa7c6e..7819dc4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -23,7 +23,11 @@ env: jobs: lint: name: Lint (Helm ${{ matrix.helm }}) - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-lint-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} strategy: fail-fast: false matrix: @@ -58,7 +62,11 @@ jobs: unittest: name: Unit tests (Helm ${{ matrix.helm }}) - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-unittest-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} strategy: fail-fast: false matrix: @@ -96,7 +104,11 @@ jobs: kubeconform: name: kubeconform - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-kubeconform-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: @@ -118,7 +130,11 @@ jobs: generated: name: Generated files are up to date - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-generated-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: @@ -139,7 +155,11 @@ jobs: metadata: name: Chart metadata - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-metadata-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: @@ -152,7 +172,11 @@ jobs: artifacthub: name: Artifact Hub lint - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-artifacthub-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} container: image: docker.io/artifacthub/ah:v1.23.0@sha256:e5d2a2cdef85aac4928e8176ffaad24746c5fe961d64ff6856f7222957095356 options: --user 1001 @@ -169,7 +193,11 @@ jobs: changelog: name: Changelog - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-changelog-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: diff --git a/.github/workflows/renovate-generate.yaml b/.github/workflows/renovate-generate.yaml index 42bee1a..8ee18aa 100644 --- a/.github/workflows/renovate-generate.yaml +++ b/.github/workflows/renovate-generate.yaml @@ -32,7 +32,11 @@ jobs: # The pull request's author, which a later pusher can't change. Forks # never get here: Renovate pushes branches to this repository. if: github.event.pull_request.user.login == 'renovate[bot]' && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-generate-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} permissions: contents: read steps: diff --git a/.github/workflows/security-scan.yaml b/.github/workflows/security-scan.yaml index 9500097..de6117a 100644 --- a/.github/workflows/security-scan.yaml +++ b/.github/workflows/security-scan.yaml @@ -19,7 +19,7 @@ env: jobs: prepare: name: Images and visibility - runs-on: ubuntu-latest + runs-on: ${{ format('gcrunner={0}-prepare-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) }} outputs: images: ${{ steps.images.outputs.images }} public: ${{ steps.repo.outputs.public }} @@ -49,7 +49,7 @@ jobs: images: name: Image ${{ matrix.image.name }} needs: prepare - runs-on: ubuntu-latest + runs-on: ${{ format('gcrunner={0}-images-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) }} permissions: contents: read security-events: write @@ -86,7 +86,7 @@ jobs: config: name: Rendered manifests needs: prepare - runs-on: ubuntu-latest + runs-on: ${{ format('gcrunner={0}-config-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) }} permissions: contents: read security-events: write diff --git a/.github/workflows/test-install.yaml b/.github/workflows/test-install.yaml index 9d1166f..33d3374 100644 --- a/.github/workflows/test-install.yaml +++ b/.github/workflows/test-install.yaml @@ -30,7 +30,11 @@ concurrency: jobs: install: name: ${{ matrix.scenario }} (k8s ${{ matrix.kubernetes }})${{ matrix.upgrade-from && format(' upgrade from {0}', matrix.upgrade-from) || '' }} - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-install-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} timeout-minutes: 45 strategy: fail-fast: false @@ -168,7 +172,11 @@ jobs: name: Install result needs: install if: always() - runs-on: ubuntu-latest + runs-on: >- + ${{ (github.event_name != 'pull_request' + || github.event.pull_request.head.repo.full_name == github.repository) + && format('gcrunner={0}-result-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) + || 'ubuntu-latest' }} permissions: {} steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 diff --git a/.github/workflows/upstream-drift.yaml b/.github/workflows/upstream-drift.yaml index f1f4939..aa1d5f8 100644 --- a/.github/workflows/upstream-drift.yaml +++ b/.github/workflows/upstream-drift.yaml @@ -12,7 +12,7 @@ permissions: jobs: check: - runs-on: ubuntu-latest + runs-on: ${{ format('gcrunner={0}-check-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) }} permissions: contents: read issues: write # open the drift issue diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34d9a40..7577e46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,6 +105,15 @@ The scenarios CI installs are the `charts/appwrite/ci/*-values.yaml` files. | [Upstream drift](.github/workflows/upstream-drift.yaml) | weekly | opens an issue when Appwrite releases a version the chart doesn't ship | | [Release](.github/workflows/release.yaml) | `Chart.yaml` changes on `main` | see [RELEASING.md](RELEASING.md) | +Jobs run on Appwrite's [gcrunner](https://github.com/appwrite-labs/gcrunner) +runners: an ephemeral Compute Engine VM per job, picked with +`runs-on: gcrunner=--/machine=` (c3d-standard-8 +for the nightly, c3d-standard-4 otherwise). Pull requests from forks run on +GitHub-hosted runners instead, so code from outside the repository never runs +on Appwrite's infrastructure. Release and Scorecard stay on GitHub-hosted +runners: Scorecard only publishes results from them, and the release's +signature and provenance attestation name the runner that built the chart. + Workflows pin every action to a commit SHA (with the version in a comment) and every tool to a version in `env:`, which Renovate updates through the custom managers in `renovate.json`. A new pinned tool needs a manager there. From 77c7304e1306603eee724a6db96b22792b132518 Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:04:51 +0200 Subject: [PATCH 2/3] ci: run ah lint with docker run as the runner's user As a job container with --user 1001, ah couldn't write the runner's files on gcrunner, whose runner user has another uid (EACCES on _runner_file_commands). docker run with the runner's own uid works on any runner. Renovate keeps the image current through the Trivy image's custom manager, now matching AH_IMAGE too. --- .github/workflows/lint.yaml | 11 ++++++----- renovate.json | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7819dc4..9fcd047 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -177,9 +177,11 @@ jobs: || github.event.pull_request.head.repo.full_name == github.repository) && format('gcrunner={0}-artifacthub-{1}/machine=c3d-standard-4', github.run_id, strategy.job-index) || 'ubuntu-latest' }} - container: - image: docker.io/artifacthub/ah:v1.23.0@sha256:e5d2a2cdef85aac4928e8176ffaad24746c5fe961d64ff6856f7222957095356 - options: --user 1001 + env: + # Run from the image, pinned by digest, with the runner's own uid: as a + # job container, its user has to match the runner's to write the + # runner's files, and that uid differs between runners. + AH_IMAGE: docker.io/artifacthub/ah:v1.23.0@sha256:e5d2a2cdef85aac4928e8176ffaad24746c5fe961d64ff6856f7222957095356 steps: - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 with: @@ -188,8 +190,7 @@ jobs: with: persist-credentials: false - name: ah lint - working-directory: charts - run: ah lint + run: docker run --rm -u "$(id -u):$(id -g)" -v "${GITHUB_WORKSPACE}/charts:/charts:ro" -w /charts "$AH_IMAGE" ah lint changelog: name: Changelog diff --git a/renovate.json b/renovate.json index 6ffa8c9..f916682 100644 --- a/renovate.json +++ b/renovate.json @@ -204,13 +204,13 @@ "extractVersionTemplate": "^v(?.*)$" }, { - "description": "Trivy image (tag and digest) in the security scan workflow", + "description": "Images run with docker run in the workflows (tag and digest): Trivy and Artifact Hub's ah", "customType": "regex", "managerFilePatterns": [ "/^\\.github/workflows/.+\\.ya?ml$/" ], "matchStrings": [ - "TRIVY_IMAGE:\\s*(?[^:\\s]+):(?[^@\\s]+)@(?sha256:[a-f0-9]{64})" + "(?:TRIVY|AH)_IMAGE:\\s*(?[^:\\s]+):(?[^@\\s]+)@(?sha256:[a-f0-9]{64})" ], "datasourceTemplate": "docker" } From df59565be7346462bf11b14dcc2fbbb0f0d966c1 Mon Sep 17 00:00:00 2001 From: Levi van Noort <73097785+levivannoort@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:27:02 +0200 Subject: [PATCH 3/3] docs: the fork fallback isn't a security boundary A pull request runs its own copy of the workflow, so a fork can rewrite runs-on and ask for gcrunner, which doesn't check where a job comes from. Say so, and name the guard for a public repository: maintainer approval of every fork pull request's workflow runs. --- CONTRIBUTING.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7577e46..2db945b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -108,9 +108,14 @@ The scenarios CI installs are the `charts/appwrite/ci/*-values.yaml` files. Jobs run on Appwrite's [gcrunner](https://github.com/appwrite-labs/gcrunner) runners: an ephemeral Compute Engine VM per job, picked with `runs-on: gcrunner=--/machine=` (c3d-standard-8 -for the nightly, c3d-standard-4 otherwise). Pull requests from forks run on -GitHub-hosted runners instead, so code from outside the repository never runs -on Appwrite's infrastructure. Release and Scorecard stay on GitHub-hosted +for the nightly, c3d-standard-4 otherwise). Pull requests from forks ask for +GitHub-hosted runners instead. That isn't a security boundary: a pull request +runs its own copy of the workflow, so a fork can ask for gcrunner, and +gcrunner doesn't check where a job comes from. While the repository is +public, the guard is that a maintainer approves every workflow run from a +fork pull request (Settings → Actions → General → "Require approval for all +external contributors") after reading its workflow changes; never approve a +fork's run that changes `runs-on`. Release and Scorecard stay on GitHub-hosted runners: Scorecard only publishes results from them, and the release's signature and provenance attestation name the runner that built the chart.