From c894a92178196d515f91e31ba87e96700d9f31f5 Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 12 Sep 2026 03:04:45 +0300 Subject: [PATCH] ci: run one pipeline per push to main Both main.yml and release.yml triggered on push to main and each defined its own verify job and its own scan caller, so every commit ran the whole gate twice: two verifies and eight scan jobs, each a fetch-depth: 0 clone plus a Docker pull. Fold main.yml into release.yml, the file npm Trusted Publishing binds to. One verify (now the same definition verify.yml gives pull requests) and one scan, with deploy and release as siblings on needs: [verify, scan]. Deploy still never depends on release: publication failure must not stop attach.uinaf.dev from shipping. --- .github/workflows/main.yml | 87 ----------------------------------- .github/workflows/release.yml | 59 +++++++++++++++++++----- .github/workflows/verify.yml | 10 +++- docs/deploy.md | 2 +- docs/releasing.md | 8 ++-- 5 files changed, 61 insertions(+), 105 deletions(-) delete mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index aa3092a..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Main - -on: - push: - branches: [main] - -permissions: - contents: read - -concurrency: - group: main-${{ github.repository }} - cancel-in-progress: false - -jobs: - verify: - if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - with: - standalone: true - - - uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 - with: - node-version-file: ".node-version" - cache: true - run-install: | - - args: ["--frozen-lockfile"] - - - name: Restore Vite Task cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: node_modules/.vite/task-cache - key: vite-task-${{ runner.os }}-${{ runner.arch }}-${{ github.sha }} - restore-keys: | - vite-task-${{ runner.os }}-${{ runner.arch }}- - - - run: pnpm exec vp run ready - - scan: - if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} - permissions: - contents: read - uses: uinaf/.github/.github/workflows/scan.yml@main - - deploy: - if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} - needs: [verify, scan] - runs-on: ubuntu-24.04 - timeout-minutes: 20 - environment: - name: production - url: https://attach.uinaf.dev - concurrency: - group: deploy-${{ github.repository }}-production - cancel-in-progress: false - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - with: - standalone: true - - - uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 - with: - node-version-file: ".node-version" - cache: false - run-install: | - - args: ["--frozen-lockfile"] - - - name: Deploy Worker - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_D1_DATABASE_ID: ${{ vars.CLOUDFLARE_D1_DATABASE_ID }} - ALLOWED_GITHUB_USER_IDS: ${{ vars.ALLOWED_GITHUB_USER_IDS }} - ATTACH_PUBLIC_BASE: ${{ vars.ATTACH_PUBLIC_BASE }} - run: | - pnpm exec vp run -t @uinaf/attach-web#build - node apps/api/scripts/deploy.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3950129..a5dbaf1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,14 @@ name: Release # npm Trusted Publishing (OIDC) for @uinaf/attach-cli + uinaf-releaser. -# Worker deploy stays in main.yml — never needs: [release]. +# The file name is load-bearing: the trusted publisher binds this package's +# OIDC identity to `.github/workflows/release.yml` plus the `release` +# environment. So the publish job stays here, and everything else that runs on +# a push to main came to it. +# +# `deploy` and `release` are siblings, both on `needs: [verify, scan]`. +# Never make deploy `needs: [release]`. attach.uinaf.dev must keep shipping +# when publication fails. # # First-time bootstrap (human, once): manual npm publish of @uinaf/attach-cli, # then `npm trust github …` — see docs/releasing.md. CI owns every later release. @@ -12,37 +19,65 @@ on: permissions: {} +concurrency: + group: main-${{ github.repository }} + cancel-in-progress: false + jobs: + # One verify and one secret scan for the whole push pipeline. These used to + # live here and in main.yml, on the same trigger, so every push to main ran + # the entire gate twice: two verifies and eight scan jobs per commit. verify: if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + permissions: + contents: read + uses: ./.github/workflows/verify.yml + + scan: + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + permissions: + contents: read + uses: uinaf/.github/.github/workflows/scan.yml@main + + deploy: + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + needs: [verify, scan] runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 20 permissions: contents: read + environment: + name: production + url: https://attach.uinaf.dev concurrency: - group: verify-release-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: deploy-${{ github.repository }}-production + cancel-in-progress: false steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 0 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: standalone: true + - uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 with: node-version-file: ".node-version" - cache: true + cache: false run-install: | - args: ["--frozen-lockfile"] - - run: pnpm exec vp run ready - scan: - if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} - permissions: - contents: read - uses: uinaf/.github/.github/workflows/scan.yml@main + - name: Deploy Worker + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_D1_DATABASE_ID: ${{ vars.CLOUDFLARE_D1_DATABASE_ID }} + ALLOWED_GITHUB_USER_IDS: ${{ vars.ALLOWED_GITHUB_USER_IDS }} + ATTACH_PUBLIC_BASE: ${{ vars.ATTACH_PUBLIC_BASE }} + run: | + pnpm exec vp run -t @uinaf/attach-web#build + node apps/api/scripts/deploy.ts release: if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3a98b80..b56d596 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -4,14 +4,22 @@ on: pull_request: types: [opened, synchronize, reopened, ready_for_review] merge_group: + # release.yml calls this for pushes to main. One definition serves every + # trigger; a second copy is how the gate drifts from the gate that guards + # the branch it ships from. + workflow_call: workflow_dispatch: permissions: contents: read concurrency: + # `github.workflow` and `github.ref` resolve to the caller's, so a called run + # groups under its caller and never collides with a PR run. group: verify-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + # A superseded pull request run is waste. A superseded push to main is not: + # it is the run whose result the release and deploy jobs need. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: verify: diff --git a/docs/deploy.md b/docs/deploy.md index 6186e86..4d9fb66 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -2,7 +2,7 @@ Production Worker deploys from CI, not from a laptop. -Push to `main` → `.github/workflows/main.yml` → GitHub Environment `production` +Push to `main` → `.github/workflows/release.yml` → GitHub Environment `production` → `apps/api/scripts/deploy.ts` (build landing assets, dry-run the Worker, validate R2 lifecycle, D1 migrate, deploy). The dry run and lifecycle gate must pass before production D1 is mutated. diff --git a/docs/releasing.md b/docs/releasing.md index 8033d07..4f6d975 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,9 +1,9 @@ # Releasing -| Workflow | On push to `main` | -| ------------------------------- | -------------------------------------------------------------------------- | -| `.github/workflows/main.yml` | verify → secret scan → Worker deploy (`production`) | -| `.github/workflows/release.yml` | verify → secret scan → npm + Homebrew (`release`, OIDC + `uinaf-releaser`) | +One workflow runs on push to `main`. `.github/workflows/release.yml` runs a +single `verify` and a single `scan`, then `deploy` (Worker, `production`) and +`release` (npm + Homebrew, `release`, OIDC + `uinaf-releaser`) as siblings, both +on `needs: [verify, scan]`. Worker deploy stays independent of npm. Do not make deploy `needs: [release]`.