Registry preview #14
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: Registry preview | |
| on: | |
| workflow_run: | |
| workflows: | |
| - Registry checks | |
| types: | |
| - completed | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| identity: | |
| name: Resolve trusted preview identity | |
| if: >- | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.head_repository.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| outputs: | |
| head_sha: ${{ steps.identity.outputs.head_sha }} | |
| pull_number: ${{ steps.identity.outputs.pull_number }} | |
| steps: | |
| - name: Resolve exact internal pull request | |
| id: identity | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const runId = context.payload.workflow_run.id | |
| const { data: run } = await github.rest.actions.getWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: runId, | |
| }) | |
| if (run.name !== 'Registry checks' || run.path !== '.github/workflows/ci.yml') { | |
| core.setFailed('Preview must be orchestrated by the trusted Registry checks workflow.') | |
| return | |
| } | |
| if (run.conclusion !== 'success' || run.event !== 'pull_request') { | |
| core.setFailed('Preview delivery requires a successful pull-request check run.') | |
| return | |
| } | |
| if (run.head_repository?.full_name !== `${context.repo.owner}/${context.repo.repo}`) { | |
| core.setFailed('Fork pull requests are not eligible for preview delivery.') | |
| return | |
| } | |
| const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| commit_sha: run.head_sha, | |
| }) | |
| const eligible = pulls.data.filter((pull) => | |
| pull.state === 'open' && | |
| pull.base.ref === 'main' && | |
| pull.head.sha === run.head_sha && | |
| pull.head.repo?.full_name === `${context.repo.owner}/${context.repo.repo}` | |
| ) | |
| if (eligible.length !== 1) { | |
| core.setFailed('Run must be the exact head of one eligible internal pull request.') | |
| return | |
| } | |
| const pull = eligible[0] | |
| core.setOutput('head_sha', run.head_sha) | |
| core.setOutput('pull_number', String(pull.number)) | |
| build: | |
| name: Build exact-head CPython image | |
| needs: identity | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout exact PR head without deployment authority | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.identity.outputs.head_sha }} | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version-file: .python-version | |
| - name: Install PDM | |
| run: python -m pip install pdm==2.28.0 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| version: 11.11.0 | |
| - name: Set up Node | |
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | |
| with: | |
| node-version-file: package.json | |
| cache: pnpm | |
| - name: Build the single service image without deployment credentials | |
| env: | |
| SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} | |
| run: | | |
| docker build --build-arg SOURCE_REVISION="$SOURCE_SHA" --tag registry-preview . | |
| docker save registry-preview --output "$RUNNER_TEMP/registry-image.tar" | |
| - name: Transfer the image to a separate delivery runner | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: registry-image | |
| path: ${{ runner.temp }}/registry-image.tar | |
| retention-days: 1 | |
| if-no-files-found: error | |
| deploy: | |
| name: ext-reg preview | |
| needs: [identity, build] | |
| concurrency: | |
| group: registry-preview-ext-reg-${{ needs.identity.outputs.pull_number }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| deployments: write | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| environment: | |
| name: preview | |
| url: ${{ steps.deploy.outputs.url }} | |
| steps: | |
| - name: Checkout only the trusted controller | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version-file: .python-version | |
| - name: Install PDM | |
| run: python -m pip install pdm==2.28.0 | |
| - name: Install pnpm | |
| uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| version: 11.11.0 | |
| - name: Set up Node | |
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 | |
| with: | |
| node-version-file: package.json | |
| cache: pnpm | |
| - name: Install frozen trusted tooling | |
| run: | | |
| pdm install --frozen-lockfile | |
| pnpm install --frozen-lockfile | |
| - name: Install the pinned Heroku CLI | |
| run: npm install --global heroku@11.8.1 | |
| - name: Download this run's service image | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| with: | |
| name: registry-image | |
| path: ${{ runner.temp }}/registry-image | |
| - name: Recheck PR identity after build and concurrency queue | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} | |
| SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} | |
| with: | |
| script: | | |
| const { data: pull } = await github.rest.pulls.get({ | |
| ...context.repo, pull_number: Number(process.env.PULL_NUMBER), | |
| }) | |
| if (pull.state !== 'open' || pull.base.ref !== 'main' || | |
| pull.head.repo?.full_name !== process.env.GITHUB_REPOSITORY || | |
| pull.head.sha !== process.env.SOURCE_SHA) { | |
| throw new Error('PR closed or moved; no preview mutation is allowed.') | |
| } | |
| - name: Apply migrations and deploy the isolated service | |
| id: deploy | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_PREVIEW_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN }} | |
| HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} | |
| NEON_API_KEY: ${{ secrets.NEON_API_KEY }} | |
| NEON_PROJECT_ID: ${{ vars.NEON_PROJECT_ID }} | |
| NEON_PREVIEW_PARENT_BRANCH_ID: ${{ vars.NEON_PREVIEW_PARENT_BRANCH_ID }} | |
| REGISTRY_PREVIEW_PUBLISHER_TOKEN: ${{ secrets.REGISTRY_PREVIEW_PUBLISHER_TOKEN }} | |
| PULL_NUMBER: ${{ needs.identity.outputs.pull_number }} | |
| SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} | |
| run: | | |
| docker load --input "$RUNNER_TEMP/registry-image/registry-image.tar" | |
| pdm run python scripts/registry_preview.py deploy \ | |
| --pull-number "$PULL_NUMBER" --source-sha "$SOURCE_SHA" --image registry-preview | |
| - name: Record interactive preview evidence | |
| env: | |
| PREVIEW_URL: ${{ steps.deploy.outputs.url }} | |
| SOURCE_SHA: ${{ needs.identity.outputs.head_sha }} | |
| run: | | |
| { | |
| echo '## Registry PR 预览' | |
| echo | |
| echo "- 来源:\`$SOURCE_SHA\`" | |
| echo "- 完整预览:$PREVIEW_URL" | |
| echo '- 目录、详情、Publisher 与原生分发运行同一 CPython 服务。' | |
| echo '- 每 PR 独立 Neon 分支与 R2 桶;不生成示例扩展。' | |
| } >> "$GITHUB_STEP_SUMMARY" |