Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 66 additions & 15 deletions .github/workflows/fork-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ on:
required: false
type: boolean
default: false
source_ref:
description: >-
Publish this ref's patch stack instead of main's. A branch or SHA on
origin holding a conflict-resolved stack; it is rebased, verified,
released, and promoted to main like any other candidate
required: false
type: string
default: ""

permissions:
contents: write
Expand All @@ -34,29 +42,66 @@ jobs:
outputs:
has_changes: ${{ steps.candidate.outputs.has_changes }}
ref: ${{ steps.candidate.outputs.ref }}
# The patch stack this run publishes, before the rebase: main's tip, or
# source_ref's commit. Release notes enumerate this stack's commits.
fork_ref: ${{ steps.candidate.outputs.fork_ref }}
# main's tip when the run started, which promotion backs up and replaces.
# Same commit as fork_ref unless source_ref was given.
main_ref: ${{ steps.candidate.outputs.main_ref }}
upstream_ref: ${{ steps.candidate.outputs.upstream_ref }}
version: ${{ steps.release_meta.outputs.version }}
tag: ${{ steps.release_meta.outputs.tag }}
release_name: ${{ steps.release_meta.outputs.name }}
previous_tag: ${{ steps.previous_tag.outputs.previous_tag }}
steps:
- name: Validate source ref
if: inputs.source_ref != ''
shell: bash
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail

# Checking before checkout turns a typo or an unpushed branch into a
# readable first-step failure instead of a checkout error.
if ! gh api "repos/${GITHUB_REPOSITORY}/commits/${SOURCE_REF}" \
--jq .sha; then
echo "source_ref '${SOURCE_REF}' does not resolve to a commit in ${GITHUB_REPOSITORY}; push the stack to origin first." >&2
exit 1
fi

- name: Checkout fork patch stack
uses: actions/checkout@v6
with:
token: ${{ secrets.FORK_RELEASE_TOKEN }}
ref: main
ref: ${{ inputs.source_ref || 'main' }}
fetch-depth: 0

- id: candidate
name: Rebase candidate onto upstream
shell: bash
env:
DRY_RUN: ${{ inputs.dry_run == true }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail

# fork_ref is the stack this run publishes: main's tip, or the
# maintainer's resolved stack on a source_ref run. main_ref is the
# commit promotion replaces, read once here so every later main-side
# check compares against the same snapshot.
fork_ref=$(git rev-parse HEAD)
if [[ -n "${SOURCE_REF:-}" ]]; then
main_ref=$(git ls-remote origin refs/heads/main | cut -f1)
if [[ -z "$main_ref" ]]; then
echo "origin has no main branch to promote onto." >&2
exit 1
fi
else
main_ref=$fork_ref
fi

git fetch origin '+refs/heads/nightly:refs/remotes/origin/nightly' || true
git fetch origin \
'+refs/heads/nightly-candidate:refs/remotes/origin/nightly-candidate' || true
Expand All @@ -77,6 +122,7 @@ jobs:
echo "has_changes=$has_changes"
echo "ref=$candidate_ref"
echo "fork_ref=$fork_ref"
echo "main_ref=$main_ref"
echo "upstream_ref=$upstream_ref"
} >> "$GITHUB_OUTPUT"

Expand All @@ -86,7 +132,7 @@ jobs:
steps.candidate.outputs.has_changes == 'false'
shell: bash
env:
FORK_REF: ${{ steps.candidate.outputs.fork_ref }}
MAIN_REF: ${{ steps.candidate.outputs.main_ref }}
run: |
set -euo pipefail

Expand All @@ -95,21 +141,23 @@ jobs:
# identical to the freshly rebased candidate (that is exactly what
# has_changes=false means) and already shipped through a fully
# verified release, so align main to it directly without
# re-verification.
# re-verification. A source_ref run reaching here resolved to that
# same already-released tree, so origin/nightly is still the right
# commit to put on main.
if ! nightly_ref=$(git rev-parse --verify \
refs/remotes/origin/nightly 2>/dev/null); then
echo "origin/nightly does not exist; nothing to align main to."
exit 0
fi
if [[ "$FORK_REF" == "$nightly_ref" ]]; then
if [[ "$MAIN_REF" == "$nightly_ref" ]]; then
echo "main already matches origin/nightly; skipping promotion."
exit 0
fi

# A PR merged since this run started moves main; that is normal, not
# an error. The next run's candidate will include it.
remote_main=$(git ls-remote origin refs/heads/main | cut -f1)
if [[ "$remote_main" != "$FORK_REF" ]]; then
if [[ "$remote_main" != "$MAIN_REF" ]]; then
echo "main moved during this run; skipping promotion — the next run's candidate will include it."
exit 0
fi
Expand All @@ -126,13 +174,13 @@ jobs:
echo "Checking origin for ${backup_ref} failed (git ls-remote exit ${status})."
exit "$status"
fi
git push origin "${FORK_REF}:${backup_ref}"
git push origin "${MAIN_REF}:${backup_ref}"
fi

# The lease pins main to the commit this run started from. A failed
# lease means main moved in the seconds since the check above; fail
# loudly and let notify_failure fire rather than promote over it.
git push --force-with-lease="refs/heads/main:${FORK_REF}" \
git push --force-with-lease="refs/heads/main:${MAIN_REF}" \
origin "${nightly_ref}:refs/heads/main"

- name: Setup Vite+
Expand Down Expand Up @@ -420,23 +468,26 @@ jobs:
shell: bash
env:
CANDIDATE_REF: ${{ needs.prepare.outputs.ref }}
FORK_REF: ${{ needs.prepare.outputs.fork_ref }}
MAIN_REF: ${{ needs.prepare.outputs.main_ref }}
run: |
set -euo pipefail

git fetch --no-tags origin "$FORK_REF"
git rev-parse --verify "${FORK_REF}^{commit}" >/dev/null
# Everything here is about main's tip as prepare saw it: the commit
# promotion replaces, backs up, and leases against. The published
# stack itself is CANDIDATE_REF.
git fetch --no-tags origin "$MAIN_REF"
git rev-parse --verify "${MAIN_REF}^{commit}" >/dev/null
git rev-parse --verify "${CANDIDATE_REF}^{commit}" >/dev/null

if [[ "$CANDIDATE_REF" == "$FORK_REF" ]]; then
echo "Rebase was a no-op; main already matches the candidate."
if [[ "$CANDIDATE_REF" == "$MAIN_REF" ]]; then
echo "main already matches the candidate; nothing to promote."
exit 0
fi

# A PR merged while this run was building moves main; that is
# normal, not an error. The next run's candidate will include it.
remote_main=$(git ls-remote origin refs/heads/main | cut -f1)
if [[ "$remote_main" != "$FORK_REF" ]]; then
if [[ "$remote_main" != "$MAIN_REF" ]]; then
echo "main moved during this run; skipping promotion — the next run's candidate will include it."
exit 0
fi
Expand All @@ -453,13 +504,13 @@ jobs:
echo "Checking origin for ${backup_ref} failed (git ls-remote exit ${status})."
exit "$status"
fi
git push origin "${FORK_REF}:${backup_ref}"
git push origin "${MAIN_REF}:${backup_ref}"
fi

# The lease pins main to the commit this run started from. A failed
# lease means main moved in the seconds since the check above; fail
# loudly and let notify_failure fire rather than promote over it.
git push --force-with-lease="refs/heads/main:${FORK_REF}" \
git push --force-with-lease="refs/heads/main:${MAIN_REF}" \
origin "${CANDIDATE_REF}:refs/heads/main"

- name: Update rolling fork features issue
Expand Down
9 changes: 7 additions & 2 deletions docs/operations/fork-nightly.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ publishes a GitHub prerelease.
failure notification fires), as does any other failure checking or pushing refs. Dry runs never
promote.
- **Maintainer-reviewed manual rebases.** When the rebase onto upstream conflicts, the nightly fails
in prepare before promoting anything. A human resolves the conflict, verifies the stack, pushes a
backup branch, and force-pushes `main` — the same procedure as before automation existed.
in prepare before promoting anything. A human resolves the conflict locally and pushes the
resolved stack to a scratch branch on origin, then dispatches the workflow with `source_ref` set to
that branch: the run rebases it onto upstream (a no-op when nothing moved since, a loud failure
when it did), verifies it, publishes the release, and promotes it to `main` through the same backup
and lease mechanics as an automated run. The ruleset blocks force-pushing `main` from the CLI, so
this dispatch is how a resolved stack reaches `main`. Pair `source_ref` with `dry_run` first to
verify a resolution without publishing or promoting anything.

## Fork features summary

Expand Down
Loading