gbfs: compare-and-swap guard on worker deploys #91
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: Deploy GBFS Workers | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'gbfs/worker/**' | |
| - 'gbfs/loader/**' | |
| - 'gbfs/api/**' | |
| - 'gbfs/compactor/**' | |
| - 'gbfs/cascade/**' | |
| - 'gbfs/lib/**' | |
| - '.github/workflows/gbfs.yml' | |
| workflow_dispatch: | |
| inputs: | |
| worker: | |
| description: 'Worker to deploy (default: all)' | |
| required: false | |
| type: choice | |
| options: [all, worker, loader, api, compactor, cascade] | |
| default: all | |
| force: | |
| description: 'Skip the compare-and-swap check (prod is ahead of git / unstamped)' | |
| required: false | |
| type: boolean | |
| default: false | |
| jobs: | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| worker: ${{ steps.filter.outputs.worker }} | |
| loader: ${{ steps.filter.outputs.loader }} | |
| api: ${{ steps.filter.outputs.api }} | |
| compactor: ${{ steps.filter.outputs.compactor }} | |
| cascade: ${{ steps.filter.outputs.cascade }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| # gbfs/lib is bundled (not a published dep) — every worker that | |
| # imports ../lib must redeploy when it changes. | |
| worker: | |
| - 'gbfs/worker/**' | |
| - 'gbfs/lib/**' | |
| loader: | |
| - 'gbfs/loader/**' | |
| - 'gbfs/lib/**' | |
| api: | |
| - 'gbfs/api/**' | |
| compactor: | |
| - 'gbfs/compactor/**' | |
| cascade: | |
| - 'gbfs/cascade/**' | |
| - 'gbfs/lib/**' | |
| deploy: | |
| needs: detect-changes | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| worker: [worker, loader, api, compactor, cascade] | |
| steps: | |
| - name: Check if this worker should deploy | |
| id: should | |
| run: | | |
| want="${{ inputs.worker || 'all' }}" | |
| if [ "$want" = "all" ]; then | |
| case "${{ matrix.worker }}" in | |
| worker) deploy="${{ needs.detect-changes.outputs.worker }}";; | |
| loader) deploy="${{ needs.detect-changes.outputs.loader }}";; | |
| api) deploy="${{ needs.detect-changes.outputs.api }}";; | |
| compactor) deploy="${{ needs.detect-changes.outputs.compactor }}";; | |
| cascade) deploy="${{ needs.detect-changes.outputs.cascade }}";; | |
| esac | |
| elif [ "$want" = "${{ matrix.worker }}" ]; then | |
| deploy=true | |
| else | |
| deploy=false | |
| fi | |
| echo "deploy=$deploy" >> "$GITHUB_OUTPUT" | |
| echo "Deploy ${{ matrix.worker }}: $deploy" | |
| - if: steps.should.outputs.deploy == 'true' | |
| uses: actions/checkout@v5 | |
| with: | |
| # `gbfs/deploy.sh` proves prod's stamped SHA is an ancestor of | |
| # HEAD; a shallow clone can't answer that. | |
| fetch-depth: 0 | |
| - if: steps.should.outputs.deploy == 'true' | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '22' | |
| - if: steps.should.outputs.deploy == 'true' | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10 | |
| - if: steps.should.outputs.deploy == 'true' | |
| run: pnpm install --frozen-lockfile | |
| working-directory: gbfs/${{ matrix.worker }} | |
| - name: Tests (gate deploy) | |
| if: steps.should.outputs.deploy == 'true' | |
| working-directory: gbfs/${{ matrix.worker }} | |
| run: | | |
| # Run `pnpm test` if defined (currently only `gbfs/api` has | |
| # tests — vitest planQuery suite). Skip silently otherwise. | |
| if node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)"; then | |
| pnpm test | |
| else | |
| echo "no test script in gbfs/${{ matrix.worker }}/package.json — skipping" | |
| fi | |
| # Compare-and-swap: refuses to overwrite a deployment whose stamp | |
| # isn't an ancestor of this commit (or that was built dirty), so a | |
| # push can't silently revert a hand-deployed worker. | |
| - if: steps.should.outputs.deploy == 'true' | |
| run: ./gbfs/deploy.sh ${{ matrix.worker }} ${{ inputs.force && '--force' || '' }} | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |