Nightly Release #44
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: Nightly Release | |
| # The scheduled-nightly release driver (PyAutoHands/docs/nightly_release_design.md | |
| # §3) — the ONLY path authorised to ship a live PyPI release without a | |
| # per-release human, under PyAutoBrain/AUTONOMY.md's standing grant (2026-07-09). | |
| # | |
| # This workflow is a scheduler, nothing more: all sequencing, gating and | |
| # notification logic lives in agents/conductors/release/nightly.sh (testable | |
| # locally via `pyauto-brain release nightly`). Scheduling authority moved HERE | |
| # from PyAutoHands's release.yml (whose cron was removed in the same change) — | |
| # Build executes releases, it does not decide when. | |
| # | |
| # Arming (design §9, all human acts, in order): | |
| # 1. the next manual live release succeeds end-to-end; | |
| # 2. PyAutoBuild#126 closed / de-labelled (`release-blocker`); | |
| # 3. a dry_run=true nightly observed GREEN end-to-end; | |
| # 4. set the repo Actions variable NIGHTLY_RELEASES=true and flip the | |
| # DRY_RUN default below from 'true' to 'false'. | |
| # ARMED 2026-07-09 (human-directed, PyAutoBuild#127): the manual live release | |
| # 2026.7.9.1 succeeded and the human flipped the default the same day — | |
| # checklist steps 2-3 were consciously waived; open release-blocker issues | |
| # (e.g. PyAutoBuild#126) still stop every night at step 3 until closed or | |
| # de-labelled. Pausing is one act: unset NIGHTLY_RELEASES. | |
| # | |
| # OUTCOME CONTRACT (2026-08-04). The driver's exit codes already distinguished | |
| # "a gate stopped the night" from "the driver broke", but this workflow flattened | |
| # every non-zero into a red run — so eight consecutive nights of the gate working | |
| # correctly looked identical to a broken driver, and the channel became one | |
| # nobody watched. The mapping is now explicit: | |
| # | |
| # exit 0 shipped / skipped / dry-run -> job SUCCESS | |
| # exit 2|3 blocked at a gate, no release -> job SUCCESS + ::warning:: + the | |
| # "Blocked at a gate" step below | |
| # exit 1|* driver error, night NOT judged -> job FAILURE | |
| # | |
| # Red is therefore reserved for "the driver itself is broken" — the one state | |
| # that needs a human to look at THIS workflow. A blocked night is a normal, | |
| # expected outcome: Slack carries which gate stopped it, and the run keeps a | |
| # named step + job summary so it is never silently green. | |
| # | |
| # `bin/overnight_status.sh` reads that step name to report a blocked night | |
| # distinctly in the morning glance — keep the step name in sync if it changes. | |
| on: | |
| schedule: | |
| # Every night: quiet nights exit at the activity gate in seconds, so | |
| # weekend scheduling costs nothing and weekend merges release on time. | |
| - cron: "0 2 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: "true = run every gate but log instead of dispatching the live release" | |
| type: choice | |
| options: ["true", "false"] | |
| default: "false" | |
| permissions: | |
| contents: read | |
| # One night at a time; a manual dispatch queues behind a scheduled run rather | |
| # than racing it (two live releases of the same date must be impossible). | |
| concurrency: | |
| group: nightly-release | |
| cancel-in-progress: false | |
| jobs: | |
| nightly: | |
| runs-on: ubuntu-latest | |
| # Rehearsal (~1h) + release-fidelity integration (~2h) + live release (~1.5h). | |
| timeout-minutes: 350 | |
| steps: | |
| - name: Checkout PyAutoBrain | |
| uses: actions/checkout@v4 | |
| - name: Checkout PyAutoHeart (sibling — resolve_heart finds it via PYAUTO_ROOT) | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: PyAutoLabs/PyAutoHeart | |
| path: PyAutoHeart | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML (Heart's only dependency) | |
| run: pip install --quiet pyyaml | |
| - name: Run the nightly driver | |
| id: driver | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_PYAUTOLABS }} | |
| PYAUTO_RELEASE_WEBHOOK_URL: ${{ secrets.PYAUTO_RELEASE_WEBHOOK_URL }} | |
| NIGHTLY_RELEASES: ${{ vars.NIGHTLY_RELEASES }} | |
| # Scheduled runs have no inputs: default 'false' — ARMED 2026-07-09 | |
| # (design §9; the kill switch is the NIGHTLY_RELEASES repo var). | |
| DRY_RUN: ${{ inputs.dry_run || 'false' }} | |
| PYAUTO_ROOT: ${{ github.workspace }} | |
| # `set +e` first: the default shell is `bash -e`, which would abort on | |
| # the driver's exit code before it can be classified (see OUTCOME | |
| # CONTRACT above). This step never fails — the two steps below decide. | |
| run: | | |
| set +e | |
| bash agents/conductors/release/nightly.sh | |
| rc=$? | |
| set -e | |
| echo "rc=$rc" >> "$GITHUB_OUTPUT" | |
| case "$rc" in | |
| 0) echo "outcome=reported" >> "$GITHUB_OUTPUT" ;; | |
| 2|3) echo "outcome=blocked" >> "$GITHUB_OUTPUT" ;; | |
| *) echo "outcome=driver-error" >> "$GITHUB_OUTPUT" ;; | |
| esac | |
| # Named, not just annotated: overnight_status.sh keys the morning glance | |
| # off this step, so a blocked night is never reported as a plain green. | |
| - name: Blocked at a gate — no release was made | |
| if: steps.driver.outputs.outcome == 'blocked' | |
| run: | | |
| echo "::warning title=Nightly release blocked::The driver stopped at a gate (exit ${{ steps.driver.outputs.rc }}); no release was made. This is the gate working — Slack carries which one." | |
| { | |
| echo "## ⏸ Blocked at a gate — no release was made" | |
| echo | |
| echo "The driver stopped deliberately (exit \`${{ steps.driver.outputs.rc }}\`):" | |
| echo "\`2\` = a gate blocked the night, \`3\` = readiness was not GREEN." | |
| echo | |
| echo "This is the gate doing its job, not a driver fault, so the run is" | |
| echo "green. The Slack page names which gate stopped it." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Driver error — the night was NOT judged | |
| if: steps.driver.outputs.outcome == 'driver-error' | |
| run: | | |
| echo "::error title=Nightly driver error::The driver failed with exit ${{ steps.driver.outputs.rc }} — the night was NOT judged and no gate verdict exists." | |
| { | |
| echo "## 🚨 Driver error — the night was NOT judged" | |
| echo | |
| echo "\`nightly.sh\` exited \`${{ steps.driver.outputs.rc }}\`, which is not a gate" | |
| echo "outcome. No release was made AND no gate verdict was reached —" | |
| echo "this workflow needs a human." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 |