diff --git a/.github/workflows/fork-nightly.yml b/.github/workflows/fork-nightly.yml index 1eca6640ac50..89d82d32334c 100644 --- a/.github/workflows/fork-nightly.yml +++ b/.github/workflows/fork-nightly.yml @@ -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 @@ -34,18 +42,40 @@ 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 @@ -53,10 +83,25 @@ jobs: 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 @@ -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" @@ -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 @@ -95,13 +141,15 @@ 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 @@ -109,7 +157,7 @@ jobs: # 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 @@ -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+ @@ -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 @@ -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 diff --git a/docs/operations/fork-nightly.md b/docs/operations/fork-nightly.md index 5b5cd40256c4..35ffd0d0acee 100644 --- a/docs/operations/fork-nightly.md +++ b/docs/operations/fork-nightly.md @@ -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