874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
257 lines
13 KiB
YAML
257 lines
13 KiB
YAML
name: nightly build
|
|
run-name: nightly build${{ inputs.request && format(' (request {0})', inputs.request) || '' }}
|
|
# The opt-in Nightly channel (docs/release-flow.md "Nightly"; Ludovic, 2026-10-27). Once a night, and when a trusted
|
|
# reviewer asks (`/nightly 2640 2642`, nightly-command.yml), it builds main plus the pull requests the reviewers
|
|
# picked, merged in order (tools/ci/nightly_set.py). With none picked, it builds the newest union branch on origin
|
|
# (union/<agent>-<name>: main plus pull requests the contribution gate proved together), or main when no union is
|
|
# newer. It publishes the result as the `nightly` pre-release. Launchers install it only for players who turn Nightly
|
|
# on. The previous nightly stays a week as nightly-<date>.
|
|
#
|
|
# Why the signing key never meets the pull requests' code:
|
|
# - `pick` merges the picked heads onto main with `git merge-tree --write-tree`: git objects only, no working tree,
|
|
# so none of their files reaches the disk and no hook, filter or script of theirs runs. It refuses any pull
|
|
# request that changes a path CODEOWNERS gives to Ludovic (this folder, tools/ci/, the signing and update code),
|
|
# pushes the merge as nightly/build-<date>-<n> and saves the set on the nightly-state branch.
|
|
# - This file, game-build.yml and every script below run from main's copy: a schedule runs the default branch, and
|
|
# `uses: ./…` takes the caller's commit. The union's own .github/ is never read.
|
|
# - `build` (game-build.yml, `trusted: false`) never reaches a release's tools: its macOS jobs run on the runner
|
|
# that already runs pull requests' parity jobs, never the release runner; its Rust cache has its own key and is
|
|
# never saved; and releases don't read the machine's shared sccache.
|
|
# - `build` (game-build.yml) checks out and compiles the merge (or the union) with no secrets: its token reads contents only and
|
|
# isn't kept in the checkout; the macOS programs are signed ad hoc (Developer ID needs the runner's keychain,
|
|
# which stays locked for nightlies).
|
|
# - `sign` is the only job with RELEASE_SIGNING_KEY. It checks out nothing but main's tools/ci/sign_release.sh, which
|
|
# runs the runner's sha256sum and OpenSSL over the archives: it hashes them, never runs them. The signed message
|
|
# names the channel ("horizon release nightly"), so a nightly's signature can't pass for a release's.
|
|
# - `publish` verifies the signature with main's horizon-release-sign, checks the smoke test, and only then moves
|
|
# the tag. Like sign, it never executes what build produced.
|
|
# - The source ships with the build: `pick` writes horizon-source.tar.gz with `git archive` (git objects again, nothing
|
|
# run), `sign` signs it with the builds, and `publish` attaches it and tags the commit nightly-source/<date>-<short>
|
|
# for good (the dated release and the build branch go after 7 days; native mods pin the commit: docs/native-mods.md).
|
|
# gh workflow run "nightly build" # now (skips when the pick is the same commit as last night's)
|
|
# gh workflow run "nightly build" -f force=true # build and publish even if unchanged
|
|
# gh workflow run "nightly build" -f dry_run=true # build and sign with a throwaway key, publish nothing
|
|
on:
|
|
schedule:
|
|
- cron: '17 20 * * *' # 20:17 UTC: after the day's waves, before the 03:07 Paris parity audit takes the Mac mini
|
|
workflow_dispatch:
|
|
inputs:
|
|
force:
|
|
description: Build and publish even if tonight's pick is the commit already published
|
|
type: boolean
|
|
default: false
|
|
dry_run:
|
|
description: Build and sign with a throwaway key, publish nothing
|
|
type: boolean
|
|
default: false
|
|
issue:
|
|
description: The issue or pull request whose `/nightly` comment asked for this build (answered there)
|
|
type: string
|
|
default: ''
|
|
request:
|
|
description: The command's id, in the run's name so it can link the run
|
|
type: string
|
|
default: ''
|
|
prs:
|
|
description: A dry run's own set, `2640@<head> 2642@<head>` (the saved set stays)
|
|
type: string
|
|
default: ''
|
|
permissions:
|
|
contents: read
|
|
concurrency:
|
|
# One build at a time. A newer request replaces one still waiting: it builds the set as it is by then, so no pick
|
|
# is lost.
|
|
group: nightly-build
|
|
cancel-in-progress: false
|
|
jobs:
|
|
pick:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write # nightly/build-<date>-<n> and the nightly-state branch (main is protected)
|
|
issues: write # says in the thread when the picked pull requests conflict
|
|
pull-requests: write
|
|
outputs:
|
|
build: ${{ steps.pick.outputs.build }}
|
|
sha: ${{ steps.pick.outputs.sha }}
|
|
short: ${{ steps.pick.outputs.short }}
|
|
source: ${{ steps.pick.outputs.source }}
|
|
date: ${{ steps.pick.outputs.date }}
|
|
version: ${{ steps.pick.outputs.version }}
|
|
nightly: ${{ steps.pick.outputs.nightly }}
|
|
source_tag: ${{ steps.pick.outputs.source_tag }}
|
|
dry_run: ${{ steps.mode.outputs.dry_run }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
- id: mode
|
|
# A dry run never publishes; a hand-started run off main is always one.
|
|
run: echo "dry_run=${{ github.event_name == 'workflow_dispatch' && (inputs.dry_run || github.ref != 'refs/heads/main') }}" >> "$GITHUB_OUTPUT"
|
|
- id: pick
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
FORCE: ${{ (inputs.force || steps.mode.outputs.dry_run == 'true') && 'yes' || '' }}
|
|
DRY_RUN: ${{ steps.mode.outputs.dry_run }}
|
|
NIGHTLY_ISSUE: ${{ inputs.issue }}
|
|
NIGHTLY_PRS: ${{ inputs.prs }}
|
|
run: |
|
|
python3 tools/ci/nightly.py pick ${FORCE:+--force} | tee -a "$GITHUB_STEP_SUMMARY"
|
|
{ echo; cat nightly-notes.md; } >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Old nightly/build-* branches deleted (scheduled runs only)
|
|
# Ludovic, 2026-10-27: older than 7 days, but never the newest build or one a published nightly points to;
|
|
# nothing but nightly/build-* (nightly.py stale_builds, tests/test_nightly.py).
|
|
if: github.event_name == 'schedule'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
{ echo "### nightly/build-* branches"; python3 tools/ci/nightly.py prune-builds --days 7; } | tee -a "$GITHUB_STEP_SUMMARY"
|
|
- name: The source of the build (horizon-source.tar.gz)
|
|
if: steps.pick.outputs.build == 'true'
|
|
env:
|
|
SHA: ${{ steps.pick.outputs.sha }}
|
|
run: git archive --format=tar.gz --prefix="horizon-${SHA:0:7}/" -o horizon-source.tar.gz "$SHA"
|
|
- uses: actions/upload-artifact@v4
|
|
if: steps.pick.outputs.build == 'true'
|
|
with:
|
|
name: horizon-source
|
|
path: horizon-source.tar.gz
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: nightly-notes
|
|
path: nightly-notes.md
|
|
retention-days: 3
|
|
|
|
build:
|
|
needs: pick
|
|
if: needs.pick.outputs.build == 'true'
|
|
uses: ./.github/workflows/game-build.yml
|
|
with:
|
|
ref: ${{ needs.pick.outputs.sha }}
|
|
version: ${{ needs.pick.outputs.version }}
|
|
nightly: ${{ needs.pick.outputs.nightly }}
|
|
developer_id: false
|
|
trusted: false # it can hold unreviewed pull requests: their own cache key, never saved, not the release runner
|
|
|
|
sign:
|
|
needs: [pick, build]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
# The key's only job: main's sign_release.sh hashes the archives and signs with the runner's OpenSSL.
|
|
environment: ${{ needs.pick.outputs.dry_run != 'true' && 'release' || '' }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
sparse-checkout: tools/ci/sign_release.sh
|
|
sparse-checkout-cone-mode: false
|
|
persist-credentials: false
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
path: dist
|
|
pattern: horizon-*
|
|
merge-multiple: true
|
|
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig) for the nightly channel
|
|
env:
|
|
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
|
|
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
|
|
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
|
|
run: |
|
|
bash tools/ci/sign_release.sh nightly dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
|
|
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz dist/horizon-source.tar.gz
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: signature
|
|
path: dist/SHA256SUMS*
|
|
if-no-files-found: error
|
|
retention-days: 3
|
|
- uses: actions/upload-artifact@v4
|
|
if: needs.pick.outputs.dry_run == 'true'
|
|
with:
|
|
name: ephemeral.pub
|
|
path: dist/ephemeral.pub
|
|
retention-days: 3
|
|
|
|
publish:
|
|
needs: [pick, build, sign]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
persist-credentials: false
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
path: dist
|
|
merge-multiple: true
|
|
- name: The signature, checked as launchers check it
|
|
env:
|
|
EPHEMERAL: ${{ needs.pick.outputs.dry_run == 'true' && 'yes' || '' }}
|
|
run: |
|
|
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
|
|
cargo run --locked --quiet -p horizon-release-sign -- verify --tag nightly --publishing "${key[@]}" dist
|
|
- name: The smoke test drew a world
|
|
# No release-blocker check: a nightly is how blockers get tested.
|
|
run: python3 tools/release_train.py gate --kind game --smoke dist/smoke-test.json
|
|
- name: Publish as the `nightly` pre-release
|
|
if: needs.pick.outputs.dry_run != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
SHA: ${{ needs.pick.outputs.sha }}
|
|
DATE: ${{ needs.pick.outputs.date }}
|
|
SOURCE_TAG: ${{ needs.pick.outputs.source_tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
python3 tools/ci/nightly.py rotate
|
|
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
|
|
# A pre-release, never GitHub's latest: launchers on the release channel never see it.
|
|
gh release create nightly --prerelease --latest=false --target "$SHA" \
|
|
--title "Nightly $DATE · ${SHA:0:7}" --notes-file dist/nightly-notes.md \
|
|
"dist/horizon-linux-x86_64.tar.gz#Nightly game files: Linux (horizon-linux-x86_64.tar.gz)" \
|
|
"dist/horizon-windows-x64.zip#Nightly game files: Windows (horizon-windows-x64.zip)" \
|
|
"dist/horizon-macos-arm64.zip#Nightly game files: macOS (horizon-macos-arm64.zip)" \
|
|
"dist/horizon-server-linux-x86_64.tar.gz#Nightly dedicated server for Linux (horizon-server-linux-x86_64.tar.gz)" \
|
|
"dist/horizon-source.tar.gz#Source of this Nightly, commit ${SHA:0:7} (horizon-source.tar.gz; kept for good as the tag $SOURCE_TAG)" \
|
|
dist/SHA256SUMS "${signature[@]}" dist/smoke-test.json
|
|
python3 tools/ci/nightly.py keep-source "$SHA" "$DATE"
|
|
python3 tools/ci/nightly.py prune --days 7
|
|
- name: Dry run, the nightly that would be published
|
|
if: needs.pick.outputs.dry_run == 'true'
|
|
run: |
|
|
echo "Would publish nightly at ${{ needs.pick.outputs.sha }} (${{ needs.pick.outputs.source }})"
|
|
ls -l dist
|
|
|
|
answer:
|
|
# Tells the thread that asked for this build how it ended. A conflict `pick` already explained there.
|
|
needs: [pick, build, sign, publish]
|
|
if: always() && inputs.issue != '' && needs.pick.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
ISSUE: ${{ inputs.issue }}
|
|
BUILD: ${{ needs.pick.outputs.build }}
|
|
PUBLISHED: ${{ needs.publish.result }}
|
|
DRY_RUN: ${{ needs.pick.outputs.dry_run }}
|
|
NAME: Nightly ${{ needs.pick.outputs.date }} · ${{ needs.pick.outputs.short }}
|
|
RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
if [[ "$BUILD" != true ]]; then
|
|
text="nothing new: the published Nightly is already this build."
|
|
elif [[ "$PUBLISHED" == success && "$DRY_RUN" == true ]]; then
|
|
text="the dry run built and signed $NAME; nothing was published. $RUN"
|
|
elif [[ "$PUBLISHED" == success ]]; then
|
|
text="published $NAME: https://github.com/$GITHUB_REPOSITORY/releases/tag/nightly. Launchers with Nightly on install it at their next start."
|
|
else
|
|
text="the build failed, nothing was published: $RUN"
|
|
fi
|
|
gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE/comments" -f body="🤖 Nightly: $text"
|