Generate and deploy Cloudflare Pages #250
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: 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." |