mirror of
https://github.com/twentyhq/twenty.git
synced 2026-08-01 10:12:06 -04:00
Follow-up to #23215 (merged). Rebased on `main`. ## Why #23215 fixes an instance of a class of bug: an upgrade command whose version is chosen at `generate:instance-command` time from `TWENTY_CURRENT_VERSION`, then left behind when `main` bumps the version before the PR merges (base-drift). The command ships one minor early and instances already on the newer version skip it forever. The existing `server-previous-version-upgrade-mutation-guard` in `ci-server.yaml` runs on `pull_request`, so it validates against the PR's base. When the base is stale (main moved after the branch was cut), the guard reads the branch's own `TWENTY_CURRENT_VERSION` and the check passes even though the command is now a version behind main. That is exactly how the original bug slipped through. ## What The version-directory and append-only-timestamp validation is extracted into a shared composite action, `.github/actions/upgrade-mutation-guard`, diffed against a caller-supplied `base_sha`. It is called from two places: - **`ci-server.yaml`** (PR-level guard, `base = pull_request.base.sha`) for fast feedback. The job keeps its existing name/check. This replaces ~290 lines of inline shell. - **`ci-merge-queue.yaml`** (new, `merge_group`-triggered, `base = merge_group.base_sha`). GitHub builds each merge-queue candidate on top of the current tip of `main`, so the checks read `TWENTY_CURRENT_VERSION` and the existing per-directory timestamps from main's real state at merge time. Because the candidate is rebased onto main, base-drift is caught by construction: the same validation simply runs where the base is guaranteed current. No origin/main comparison hack; the logic now lives in one place. ## Bypass semantics The guard has two independent checks, and they are treated differently on purpose: - **Version-directory check** keeps its `ci:allow-previous-version-upgrade-mutation` bypass, a deliberate, reviewed escape hatch for legitimately touching a previous-version directory. The PR-level guard reads the label directly; the merge-queue guard resolves it from the queued PR (the `merge_group` event carries no labels) and passes it to the composite action, which skips only the version-directory step. - **Timestamp / append-only check has no bypass.** The old `ci:allow-upgrade-command-timestamp-exception` label is removed. A fake or out-of-order timestamp rewinds the upgrade cursor and re-hides already-applied columns, so there is no "allowed" version of it: the timestamp just has to be configured correctly (real epoch millis, strictly greater than every existing command in the same version directory). If a blocking existing max is itself a fabricated future timestamp, re-slot that command to its real merge epoch rather than reaching for a bypass. Preventing previous-version mutation is the guard's primary purpose. In the merge queue the guard job always runs and skips only the version-directory step when the bypass label is set, so it reports a real success/failure (the required check never resolves to a skipped state, and a label-lookup failure fails closed) and the timestamp check always runs. ## Requires a settings change (not in this diff) Enabling the merge queue and marking the check required are branch-protection settings, not file changes. After merge, an admin needs to: 1. Enable the merge queue for `main` in branch protection. 2. Add `CI - Merge Queue / upgrade-mutation-guard` to the merge queue's required checks. ## Notes - Composite action, not a `workflow_call` reusable workflow, deliberately: converting the `ci-server.yaml` job to a reusable-workflow call would rename its status check to `server-previous-version-upgrade-mutation-guard / ` and break that required-check mapping in branch protection. A composite action dedups the logic while keeping both callers' check names intact. --------- Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com>
61 lines
2.4 KiB
YAML
61 lines
2.4 KiB
YAML
name: CI - Merge Queue
|
|
|
|
# Runs when a PR is queued to merge. GitHub builds the merge candidate on top of
|
|
# the current tip of main, so these checks validate against main's real state at
|
|
# merge time rather than the PR's (possibly stale) base. This is what closes the
|
|
# upgrade-command base-drift race: a command generated against an old version,
|
|
# then left behind when main bumps, is caught here even if the PR-level guard
|
|
# passed on a stale base.
|
|
|
|
on:
|
|
merge_group:
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
|
|
jobs:
|
|
# merge_group carries no PR labels, so resolve them from the queued PR (its
|
|
# number is in the merge-queue ref) and pass the version-mutation bypass to the
|
|
# guard as an input, which skips only the version-directory step. The job
|
|
# always runs and reports a real success/failure, so the required check never
|
|
# resolves to a skipped state and a label-lookup failure fails closed. The
|
|
# timestamp / append-only check has no bypass and always runs: violating it
|
|
# rewinds the upgrade cursor.
|
|
upgrade-mutation-guard:
|
|
timeout-minutes: 5
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout merge candidate
|
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
with:
|
|
fetch-depth: 2
|
|
- name: Resolve version-mutation bypass label from the queued PR
|
|
id: labels
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
HEAD_REF: ${{ github.event.merge_group.head_ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
PR_NUMBER=$(echo "$HEAD_REF" | sed -n -E 's|.*/pr-([0-9]+)-[0-9a-f]+$|\1|p')
|
|
|
|
skip=false
|
|
if [ -n "$PR_NUMBER" ]; then
|
|
LABELS=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json labels --jq '.labels[].name')
|
|
echo "Queued PR #$PR_NUMBER labels:"
|
|
echo "$LABELS"
|
|
|
|
if echo "$LABELS" | grep -qxF 'ci:allow-previous-version-upgrade-mutation'; then
|
|
skip=true
|
|
fi
|
|
else
|
|
echo "Could not parse a PR number from '$HEAD_REF'; enforcing strictly."
|
|
fi
|
|
|
|
echo "skip=$skip" >> "$GITHUB_OUTPUT"
|
|
- name: Validate upgrade command mutations against main
|
|
uses: ./.github/actions/upgrade-mutation-guard
|
|
with:
|
|
base_sha: ${{ github.event.merge_group.base_sha }}
|
|
allow_previous_version_mutation: ${{ steps.labels.outputs.skip }}
|