Repository navigation
ci: add manual production deployment with a shared deploy driver #3
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: Remote cache staging | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # All PRs and main share staging. Finish deployment, smoke tests, and notification before replacing it. | |
| concurrency: | |
| group: remote-cache-staging | |
| cancel-in-progress: false | |
| env: | |
| REMOTE_CACHE_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| REMOTE_CACHE_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| REMOTE_CACHE_RESOURCE_PREFIX: ${{ vars.REMOTE_CACHE_RESOURCE_PREFIX || 'vp-cache-ci' }} | |
| REMOTE_CACHE_WORKERS_SUBDOMAIN: ${{ vars.REMOTE_CACHE_WORKERS_SUBDOMAIN }} | |
| REMOTE_CACHE_PROFILE: ${{ vars.REMOTE_CACHE_PROFILE || 'free' }} | |
| jobs: | |
| check: | |
| name: Check deployment source | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| ref: ${{ env.REMOTE_CACHE_SOURCE_SHA }} | |
| persist-credentials: false | |
| - uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1 | |
| - run: pnpm check | |
| - run: pnpm smoke | |
| - name: Explain staging deployment availability | |
| if: >- | |
| always() && github.event_name == 'pull_request' && | |
| (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| with: | |
| script: | | |
| await core.summary.addHeading('Remote cache staging').addRaw( | |
| 'Fork and Dependabot PRs run local checks without Cloudflare credentials. ' + | |
| 'A maintainer can copy the reviewed commit to an internal branch and open a PR to deploy to staging. ' + | |
| 'No live deployment passed verification for this PR.' | |
| ).write(); | |
| deploy: | |
| name: Deploy staging and run smoke tests | |
| needs: check | |
| if: >- | |
| vars.REMOTE_CACHE_DEPLOY_ENABLED == 'true' && | |
| github.event.pull_request.user.login != 'dependabot[bot]' && | |
| (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && | |
| (github.event_name != 'workflow_dispatch' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pull-requests: read | |
| outputs: | |
| endpoint: ${{ steps.deploy.outputs.endpoint }} | |
| steps: | |
| - name: Check that the PR still selects this commit | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| with: | |
| script: | | |
| const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: context.issue.number }); | |
| if (pr.state !== 'open' || pr.head.sha !== process.env.REMOTE_CACHE_SOURCE_SHA) { | |
| core.setFailed('This PR deployment is obsolete.'); | |
| } | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| ref: ${{ env.REMOTE_CACHE_SOURCE_SHA }} | |
| persist-credentials: false | |
| - uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1 | |
| - name: Create or update persistent staging resources | |
| id: deploy | |
| run: pnpm ci:deploy staging | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| - name: Run smoke tests against the staging Worker, D1, and R2 | |
| run: pnpm e2e | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| - name: Save verification results and manual fixtures | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: remote-cache-e2e-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: e2e-results/ | |
| retention-days: 7 | |
| if-no-files-found: warn | |
| notify: | |
| name: Update PR staging verification instructions | |
| if: >- | |
| always() && github.event_name == 'pull_request' && | |
| github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' | |
| needs: [check, deploy] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| DEPLOY_RESULT: ${{ needs.deploy.result }} | |
| CHECK_RESULT: ${{ needs.check.result }} | |
| DEPLOY_ENDPOINT: ${{ needs.deploy.outputs.endpoint }} | |
| with: | |
| script: | | |
| // Reuse the marker so existing PR comments are updated. | |
| const marker = '<!-- remote-cache-preview -->'; | |
| const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: context.issue.number }); | |
| if (pr.state !== 'open' || pr.head.sha !== process.env.REMOTE_CACHE_SOURCE_SHA) return; | |
| const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const docs = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/blob/${pr.head.sha}/docs/e2e-plan.md`; | |
| const passed = process.env.DEPLOY_RESULT === 'success'; | |
| let status; | |
| if (passed) { | |
| const endpoint = process.env.DEPLOY_ENDPOINT; | |
| const expected = `https://${process.env.REMOTE_CACHE_RESOURCE_PREFIX}-staging.${process.env.REMOTE_CACHE_WORKERS_SUBDOMAIN}.workers.dev/projects/manual`; | |
| if (endpoint !== expected) throw new Error('Unexpected deployment endpoint'); | |
| status = `Cloudflare staging deployment and smoke tests passed. You can now perform manual verification.\n\n` + | |
| `Endpoint: ${endpoint}\n\n` + | |
| `Download the **remote-cache-e2e-${context.runId}-${process.env.GITHUB_RUN_ATTEMPT}** artifact from the [workflow run](${run}). ` + | |
| `It contains \`manual-fetch.cbor\`, \`manual-manifest.json\`, and \`report.json\`.\n\n` + | |
| `PR checks cover public reads and rejected writes. Main-branch push checks also cover authorized HTTP stores. ` + | |
| `This staging endpoint is shared by all PRs and main. A later deployment replaces its code and manual fixture. ` + | |
| `Compare the deployment ID in the response with the artifact before manual verification. Closing this PR does not remove staging.`; | |
| } else if (process.env.CHECK_RESULT !== 'success') { | |
| status = `The source checks did not pass. No verified Cloudflare staging deployment is ready. See the [workflow run](${run}).`; | |
| } else if (process.env.DEPLOY_RESULT === 'skipped') { | |
| status = `Cloudflare deployment is disabled. Configure the repository secrets and variables described in the [e2e plan](${docs}), ` + | |
| `then set \`REMOTE_CACHE_DEPLOY_ENABLED=true\` and rerun this workflow. No live deployment passed verification.`; | |
| } else { | |
| status = `Cloudflare deployment or e2e verification failed. The staging deployment is not marked ready. See the [workflow run](${run}).`; | |
| } | |
| const body = `${marker}\n### Remote cache staging\n\nCommit: \`${pr.head.sha}\`\n\n${status}\n\n[Manual checks and complete e2e plan](${docs}).`; | |
| const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: pr.number, per_page: 100 }); | |
| const previous = comments.find(c => c.user?.login === 'github-actions[bot]' && c.body?.includes(marker)); | |
| if (previous) await github.rest.issues.updateComment({ ...context.repo, comment_id: previous.id, body }); | |
| else await github.rest.issues.createComment({ ...context.repo, issue_number: pr.number, body }); |