Skip to content

Generate and deploy Cloudflare Pages #250

Generate and deploy Cloudflare Pages

Generate and deploy Cloudflare Pages #250

name: Generate and deploy Cloudflare Pages
on:
schedule:
# Run after the 00Z/06Z/12Z/18Z HRRR extended cycles normally have f48.
# The second run in each hour is a backup in case GitHub drops or delays the
# first scheduled event; the refresh script exits when production is current.
- cron: '0,15 3,9,15,21 * * *'
workflow_dispatch:
inputs:
force_deploy:
description: 'Generate and deploy even when production already reports the detected cycle.'
required: false
default: 'false'
d2_carry_forward_base_url:
description: 'Optional prior Cloudflare deployment used to recover a still-valid D2 archive.'
required: false
default: ''
concurrency:
group: autooutlook-free-direct-refresh
cancel-in-progress: false
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
AUTOOUTLOOK_PRODUCTION_INDEX_URL: ${{ vars.AUTOOUTLOOK_PRODUCTION_INDEX_URL || 'https://autooutlook.tech/api/outlook/incremental' }}
CLOUDFLARE_PAGES_PROJECT: ${{ vars.CLOUDFLARE_PAGES_PROJECT || 'autooutlook-pages' }}
CLOUDFLARE_PAGES_BRANCH: ${{ vars.CLOUDFLARE_PAGES_BRANCH || 'master' }}
AUTOOUTLOOK_HOUR_WORKERS: '3'
AUTOOUTLOOK_RANGE_WORKERS: '4'
AUTOOUTLOOK_RANGE_COALESCE_GAP_BYTES: '2097152'
AUTOOUTLOOK_GRID_STRIDE: '2'
AUTOOUTLOOK_TILE_STRIDE: '1'
jobs:
refresh:
runs-on: ubuntu-latest
timeout-minutes: 300
steps:
- name: Check out source
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Install system libraries
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libgomp1
- name: Generate Cloudflare Pages bundle
id: prepare
env:
AUTOOUTLOOK_FORCE_DEPLOY: ${{ github.event.inputs.force_deploy || 'false' }}
AUTOOUTLOOK_D2_CARRY_FORWARD_BASE_URL: ${{ github.event.inputs.d2_carry_forward_base_url || '' }}
AUTOOUTLOOK_VENV_DIR: ${{ runner.temp }}/autooutlook-venv
AUTOOUTLOOK_STATE_DIR: ${{ runner.temp }}/autooutlook-state
AUTOOUTLOOK_HRRR_CACHE_DIR: ${{ runner.temp }}/autooutlook-hrrr-cache
AUTOOUTLOOK_LOG_DIR: ${{ runner.temp }}/autooutlook-logs
run: |
refresh_args=(-NoProfile -File scripts/windows/refresh-autooutlook.ps1 -SkipEnvFile -SkipDeploy)
if [ "${AUTOOUTLOOK_FORCE_DEPLOY}" = "true" ]; then
refresh_args+=(-Force)
fi
pwsh "${refresh_args[@]}"
- name: Deploy bundle with Cloudflare Wrangler Action
id: deploy
if: ${{ steps.prepare.outputs.deploy_required == 'true' }}
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: '4'
command: pages deploy dist --project-name=${{ env.CLOUDFLARE_PAGES_PROJECT }} --branch=${{ env.CLOUDFLARE_PAGES_BRANCH }} --commit-dirty=true
- name: Verify production serves the deployed cycle
if: ${{ steps.prepare.outputs.deploy_required == 'true' }}
shell: pwsh
env:
EXPECTED_CYCLE_TIME_ISO: ${{ steps.prepare.outputs.cycle_time_iso }}
EXPECTED_D2_FORECAST_DATE: ${{ steps.prepare.outputs.d2_forecast_date }}
run: |
$separator = if ($env:AUTOOUTLOOK_PRODUCTION_INDEX_URL.Contains('?')) { '&' } else { '?' }
$verificationUrl = "$env:AUTOOUTLOOK_PRODUCTION_INDEX_URL${separator}deployment=$env:GITHUB_RUN_ID"
$indexUri = [Uri]$env:AUTOOUTLOOK_PRODUCTION_INDEX_URL
$origin = $indexUri.GetLeftPart([UriPartial]::Authority)
function Test-DeployedD2 {
if ([string]::IsNullOrWhiteSpace($env:EXPECTED_D2_FORECAST_DATE)) {
return $true
}
$date = $env:EXPECTED_D2_FORECAST_DATE
$available = Invoke-RestMethod -Uri "$origin/api/outlook/merged-d2-available-dates?date=$date&deployment=$env:GITHUB_RUN_ID" -TimeoutSec 20
if ($date -notin @($available.dates)) {
return $false
}
foreach ($route in @(
'/api/outlook/merged-d2-risk-polygons',
'/api/outlook/merged-d2-probability-tile',
'/api/outlook/merged-d2-spc-category'
)) {
$response = Invoke-WebRequest -Uri "${origin}${route}?date=$date&deployment=$env:GITHUB_RUN_ID" -Method Head -TimeoutSec 20
if ([int]$response.StatusCode -ne 200) {
return $false
}
}
return $true
}
for ($attempt = 1; $attempt -le 18; $attempt++) {
try {
$payload = Invoke-RestMethod -Uri $verificationUrl -TimeoutSec 20
$ready = @($payload.readyForecastHours)
$productionCycleTimeIso = ([DateTimeOffset]$payload.cycleTimeISO).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
$cycleReady = $productionCycleTimeIso -eq $env:EXPECTED_CYCLE_TIME_ISO -and $payload.status -eq 'complete' -and $ready.Count -ge 49
if ($cycleReady -and (Test-DeployedD2)) {
$d2Status = if ($env:EXPECTED_D2_FORECAST_DATE) { " and D2 $env:EXPECTED_D2_FORECAST_DATE" } else { '' }
Write-Host "Cloudflare production confirmed $env:EXPECTED_CYCLE_TIME_ISO with $($ready.Count) forecast hours$d2Status."
exit 0
}
Write-Host "Attempt ${attempt}: production reports $productionCycleTimeIso, expected $env:EXPECTED_CYCLE_TIME_ISO."
}
catch {
Write-Host "Attempt ${attempt}: production verification failed: $($_.Exception.Message)"
}
Start-Sleep -Seconds 10
}
throw "Cloudflare production did not confirm $env:EXPECTED_CYCLE_TIME_ISO within three minutes."