snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
name: Deploy Twenty App
description: Build and deploy a Twenty app to a remote instance
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target instance
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY, accessToken: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:publish --private --remote target

View File

@@ -0,0 +1,53 @@
name: Install Twenty App
description: Install (or upgrade) a Twenty app on a specific workspace
inputs:
api-url:
description: Base URL of the target Twenty instance (e.g. https://my.twenty.instance)
required: true
api-key:
description: API key or access token for the target workspace
required: true
app-path:
description: Path to the app directory (relative to repo root). Defaults to repo root.
required: false
default: '.'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Configure remote
shell: bash
run: |
mkdir -p ~/.twenty
node -e "
const fs = require('fs'), path = require('path'), os = require('os');
fs.writeFileSync(path.join(os.homedir(), '.twenty', 'config.json'), JSON.stringify({
version: 1,
remotes: { target: { apiUrl: process.env.API_URL, apiKey: process.env.API_KEY, accessToken: process.env.API_KEY } }
}, null, 2));
"
env:
API_URL: ${{ inputs.api-url }}
API_KEY: ${{ inputs.api-key }}
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:install --remote target

39
.github/actions/nx-affected/action.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Nx Affected CI
inputs:
parallel:
required: false
default: '3'
tag:
required: false
tasks:
required: true
configuration:
required: false
default: 'ci'
args:
required: false
runs:
using: "composite"
steps:
- name: Fetch main branch for diff
shell: bash
run: git fetch origin main --depth=1
- name: Get last successful commit
if: env.NX_BASE == ''
uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4
- name: Fallback to origin/main if no base found
if: env.NX_BASE == ''
shell: bash
run: echo "NX_BASE=$(git rev-parse origin/main)" >> $GITHUB_ENV
- name: Run affected command
shell: bash
env:
NX_CONFIGURATION: ${{ inputs.configuration }}
NX_TASKS: ${{ inputs.tasks }}
NX_PARALLEL: ${{ inputs.parallel }}
NX_TAG: ${{ inputs.tag }}
NX_ARGS: ${{ inputs.args }}
# Nx buffers task output into an internal stream by default, so a task
# that gets killed takes its buffer with it: the log ends on whatever last
# happened to flush and the stack trace is never printed.
run: npx nx affected --nxBail --outputStyle=stream --configuration="$NX_CONFIGURATION" -t="$NX_TASKS" --parallel="$NX_PARALLEL" --exclude="*,!tag:$NX_TAG" $NX_ARGS

View File

@@ -0,0 +1,38 @@
name: Restore cache
inputs:
key:
required: true
description: Prefix to the cache key
additional-paths:
required: false
outputs:
cache-primary-key:
description: actions/cache/restore cache-primary-key outputs proxy
value: ${{ steps.restore-cache.outputs.cache-primary-key }}
cache-hit:
description: String bool indicating whether cache has been directly or indirectly hit
value: ${{ steps.restore-cache.outputs.cache-hit == 'true' || steps.restore-cache.outputs.cache-matched-key != '' }}
runs:
using: composite
steps:
- name: Cache primary key builder
id: cache-primary-key-builder
shell: bash
env:
CACHE_KEY: ${{ inputs.key }}
REF_NAME: ${{ github.ref_name }}
run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
- name: Restore cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
id: restore-cache
with:
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
restore-keys: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-
path: |
.cache
.nx
node_modules/.cache
packages/*/node_modules/.cache
${{ inputs.additional-paths }}

24
.github/actions/save-cache/action.yaml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Save cache
inputs:
key:
required: true
description: Primary key to the cache, should be retrieved from `cache-restore` composite action outputs.
additional-paths:
required: false
runs:
using: "composite"
steps:
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache
if: ${{ format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with:
key: ${{ inputs.key }}
path: |
.cache
.nx
node_modules/.cache
packages/*/node_modules/.cache
${{ inputs.additional-paths }}

View File

@@ -0,0 +1,50 @@
name: Spawn Twenty App Dev Test
description: >
Starts a Twenty all-in-one test instance (server, worker, database, redis)
using the twentycrm/twenty-app-dev Docker image on port 2021.
The server is available at http://localhost:2021 with seeded demo data.
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag for twenty-app-dev (e.g., "latest" or "v1.20.0").'
required: false
default: 'latest'
outputs:
server-url:
description: 'URL where the Twenty test server can be reached'
value: http://localhost:2021
api-key:
description: 'ACCESS token for the seeded Twenty dev admin (Tim)'
value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ1c2VySWQiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDB9.Ow8z-Dptg4LDiO2nApu5UQcX2a-gXdu_McalWs7Pgt8
runs:
using: 'composite'
steps:
- name: Start twenty-app-dev-test container
shell: bash
run: |
docker run -d \
--name twenty-app-dev-test \
-p 2021:2021 \
-e NODE_PORT=2021 \
-e SERVER_URL=http://localhost:2021 \
-e MARKETPLACE_CATALOG_SYNC_CRON_ENABLED=false \
-e API_RATE_LIMITING_SHORT_LIMIT=100000 \
-e API_RATE_LIMITING_LONG_LIMIT=100000 \
twentycrm/twenty-app-dev:${{ inputs.twenty-version }}
echo "Waiting for Twenty test instance to become healthy…"
TIMEOUT=180
ELAPSED=0
until curl -sf http://localhost:2021/healthz > /dev/null 2>&1; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "::error::Twenty did not become healthy within ${TIMEOUT}s"
docker logs twenty-app-dev-test 2>&1 | tail -80
exit 1
fi
sleep 3
ELAPSED=$((ELAPSED + 3))
echo " … waited ${ELAPSED}s"
done
echo "Twenty test instance is ready at http://localhost:2021 (took ~${ELAPSED}s)"

View File

@@ -0,0 +1,88 @@
name: Spawn Twenty Docker Image
description: >
Starts a full Twenty instance (server, worker, database, redis) using Docker
Compose. The server is available at http://localhost:3000 for subsequent steps
in the caller's job.
Accepts "latest" (pulls the latest Docker Hub image, checks out main) or a
semver tag (e.g., v0.40.0).
Designed to be consumed from external repositories (e.g., twenty-app).
inputs:
twenty-version:
description: 'Twenty Docker Hub image tag — either "latest" or a semver tag (e.g., v0.40.0).'
required: true
twenty-repository:
description: 'Twenty repository to checkout docker compose files from.'
required: false
default: 'twentyhq/twenty'
github-token:
description: 'GitHub token for cross-repo checkout. Required when calling from an external repository.'
required: false
default: ${{ github.token }}
outputs:
server-url:
description: 'URL where the Twenty server can be reached'
value: http://localhost:3000
access-token:
description: 'Admin access token for the Twenty instance'
value: ${{ steps.admin-token.outputs.access-token }}
runs:
using: 'composite'
steps:
- name: Resolve version
id: resolve
shell: bash
run: |
VERSION="${{ inputs.twenty-version }}"
if [ "$VERSION" = "latest" ]; then
echo "docker-tag=latest" >> "$GITHUB_OUTPUT"
echo "git-ref=main" >> "$GITHUB_OUTPUT"
elif echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "docker-tag=$VERSION" >> "$GITHUB_OUTPUT"
echo "git-ref=$VERSION" >> "$GITHUB_OUTPUT"
else
echo "::error::twenty-version must be \"latest\" or a semver tag (e.g., v0.40.0). Got: '$VERSION'"
exit 1
fi
- name: Checkout docker compose files
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
repository: ${{ inputs.twenty-repository }}
ref: ${{ steps.resolve.outputs.git-ref }}
token: ${{ inputs.github-token }}
sparse-checkout: |
packages/twenty-docker
sparse-checkout-cone-mode: false
path: .twenty-spawn
- name: Prepare environment
shell: bash
working-directory: ./.twenty-spawn/packages/twenty-docker
run: |
cp .env.example .env
echo "" >> .env
echo "TAG=${{ steps.resolve.outputs.docker-tag }}" >> .env
echo "APP_SECRET=replace_me_with_a_random_string" >> .env
echo "SERVER_URL=http://localhost:3000" >> .env
- name: Start Twenty instance
shell: bash
working-directory: ./.twenty-spawn/packages/twenty-docker
run: |
docker compose up -d --wait || {
echo "::error::Docker compose failed to start or health checks timed out"
docker compose logs
exit 1
}
echo "Twenty instance is ready at http://localhost:3000"
- name: Set admin access token
id: admin-token
shell: bash
run: |
ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik"
echo "::add-mask::$ACCESS_TOKEN"
echo "access-token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT"

View File

@@ -0,0 +1,124 @@
name: Spawn Twenty Server
description: >
Provisions a running Twenty server and returns its URL + API key, from one of
two sources:
- dockerhub-latest: the latest published twentycrm/twenty-app-dev image
(delegates to spawn-twenty-app-dev-test, port 2021).
- local: a server built from the monorepo's current files (port 3000). It
brings up its own postgres + redis via docker, so it works inside any job
without declaring job-level services.
Both sources expose the same server-url / api-key contract so callers never
branch on how the server came up.
inputs:
source:
description: 'Where the server comes from: "dockerhub-latest" or "local".'
required: false
default: 'local'
twenty-version:
description: 'Docker Hub image tag, only used when source is "dockerhub-latest".'
required: false
default: 'latest'
server-build-cache-key:
description: 'Cache key used to speed up the local server build (nx cache).'
required: false
default: 'server-build'
outputs:
server-url:
description: 'URL where the Twenty server can be reached'
value: ${{ steps.resolve.outputs.server-url }}
api-key:
description: 'API key (or access token) for the seeded workspace'
value: ${{ steps.resolve.outputs.api-key }}
runs:
using: 'composite'
steps:
- name: Spawn from Docker Hub image
id: dockerhub
if: inputs.source == 'dockerhub-latest'
uses: ./.github/actions/spawn-twenty-app-dev-test
with:
twenty-version: ${{ inputs.twenty-version }}
- name: Start postgres and redis
if: inputs.source == 'local'
shell: bash
run: |
docker run -d --name twenty-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
postgres:18
docker run -d --name twenty-redis -p 6379:6379 redis
echo "Waiting for postgres…"
for i in {1..30}; do
if PGPASSWORD=postgres pg_isready -h localhost -p 5432 -U postgres > /dev/null 2>&1; then
echo "Postgres ready"
break
fi
sleep 2
if [ "$i" -eq 30 ]; then
echo "::error::Postgres did not become ready in time"
docker logs twenty-postgres 2>&1 | tail -40
exit 1
fi
done
- name: Install dependencies
if: inputs.source == 'local'
uses: ./.github/actions/yarn-install
- name: Restore server build cache
if: inputs.source == 'local'
uses: ./.github/actions/restore-cache
with:
key: ${{ inputs.server-build-cache-key }}
- name: Build server from monorepo
if: inputs.source == 'local'
shell: bash
run: |
npx nx build twenty-shared
npx nx build twenty-server
- name: Write server env and create databases
if: inputs.source == 'local'
shell: bash
run: |
npx nx reset:env:e2e-testing-server twenty-server
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:reset
- name: Start server
if: inputs.source == 'local'
shell: bash
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
if: inputs.source == 'local'
shell: bash
run: npx wait-on http://localhost:3000/healthz --timeout 180000 --interval 1000
- name: Resolve server url and api key
id: resolve
shell: bash
env:
SOURCE: ${{ inputs.source }}
DOCKERHUB_URL: ${{ steps.dockerhub.outputs.server-url }}
DOCKERHUB_KEY: ${{ steps.dockerhub.outputs.api-key }}
LOCAL_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
run: |
if [ "$SOURCE" = "dockerhub-latest" ]; then
echo "server-url=$DOCKERHUB_URL" >> "$GITHUB_OUTPUT"
echo "api-key=$DOCKERHUB_KEY" >> "$GITHUB_OUTPUT"
elif [ "$SOURCE" = "local" ]; then
echo "server-url=http://localhost:3000" >> "$GITHUB_OUTPUT"
echo "api-key=$LOCAL_KEY" >> "$GITHUB_OUTPUT"
else
echo "::error::Unknown source '$SOURCE' (expected 'dockerhub-latest' or 'local')"
exit 1
fi

View File

@@ -0,0 +1,85 @@
name: Test Twenty App
description: >
Exercises a single Twenty app against an already-running server. In
"installation-and-integration-test" mode it runs the app's integration suite,
then deploys and installs the app. In "installation-only" mode it only deploys
and installs, which is enough to catch server-side install regressions without
paying for each app's full test suite.
inputs:
api-url:
description: Base URL of the target Twenty instance
required: true
api-key:
description: API key or access token for the target workspace
required: true
app-path:
description: Path to the app directory (relative to repo root)
required: true
mode:
description: >
What to run: "installation-and-integration-test" (integration + deploy +
install), "integration-test-only" (integration suite only) or
"installation-only" (deploy + install only).
required: false
default: 'installation-and-integration-test'
runs:
using: composite
steps:
- name: Validate mode
shell: bash
env:
MODE: ${{ inputs.mode }}
run: |
case "$MODE" in
installation-and-integration-test|integration-test-only|installation-only) ;;
*)
echo "::error::Unknown mode '$MODE' (expected 'installation-and-integration-test', 'integration-test-only' or 'installation-only')"
exit 1
;;
esac
- name: Enable Corepack
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
shell: bash
run: corepack enable
- name: Setup Node.js
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
cache-dependency-path: '${{ inputs.app-path }}/yarn.lock'
- name: Install dependencies
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn install --immutable
- name: Integration tests
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'integration-test-only'
shell: bash
working-directory: ${{ inputs.app-path }}
env:
TWENTY_API_URL: ${{ inputs.api-url }}
TWENTY_API_KEY: ${{ inputs.api-key }}
run: yarn test
- name: Deploy app
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'installation-only'
uses: ./.github/actions/deploy-twenty-app
with:
api-url: ${{ inputs.api-url }}
api-key: ${{ inputs.api-key }}
app-path: ${{ inputs.app-path }}
- name: Install app
if: inputs.mode == 'installation-and-integration-test' || inputs.mode == 'installation-only'
uses: ./.github/actions/install-twenty-app
with:
api-url: ${{ inputs.api-url }}
api-key: ${{ inputs.api-key }}
app-path: ${{ inputs.app-path }}

View File

@@ -0,0 +1,179 @@
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

View File

@@ -0,0 +1,56 @@
name: Yarn Install
inputs:
node-version:
required: false
default: '24'
runs:
using: 'composite'
steps:
- name: Free disk space for install
if: runner.os == 'Linux'
shell: bash
run: |
# Default GitHub images ship large SDKs this repo does not use; removing
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h
- name: Cache primary key builder
id: globals
shell: bash
run: |
echo "ACTION_SHELL=bash" >> "${GITHUB_OUTPUT}"
echo "CACHE_KEY_PREFIX=node_modules-cache-node-${{ inputs.node-version }}-${{ hashFiles('yarn.lock') }}" >> "${GITHUB_OUTPUT}"
echo 'PATH_TO_CACHE<<EOF' >> $GITHUB_OUTPUT
echo "node_modules" >> $GITHUB_OUTPUT
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: Setup Node.js and get yarn cache
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
- name: Restore node_modules
id: cache-node-modules
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
with:
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
- name: Install Dependencies
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
run: |
yarn config set enableHardenedMode true
yarn config set enableScripts false
yarn --immutable --check-cache
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' && format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with:
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}