Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
180 lines
6.9 KiB
YAML
180 lines
6.9 KiB
YAML
name: Upgrade Mutation Guard
|
|
description: >
|
|
Validates that added/modified upgrade commands stay in the current version
|
|
directory and keep timestamps real and append-only, diffed against a given
|
|
base. Shared by the PR-level guard (base = PR base) and the merge-queue guard
|
|
(base = merge candidate's base, i.e. the current tip of main).
|
|
|
|
inputs:
|
|
base_sha:
|
|
description: Commit to diff the working tree against.
|
|
required: true
|
|
allow_previous_version_mutation:
|
|
description: Skip the version-directory check (PR label bypass).
|
|
required: false
|
|
default: 'false'
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Fetch base commit
|
|
shell: bash
|
|
env:
|
|
BASE_SHA: ${{ inputs.base_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$BASE_SHA" ]; then
|
|
echo "::error::base_sha input is empty"
|
|
exit 1
|
|
fi
|
|
git fetch --no-tags --depth=1 origin "$BASE_SHA"
|
|
|
|
- name: Check upgrade version commands are in current version only
|
|
if: ${{ inputs.allow_previous_version_mutation != 'true' }}
|
|
shell: bash
|
|
env:
|
|
BASE_SHA: ${{ inputs.base_sha }}
|
|
COMMANDS_ROOT: packages/twenty-server/src/database/commands/upgrade-version-command
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION_CONSTANT_FILE="packages/twenty-server/src/engine/core-modules/upgrade/constants/twenty-current-version.constant.ts"
|
|
|
|
CURRENT_VERSION=$(sed -n "s/.*TWENTY_CURRENT_VERSION = '\([0-9.]*\)'.*/\1/p" "$VERSION_CONSTANT_FILE")
|
|
|
|
if [ -z "$CURRENT_VERSION" ]; then
|
|
echo "::error::Could not extract TWENTY_CURRENT_VERSION from $VERSION_CONSTANT_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
CURRENT_DIR=$(echo "$CURRENT_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+)\..*/\1-\2/')
|
|
|
|
echo "Current version: $CURRENT_VERSION (directory: $CURRENT_DIR)"
|
|
|
|
DIFF=$(git diff --name-status --find-renames "$BASE_SHA" HEAD -- "$COMMANDS_ROOT")
|
|
|
|
ADDED_FILES=$(echo "$DIFF" | awk '$1 == "A" { print $2 } $1 ~ /^[RC]/ { print $3 }')
|
|
MODIFIED_FILES=$(echo "$DIFF" | awk '$1 == "M" { print $2 }')
|
|
|
|
ADDED_OFFENDERS=""
|
|
MODIFIED_OFFENDERS=""
|
|
|
|
check_files() {
|
|
local category="$1"
|
|
shift
|
|
for file in "$@"; do
|
|
VERSION_DIR=$(echo "$file" | sed -n 's|.*upgrade-version-command/\([0-9]*-[0-9]*\)/.*|\1|p')
|
|
|
|
if [ -n "$VERSION_DIR" ] && [ "$VERSION_DIR" != "$CURRENT_DIR" ]; then
|
|
if [ "$category" = "added" ]; then
|
|
ADDED_OFFENDERS="$ADDED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
|
|
else
|
|
MODIFIED_OFFENDERS="$MODIFIED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
check_files "added" $ADDED_FILES
|
|
check_files "modified" $MODIFIED_FILES
|
|
|
|
if [ -n "$ADDED_OFFENDERS" ] || [ -n "$MODIFIED_OFFENDERS" ]; then
|
|
echo "This change touches upgrade command files outside the current version directory ($CURRENT_DIR / $CURRENT_VERSION)."
|
|
|
|
if [ -n "$ADDED_OFFENDERS" ]; then
|
|
echo ""
|
|
echo "New files added to non-current version directories:"
|
|
echo -e "$ADDED_OFFENDERS"
|
|
fi
|
|
|
|
if [ -n "$MODIFIED_OFFENDERS" ]; then
|
|
echo ""
|
|
echo "Existing files modified in non-current version directories:"
|
|
echo -e "$MODIFIED_OFFENDERS"
|
|
fi
|
|
|
|
echo ""
|
|
echo "If this is intentional, add the label 'ci:allow-previous-version-upgrade-mutation' to the PR and re-run CI."
|
|
echo "Otherwise, move your changes to the current version directory ($CURRENT_DIR)."
|
|
|
|
echo "::error::Upgrade commands were added or modified in non-current version directories."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Check upgrade command timestamps are real and keep the sequence append-only
|
|
shell: bash
|
|
env:
|
|
BASE_SHA: ${{ inputs.base_sha }}
|
|
COMMANDS_ROOT: packages/twenty-server/src/database/commands/upgrade-version-command
|
|
run: |
|
|
set -euo pipefail
|
|
extract_ts() {
|
|
basename "$1" | sed -n -E 's/.*-(instance-command-(fast|slow)|workspace-command)-([0-9]{13})-.*/\3/p'
|
|
}
|
|
|
|
DIFF=$(git diff --name-status --find-renames "$BASE_SHA" HEAD -- "$COMMANDS_ROOT")
|
|
|
|
NEW_FILES=$(echo "$DIFF" | awk '$1 == "A" { print $2 } $1 ~ /^[RC]/ { print $3 }')
|
|
GONE_FILES=$(echo "$DIFF" | awk '$1 == "D" { print $2 } $1 ~ /^R/ { print $2 }')
|
|
|
|
NOW_MS=$(($(date +%s) * 1000))
|
|
MIN_TS=$((NOW_MS - 60 * 24 * 3600 * 1000))
|
|
MAX_TS=$((NOW_MS + 2 * 24 * 3600 * 1000))
|
|
|
|
FAILED=""
|
|
|
|
for file in $NEW_FILES; do
|
|
case "$file" in *.spec.ts) continue ;; esac
|
|
|
|
TS=$(extract_ts "$file")
|
|
|
|
if [ -z "$TS" ]; then
|
|
continue
|
|
fi
|
|
|
|
if [ "$TS" -lt "$MIN_TS" ] || [ "$TS" -gt "$MAX_TS" ]; then
|
|
FAILED="$FAILED\n - $file (timestamp $TS is outside [now - 60 days, now + 2 days])"
|
|
continue
|
|
fi
|
|
|
|
VERSION_DIR=$(dirname "$file")
|
|
|
|
MAX_BASE_TS=0
|
|
for base_file in $(git ls-tree -r --name-only "$BASE_SHA" -- "$VERSION_DIR"); do
|
|
case "$base_file" in *.spec.ts) continue ;; esac
|
|
|
|
if echo "$GONE_FILES" | grep -qxF "$base_file"; then
|
|
continue
|
|
fi
|
|
|
|
BASE_TS=$(extract_ts "$base_file")
|
|
|
|
if [ -n "$BASE_TS" ] && [ "$BASE_TS" -gt "$MAX_BASE_TS" ]; then
|
|
MAX_BASE_TS=$BASE_TS
|
|
fi
|
|
done
|
|
|
|
if [ "$MAX_BASE_TS" -gt 0 ] && [ "$TS" -le "$MAX_BASE_TS" ]; then
|
|
FAILED="$FAILED\n - $file (timestamp $TS <= existing max $MAX_BASE_TS)"
|
|
fi
|
|
done
|
|
|
|
if [ -n "$FAILED" ]; then
|
|
echo "Upgrade command timestamps must be the real epoch millis of when the command"
|
|
echo "is authored (within 60 days before / 2 days after now), and strictly greater"
|
|
echo "than every existing command in the same version directory."
|
|
echo ""
|
|
echo "The upgrade cursor tracks a single last-applied position: a command that sorts"
|
|
echo "before already-applied commands moves the cursor backwards when it runs,"
|
|
echo "re-hiding columns gated by @WasIntroducedInUpgrade on instances that already"
|
|
echo "applied them. Fabricated future timestamps poison the sequence for every"
|
|
echo "command added after them."
|
|
echo ""
|
|
echo "If the blocking existing max is itself a fabricated future timestamp, re-slot"
|
|
echo "that command to its real merge epoch (rename the file and the timestamp in the"
|
|
echo "decorator/constant, and make its up() idempotent since it will re-run under"
|
|
echo "the new name) instead of inflating the new command's timestamp."
|
|
echo -e "$FAILED"
|
|
echo "::error::Upgrade command timestamps must be real (close to now) and append-only within their version directory."
|
|
exit 1
|
|
fi
|