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
42 changes: 37 additions & 5 deletions .github/scripts/rebase-onto-upstream.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,43 @@ set -euo pipefail

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote add upstream https://github.com/pingdotgg/t3code.git
git fetch --no-tags upstream main
# shellcheck disable=SC2034 # Consumed by scripts that source this file.
UPSTREAM_REF=$(git rev-parse FETCH_HEAD)
git rebase "$UPSTREAM_REF"
FORK_REF=$(git rev-parse HEAD)

# Tests pass an already-fetched ref so they exercise the production rebase
# policy without reaching GitHub. Workflow callers fetch upstream here.
if [[ -n "${1:-}" ]]; then
UPSTREAM_REF=$(git rev-parse --verify "${1}^{commit}")
else
git remote add upstream https://github.com/pingdotgg/t3code.git
git fetch --no-tags upstream main
UPSTREAM_REF=$(git rev-parse FETCH_HEAD)
fi

OLD_UPSTREAM_REF=$(git merge-base "$FORK_REF" "$UPSTREAM_REF")

# Reapply patches already found upstream so they become explicit empty-patch
# stops. Retiring a fork patch is a maintainer decision, even when upstream has
# accepted an identical change.
if ! git rebase --reapply-cherry-picks --empty=stop "$UPSTREAM_REF"; then
echo "::error title=Fork patch rebase needs review::Resolve conflicts or explicitly retire any fork patch that became empty, then use a source_ref run." >&2
exit 1
fi

if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
if ! range_diff=$(git range-diff --no-color --no-patch \
"$OLD_UPSTREAM_REF..$FORK_REF" \
"$UPSTREAM_REF..HEAD"); then
echo "::warning title=Fork patch range-diff unavailable::The rebase succeeded, but its patch mapping could not be generated." >&2
elif ! {
echo "## Fork patch range-diff"
echo
echo '```text'
echo "$range_diff"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"; then
echo "::warning title=Fork patch range-diff unavailable::The rebase succeeded, but its patch mapping could not be added to the workflow summary." >&2
fi
fi

# The patch stack must never touch upstream's migration manifest or migration
# files. A fork commit in upstream's numbered sequence collides silently on a
Expand Down
30 changes: 12 additions & 18 deletions .github/workflows/fork-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,32 +170,26 @@ jobs:
echo "upstream_ref=$upstream_ref"
} >> "$GITHUB_OUTPUT"

- name: Align main to released nightly
- name: Promote tree-equivalent candidate to main
if: >-
inputs.dry_run != true &&
steps.candidate.outputs.has_changes == 'false'
shell: bash
env:
CANDIDATE_REF: ${{ steps.candidate.outputs.ref }}
MAIN_REF: ${{ steps.candidate.outputs.main_ref }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
set -euo pipefail

# No new upstream this run, so the release job (and its promotion
# step) will not run. The existing origin/nightly commit has a tree
# 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. 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 [[ "$MAIN_REF" == "$nightly_ref" ]]; then
echo "main already matches origin/nightly; skipping promotion."
# has_changes=false means the candidate tree is identical to the
# fully verified released nightly, so no release or repeated test
# pass is needed. Its history can still carry a newer upstream base
# or a reviewed patch retirement; promote that stack instead of
# restoring the older nightly history.
git rev-parse --verify "${CANDIDATE_REF}^{commit}" >/dev/null
if [[ "$MAIN_REF" == "$CANDIDATE_REF" ]]; then
echo "main already matches the tree-equivalent candidate; skipping promotion."
exit 0
fi

Expand Down Expand Up @@ -233,7 +227,7 @@ jobs:
# 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:${MAIN_REF}" \
origin "${nightly_ref}:refs/heads/main"
origin "${CANDIDATE_REF}:refs/heads/main"

- name: Setup Vite+
if: steps.candidate.outputs.has_changes == 'true'
Expand Down Expand Up @@ -580,7 +574,7 @@ jobs:

# The day's first promotion snapshots pre-promotion main; later
# promotions leave the snapshot alone. Same mechanics as prepare's
# "Align main to released nightly" step; keep the two in sync.
# tree-equivalent promotion step; keep the two in sync.
backup_ref="refs/heads/backup/main-$(date -u +%Y%m%d)"
if git ls-remote --exit-code origin "$backup_ref" >/dev/null; then
echo "${backup_ref#refs/heads/} already exists; leaving it in place."
Expand Down
29 changes: 18 additions & 11 deletions docs/operations/fork-nightly.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ publishes a GitHub prerelease.
`main` is the fork patch stack, rebased onto upstream. Two paths move it:

- **Automated promotion (mechanical rebases only).** Every green run promotes the verified rebased
stack to `main`; when upstream has not advanced, the push is skipped as a no-op. When the run has
new changes, promotion happens after the release publishes. When it has none (the candidate tree
matches `origin/nightly`), the prepare job instead aligns `main` to the already-released
`origin/nightly` commit — same tree, already fully verified — skipping as a no-op when `main`
already matches it. The day's first actual promotion snapshots the pre-promotion `main` to
stack to `main`; when upstream has not advanced, the push is skipped as a no-op. A rebase stops
when a fork patch becomes empty, including when upstream accepts an identical change, so retiring
that patch always receives maintainer review through the `source_ref` path. Each successful rebase
writes a patch `range-diff` to the workflow summary. When the run has new changes, promotion happens
after the release publishes. When it has none (the candidate tree matches `origin/nightly`), the
prepare job skips the release but still promotes the candidate when its history advanced. The tree
is already fully verified, while the candidate preserves the newer upstream ancestry or reviewed
patch retirement instead of restoring the old nightly history. The `nightly` ref remains pinned to
the source of the last published artifact, so it can be behind `main` in history while retaining the
same tree. The day's first actual promotion snapshots the pre-promotion `main` to
`backup/main-YYYYMMDD`; later promotions that day leave the snapshot alone. If `main` moved while
the run was in flight (a PR merge, say), promotion is skipped as expected and the next run's
candidate includes the change; a `source_ref` run fails loudly at that point instead, since its
Expand All @@ -22,12 +27,14 @@ publishes a GitHub prerelease.
started from, so a push landing in the last seconds still fails the step loudly (and the Discord
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 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
- **Maintainer-reviewed manual rebases.** When the rebase onto upstream conflicts or a fork patch
becomes empty, the nightly fails in prepare before promoting anything. A human resolves the
conflict locally, or verifies that upstream made the empty patch redundant before skipping it,
and pushes the resolved stack to a scratch branch on origin. Dispatching the workflow with
`source_ref` set to that branch rebases it onto upstream (a no-op when nothing moved since, a loud
failure when it did), verifies it, publishes a release when its tree changed, 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. Prepare compares the supplied
stack's patch IDs with every fork patch currently carried by `main` and fails with the missing
Expand Down
Loading
Loading