fix(send): close the storage quota bypass on presigned uploads (#1166) #24
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
| --- | |
| # Additive image-publish pipeline for the EKS/Kargo path. On merge to main it | |
| # builds the Send backend and the Send frontend (nginx SPA) as native multi-arch | |
| # (amd64 + arm64) manifest lists and pushes them to GHCR, tagged :<sha>, | |
| # :v<version> and :latest. | |
| # | |
| # This is INDEPENDENT of the existing deploy pipeline (merge.yml / release.yml): | |
| # it uses no cloud credentials, runs no Pulumi, touches no S3/ECS/ECR/ATN, and | |
| # deploys nothing -- it only publishes images. The stage/prod S3 + ECS builds and | |
| # the XPI builds are left exactly as they were. | |
| # | |
| # The frontend image is configured at RUNTIME (window.__APP_CONFIG__ via | |
| # /config.js), so one built bundle runs in every environment. See | |
| # packages/send/frontend/src/config.ts. | |
| name: publish-images | |
| concurrency: | |
| # Keyed on the SHA, not the ref, ON PURPOSE. `cancel-in-progress: false` only | |
| # protects a RUNNING run: GitHub cancels the PENDING run of a group when a newer | |
| # one queues, so a ref-keyed group would drop the middle of three quick merges | |
| # and never publish that commit's :<sha> images -- exactly the matched-set hole | |
| # this workflow exists to close. Per-SHA groups never displace each other, and | |
| # concurrent runs are safe because the per-arch pushes are by digest. | |
| group: publish-images-${{ github.sha }} | |
| cancel-in-progress: false | |
| on: | |
| push: | |
| branches: | |
| - main | |
| # Add-on-only commits must not publish Send images. The two images themselves | |
| # are NOT path-filtered against each other -- see the note on the jobs below. | |
| paths: | |
| - packages/send/** | |
| # Workspace-level build inputs: deploy.dockerfile COPYs these and installs | |
| # with --frozen-lockfile, so a lockfile-only security bump does change what | |
| # is in the image. Without them here the promoted image would keep shipping | |
| # the pre-bump dependency tree until the next packages/send commit. | |
| - pnpm-lock.yaml | |
| - pnpm-workspace.yaml | |
| - package.json | |
| - lerna.json | |
| - nx.json | |
| - .npmrc | |
| - packages/addon/package.json | |
| - .github/workflows/publish-images.yml | |
| - .github/workflows/build-multiarch-image.yml | |
| workflow_dispatch: | |
| # MUST stay at workflow scope and MUST include packages: write. A reusable | |
| # workflow can only DOWNGRADE the caller's GITHUB_TOKEN permissions, so dropping | |
| # packages: write here would break the GHCR push inside | |
| # build-multiarch-image.yml no matter what that file declares. | |
| permissions: | |
| contents: read | |
| packages: write | |
| jobs: | |
| # Read the version once. lerna.json is `independent` and the root package.json | |
| # is 0.0.0, so neither is a usable source; packages/send/frontend/package.json | |
| # is the version the backend also carries. NOTE: both images are published under | |
| # it, so a backend-only change still ships under the frontend's version. | |
| version: | |
| runs-on: ubuntu-latest | |
| # This job publishes nothing, so it does not need packages: write. | |
| permissions: | |
| contents: read | |
| outputs: | |
| value: ${{ steps.read-version.outputs.value }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Read version from packages/send/frontend/package.json | |
| id: read-version | |
| run: | | |
| set -euo pipefail | |
| # `jq -e` already exits non-zero on a missing or null .version, and | |
| # `set -e` aborts on it; this only has to catch an empty string. Written | |
| # as an if-block rather than `A && B || C`, which trips shellcheck | |
| # SC2015 and fails the actionlint gate in validate-workflows.yml. | |
| v="$(jq -er .version packages/send/frontend/package.json)" | |
| if [ -z "$v" ]; then | |
| echo "::error::empty version in packages/send/frontend/package.json" | |
| exit 1 | |
| fi | |
| # The value is interpolated into an image tag and (deliberately | |
| # word-split) imagetools arguments downstream, so reject anything | |
| # that is not a plain semver-ish token -- a version containing a | |
| # space or a leading dash could otherwise inject extra arguments. | |
| if ! echo "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.-]+)?$'; then | |
| echo "::error::version '$v' in packages/send/frontend/package.json is not a plain semver string" | |
| exit 1 | |
| fi | |
| echo "value=$v" >> "$GITHUB_OUTPUT" | |
| # Build BOTH images on every run so both always carry the same :<sha>. The | |
| # matched-set invariant (Kargo Freight = {backend@sha, frontend@sha} for the | |
| # exact HEAD sha) requires both :<sha> refs to exist, so the images are | |
| # deliberately NOT path-filtered against each other inside this workflow. | |
| # Moving tags (:latest, :v<version>) are published only from the main branch. | |
| backend: | |
| needs: version | |
| uses: ./.github/workflows/build-multiarch-image.yml | |
| with: | |
| image: ghcr.io/thunderbird/thunderbird-send | |
| dockerfile: packages/send/backend/Dockerfile | |
| # The backend Dockerfile ADDs pnpm-lock.yaml, which lives at the repo root, | |
| # so it needs the assembled .docker-build context that this script creates | |
| # -- the same context merge.yml builds the ECR image from. | |
| prepare-script: packages/send/backend/scripts/build.sh | |
| context: packages/send/backend/.docker-build | |
| version: ${{ needs.version.outputs.value }} | |
| artifact-prefix: backend | |
| moving-tags: ${{ github.ref == 'refs/heads/main' }} | |
| # Backend only: makes a missing token a visible workflow warning rather | |
| # than a line buried in the buildx log. | |
| expect-sourcemap-upload: true | |
| # Only the backend Dockerfile uploads sourcemaps, so only this call forwards | |
| # the token. Absent repo secret -> empty string -> Dockerfile skips upload. | |
| secrets: | |
| sentry_auth_token: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| frontend: | |
| needs: version | |
| uses: ./.github/workflows/build-multiarch-image.yml | |
| with: | |
| image: ghcr.io/thunderbird/thunderbird-send-frontend | |
| dockerfile: packages/send/frontend/deploy.dockerfile | |
| # Repo root: the SPA build needs the pnpm workspace (lockfile + every | |
| # member's package.json), not just the frontend directory. | |
| context: . | |
| version: ${{ needs.version.outputs.value }} | |
| artifact-prefix: frontend | |
| moving-tags: ${{ github.ref == 'refs/heads/main' }} | |
| # Assert the matched set actually landed. | |
| # | |
| # Tagging happens per image, in each caller's own `merge` job, and neither | |
| # references the other. If one image's build fails and the other succeeds, the | |
| # run publishes HALF a matched set under :<sha> -- and a Kargo Warehouse with | |
| # two image subscriptions would then silently pair frontend@newSha with | |
| # backend@oldSha, because both tags it picked resolve. Fail the run loudly | |
| # instead, so the hole is visible here rather than in a promotion. | |
| verify-matched-set: | |
| needs: [backend, frontend] | |
| runs-on: ubuntu-latest | |
| # Read-only: this job inspects manifests, it never pushes. | |
| permissions: | |
| packages: read | |
| steps: | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Both :<sha> images resolve | |
| env: | |
| BACKEND: ${{ needs.backend.outputs.image }} | |
| FRONTEND: ${{ needs.frontend.outputs.image }} | |
| run: | | |
| set -euo pipefail | |
| for ref in "$BACKEND" "$FRONTEND"; do | |
| echo "checking $ref" | |
| docker buildx imagetools inspect "$ref" >/dev/null | |
| done | |
| echo "matched set OK: $BACKEND + $FRONTEND" |