Skip to content

fix(send): close the storage quota bypass on presigned uploads (#1166) #24

fix(send): close the storage quota bypass on presigned uploads (#1166)

fix(send): close the storage quota bypass on presigned uploads (#1166) #24

---
# 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"