Skip to content

ci: add manual production deployment with a shared deploy driver #3

ci: add manual production deployment with a shared deploy driver

ci: add manual production deployment with a shared deploy driver #3

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 });