Deploy Agent-Native beta sites from GitHub #1896
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 Agent-Native beta sites from GitHub | |
| # GitHub Actions is the sole automatic beta publisher. It builds once and | |
| # uploads prebuilt artifacts to the independent Netlify beta sites; Netlify | |
| # Git-connected auto-builds are disabled to prevent stale queued builds from | |
| # overwriting a newer result. | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| source_ref: | |
| description: Optional main SHA to publish; blank uses the latest main | |
| required: false | |
| type: string | |
| default: "" | |
| handoff: | |
| description: Requeue the latest main source after a production operation | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| actions: read | |
| # Beta is a moving mirror of main. Keep one accepted automatic publisher running | |
| # and coalesce pending automatic and production-handoff runs. Manual runs use | |
| # isolated validation so an invalid input cannot evict a valid pending publish; | |
| # builds use per-run artifacts so the shared production-site queue only covers | |
| # the migration and publish critical section. | |
| concurrency: | |
| group: >- | |
| ${{ github.event_name == 'workflow_dispatch' | |
| && !inputs.handoff | |
| && format('deploy-agent-native-beta-manual-{0}', github.run_id) | |
| || 'deploy-agent-native-beta-sites-prebuilt' }} | |
| cancel-in-progress: false | |
| jobs: | |
| resolve-source: | |
| name: Resolve beta source revision | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| source_sha: ${{ steps.source.outputs.source_sha }} | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| id: source | |
| env: | |
| source_ref: ${{ inputs.source_ref }} | |
| with: | |
| script: | | |
| const requested = process.env.source_ref.trim(); | |
| if (requested && !/^[0-9a-f]{40}$/i.test(requested)) { | |
| core.setFailed('source_ref must be a full 40-character commit SHA.'); | |
| return; | |
| } | |
| const mainSha = (await github.rest.git.getRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: 'heads/main', | |
| })).data.object.sha; | |
| const sourceSha = requested || (context.eventName === 'push' ? context.sha : mainSha); | |
| if (requested) { | |
| const comparison = await github.rest.repos.compareCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| base: sourceSha, | |
| head: mainSha, | |
| }); | |
| if (!['ahead', 'identical'].includes(comparison.data.status)) { | |
| core.setFailed(`source_ref ${sourceSha} is not an ancestor of main ${mainSha}.`); | |
| return; | |
| } | |
| } | |
| core.setOutput('source_sha', sourceSha); | |
| core.info(`beta source is pinned to ${sourceSha}`); | |
| discover-sites: | |
| name: Discover beta sites | |
| needs: resolve-source | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.matrix.outputs.matrix }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| ref: ${{ needs.resolve-source.outputs.source_sha }} | |
| fetch-depth: 1 | |
| sparse-checkout: scripts/netlify-beta-sites.json | |
| sparse-checkout-cone-mode: false | |
| - name: Build the beta site matrix | |
| id: matrix | |
| run: | | |
| set -euo pipefail | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const sites = JSON.parse( | |
| fs.readFileSync('scripts/netlify-beta-sites.json', 'utf8'), | |
| ); | |
| if (!sites.length) throw new Error('No beta sites configured.'); | |
| const matrix = { include: sites.map(({ id }) => ({ site: id })) }; | |
| fs.appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| [ | |
| `matrix=${JSON.stringify(matrix)}`, | |
| '', | |
| ].join('\n'), | |
| ); | |
| console.log(`Discovered ${sites.length} beta sites.`); | |
| NODE | |
| build: | |
| name: ${{ matrix.site }} beta prebuilt build | |
| permissions: | |
| contents: read | |
| needs: [resolve-source, discover-sites] | |
| strategy: | |
| fail-fast: false | |
| # Artifact builds do not touch the production database or publish queue; | |
| # keep the 17-site fleet from waiting behind the eight-job publish cap. | |
| max-parallel: 16 | |
| matrix: ${{ fromJSON(needs.discover-sites.outputs.matrix) }} | |
| uses: ./.github/workflows/deploy-netlify-prebuilt.yml | |
| with: | |
| target: beta | |
| site: ${{ matrix.site }} | |
| caller: automatic-build | |
| source_ref: ${{ needs.resolve-source.outputs.source_sha }} | |
| build_context: branch-deploy | |
| deploy: false | |
| deploy_mode: draft | |
| smoke: false | |
| migration_only: false | |
| artifact_upload: true | |
| artifact_name: beta-${{ matrix.site }}-${{ github.run_id }} | |
| secrets: inherit | |
| confirm-current-source: | |
| name: Confirm beta source is current | |
| # build is a fail-fast:false matrix; one site's failed build must not skip | |
| # every other site's deploy. That site's own deploy job still fails | |
| # (artifact download finds nothing), which is the isolated outcome we want. | |
| if: >- | |
| ${{ always() && !cancelled() && | |
| needs.resolve-source.result == 'success' && | |
| needs.discover-sites.result == 'success' && | |
| contains(fromJSON('["success","failure"]'), needs.build.result) }} | |
| needs: [resolve-source, discover-sites, build] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| current: ${{ steps.source.outputs.current }} | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| id: source | |
| env: | |
| SOURCE_SHA: ${{ needs.resolve-source.outputs.source_sha }} | |
| with: | |
| script: | | |
| const sourceSha = process.env.SOURCE_SHA.trim(); | |
| const mainSha = (await github.rest.git.getRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: 'heads/main', | |
| })).data.object.sha; | |
| // Monotonic check: the source only needs to be an ancestor of (or | |
| // equal to) current main, not an exact match — main moves faster | |
| // than this job runs, so exact equality livelocks the whole fleet. | |
| const onMain = ['ahead', 'identical'].includes( | |
| (await github.rest.repos.compareCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| base: sourceSha, | |
| head: mainSha, | |
| })).data.status, | |
| ); | |
| core.setOutput('current', onMain ? 'true' : 'false'); | |
| if (!onMain) { | |
| core.info(`Skipping beta source ${sourceSha}: not on main (main is ${mainSha}).`); | |
| } | |
| deploy: | |
| name: ${{ matrix.site }} beta prebuilt deploy | |
| if: >- | |
| ${{ always() && !cancelled() && | |
| needs.resolve-source.result == 'success' && | |
| needs.discover-sites.result == 'success' && | |
| contains(fromJSON('["success","failure"]'), needs.build.result) && | |
| needs.confirm-current-source.outputs.current == 'true' }} | |
| permissions: | |
| contents: read | |
| needs: [resolve-source, discover-sites, build, confirm-current-source] | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 8 | |
| matrix: ${{ fromJSON(needs.discover-sites.outputs.matrix) }} | |
| uses: ./.github/workflows/deploy-netlify-prebuilt.yml | |
| with: | |
| target: beta | |
| site: ${{ matrix.site }} | |
| caller: ${{ github.event_name == 'workflow_dispatch' && inputs.handoff && 'automatic' || github.event_name == 'workflow_dispatch' && 'manual' || 'automatic' }} | |
| source_ref: ${{ needs.resolve-source.outputs.source_sha }} | |
| build_context: branch-deploy | |
| deploy: true | |
| deploy_mode: production | |
| smoke: true | |
| migration_only: false | |
| artifact_download: true | |
| artifact_name: beta-${{ matrix.site }}-${{ github.run_id }} | |
| secrets: inherit |