Skip to content

Deploy Agent-Native beta sites from GitHub #1896

Deploy Agent-Native beta sites from GitHub

Deploy Agent-Native beta sites from GitHub #1896

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