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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/upshift-branding-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
- name: Test the docs importer
run: node --test upshift/docs/test/*.test.mjs

- name: Check the docs site matches Twenty's docs
- name: Check the docs import against this branch's Twenty docs
run: node upshift/docs/import-docs.mjs --check

disable-twenty-workflows:
Expand Down
122 changes: 122 additions & 0 deletions .github/workflows/upshift-docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Upshift Docs

on:
push:
branches: [upshift-main]
paths:
- 'upshift/docs/**'
- 'upshift/branding/**'
- '.github/workflows/upshift-docs.yaml'
pull_request:
branches: [upshift-main]
paths:
- 'upshift/docs/**'
- 'upshift/branding/**'
- '.github/workflows/upshift-docs.yaml'
workflow_run:
workflows: [Upshift Release]
types: [completed]
branches: [upshift-main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: upshift-docs-${{ github.event_name == 'pull_request' && github.ref || 'publish' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
docs:
# A failed release keeps the docs of the previous one
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Check out the Upshift layer
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- name: Find the latest Upshift release
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
tag=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)

if ! [[ "$tag" =~ ^upshift/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::The latest GitHub Release is not an Upshift version: '$tag'"
exit 1
fi

version="${tag#upshift/}"
echo "Upshift release: $version, built on Twenty twenty/$version"
{
echo "version=$version"
echo "app-version=${version#v}"
echo "twenty-tag=twenty/$version"
} >> "$GITHUB_OUTPUT"

- name: Check out the Twenty sources of Upshift ${{ steps.release.outputs.version }}
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
repository: twentyhq/twenty
ref: refs/tags/${{ steps.release.outputs.twenty-tag }}
path: .upshift-docs/twenty
sparse-checkout: |
packages/twenty-docs
packages/twenty-shared/src/constants
packages/twenty-server/src/engine/core-modules/upgrade/constants
filter: blob:none
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24

- name: Test the docs importer
run: node --test upshift/docs/test/*.test.mjs

- name: Build the docs of Upshift ${{ steps.release.outputs.version }}
env:
APP_VERSION: ${{ steps.release.outputs.app-version }}
run: node upshift/docs/import-docs.mjs --root .upshift-docs/twenty --version "$APP_VERSION"

# Mintlify deploys the docs-site branch; pull requests only prove the build
- name: Publish the docs-site branch
if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.release.outputs.version }}
run: |
site="$GITHUB_WORKSPACE/upshift/docs/site"
remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

mkdir -p "$RUNNER_TEMP/docs-site"
cd "$RUNNER_TEMP/docs-site"
git init -q
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'

if git fetch -q --depth 1 "$remote" docs-site 2>/dev/null; then
git checkout -q -b docs-site FETCH_HEAD
git rm -r -q .
else
git checkout -q --orphan docs-site
fi

cp -R "$site"/. .
git add -A

if git diff --cached --quiet; then
echo "The docs-site branch already carries Upshift $VERSION from this layer."
exit 0
fi

git commit -q -m "Docs of Upshift $VERSION from layer ${GITHUB_SHA::10}"
git push -q "$remote" HEAD:docs-site
echo "Published the docs of Upshift $VERSION to the docs-site branch."
19 changes: 11 additions & 8 deletions upshift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ upshift/
.env.example
docs/
rules.mjs what is published and how it is rebranded
import-docs.mjs builds docs/site from packages/twenty-docs
site/ generated Mintlify site, deployed to docs.upshiftcloud.com
import-docs.mjs builds docs/site from a Twenty checkout
site/ generated Mintlify site, not committed: the Upshift Docs workflow
builds it from the latest Upshift release and publishes the docs-site branch
```

## How a release is built
Expand Down Expand Up @@ -102,18 +103,20 @@ docker compose up -d --remove-orphans

- `docs/rules.mjs` says what is published and how it is rebranded: the Getting Started and User Guide tabs in French (the default, served at the root) and English (under `/en/`), without Twenty's billing, legal, cloud migration and partner pages, with Twenty's links and addresses pointed at Upshift. Pages those tabs link to but that Twenty keeps out of its navigation are published too, without a navigation entry. French links to a page Twenty has not translated yet go to its English version. Upshift covers the whole chain from first contact to reporting, so the rules also reword prose that calls Upshift or its data "CRM"; "CRM" stays only for other products, migrations and the prospecting step.
- `docs/overrides/` holds pages written for Upshift, such as "Pourquoi Upshift" (`getting-started/introduction.mdx`, and its English version under `en/`), which replace Twenty's version of the same page, and the React snippets those pages import from `snippets/`, such as the interactive process map (`snippets/process-map.jsx`, styled in `docs/upshift.css`).
- `docs/import-docs.mjs` applies the rules and overrides and writes `docs/site/`, the folder Mintlify deploys from `upshift-main`. The navigation carries a single version, `vX.Y.Z` from the server's `TWENTY_CURRENT_VERSION`, so readers see which release the docs describe. It moves with each Twenty bump when the site is regenerated. Never edit `docs/site/` by hand or in the Mintlify web editor: the next import replaces it. Change the rules or add an override instead.
- `docs/import-docs.mjs` applies the rules and overrides to a Twenty checkout and writes `docs/site/`. `--root <checkout>` says which Twenty sources to read (this repository by default), `--version X.Y.Z` names the Upshift release the site documents and fails when the checkout carries another version, and `--check` builds without writing. The navigation carries a single version, `vX.Y.Z` from the server's `TWENTY_CURRENT_VERSION` of that checkout, so readers see which release the docs describe.

The site is not committed. The [Upshift Docs](../.github/workflows/upshift-docs.yaml) workflow builds it from the latest Upshift release: it takes the newest `upshift/vX.Y.Z` GitHub Release, checks out Twenty at the matching `twenty/vX.Y.Z` tag, applies the rules and overrides of `upshift-main`, and publishes the result to the `docs-site` branch, which Mintlify deploys to docs.upshiftcloud.com. It runs after every Upshift Release run, on every push to `upshift-main` that touches `docs/` or `branding/`, and on demand. Pull requests that touch those folders run the same build without publishing. Never edit the `docs-site` branch by hand or in the Mintlify web editor: the next run replaces it. Change the rules or add an override instead.

```bash
node upshift/docs/import-docs.mjs
```

The import fails when a sentence a rule expects has changed in Twenty's docs, when an excluded section no longer matches any Twenty page (so a moved legal or billing page cannot slip through), when a page still references a Twenty address or links to a page that is not published, when a page presents Upshift as a CRM (add a term rule, or an allowed context in `CRM_ALLOWED_CONTEXTS`), or when an override no longer matches a published page or no page imports an override snippet. Update the rule's text or `count` in `rules.mjs` and run it again. The Upshift Branding Check runs the importer's tests and fails while `docs/site/` differs from what the import produces.
The import fails when a sentence a rule expects has changed in Twenty's docs, when an excluded section no longer matches any Twenty page (so a moved legal or billing page cannot slip through), when a page still references a Twenty address or links to a page that is not published, when a page presents Upshift as a CRM (add a term rule, or an allowed context in `CRM_ALLOWED_CONTEXTS`), or when an override no longer matches a published page or no page imports an override snippet. Update the rule's text or `count` in `rules.mjs` and run it again. The Upshift Branding Check runs the importer's tests and a `--check` build against this branch's Twenty docs, so a Twenty change that breaks a rule shows up before the next release. The Upshift Docs workflow runs the build against the released docs.

To preview the site locally:
To preview the site locally, build it from this checkout, then serve it:

```bash
cd upshift/docs/site && npx --yes mintlify@4.2.956 dev
node upshift/docs/import-docs.mjs && cd upshift/docs/site && npx --yes mintlify@4.2.956 dev
```

## Syncing Twenty into upshift-main
Expand All @@ -132,15 +135,15 @@ git switch main && git pull --ff-only
git switch -c sync/twenty-$(date +%Y-%m-%d) upshift-main && git merge main
```

Then refresh the docs in the same branch with `node upshift/docs/import-docs.mjs` and commit `upshift/docs/site` if it changed.
Then run `node upshift/docs/import-docs.mjs --check` in the same branch: it fails when Twenty's docs changed in a way the rules no longer match.

Open a PR into `upshift-main`. The merge has no conflicts because Upshift only adds files. The [Upshift Branding Check](../.github/workflows/upshift-branding-check.yaml) workflow applies the layer to the PR and to the latest Twenty release, so you learn about broken patch points before the next release.

`upshift-main` modifies none of Twenty's files, so syncing never conflicts. The only path both sides could create is `.github/README.md`; if Twenty ever adds one, keep ours.

## GitHub Actions

Only `upshift-release.yaml` and `upshift-branding-check.yaml` run. Twenty's own workflows test, deploy and translate Twenty itself (several need Twenty's private secrets), so they are disabled in the repository settings. When a sync brings a new Twenty workflow, the `disable-twenty-workflows` job of the branding check disables it on the next push to `upshift-main`. It can run once on that same push before being disabled.
Only `upshift-release.yaml`, `upshift-branding-check.yaml` and `upshift-docs.yaml` run. Twenty's own workflows test, deploy and translate Twenty itself (several need Twenty's private secrets), so they are disabled in the repository settings. When a sync brings a new Twenty workflow, the `disable-twenty-workflows` job of the branding check disables it on the next push to `upshift-main`. It can run once on that same push before being disabled.

To turn one of Twenty's workflows back on:

Expand Down
1 change: 1 addition & 0 deletions upshift/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
site/
76 changes: 39 additions & 37 deletions upshift/docs/import-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export const TWENTY_DOCS_ROOT = path.join(
'packages/twenty-docs',
);

export const buildSite = (twentyDocsRoot) => {
export const buildSite = (
twentyDocsRoot,
{ appRoot = LAYER_REPOSITORY_ROOT, releaseVersion } = {},
) => {
const readTwenty = (relativePath) =>
readFileSync(path.join(twentyDocsRoot, relativePath));
const twentyDocsConfig = JSON.parse(readTwenty('docs.json').toString());
Expand Down Expand Up @@ -162,7 +165,7 @@ export const buildSite = (twentyDocsRoot) => {
const readBranding = (fileName) =>
readFileSync(path.join(BRANDING_ROOT, fileName));
const readApp = (relativePath) =>
readFileSync(path.join(LAYER_REPOSITORY_ROOT, relativePath), 'utf8');
readFileSync(path.join(appRoot, relativePath), 'utf8');
const appLanguages = [
...readApp(APP_DOCUMENTATION_LANGUAGES).matchAll(/'([a-z]{2})'/g),
].map(([, code]) => code);
Expand All @@ -175,15 +178,19 @@ export const buildSite = (twentyDocsRoot) => {
);
}

const [, releaseVersion] =
const [, sourceVersion] =
readApp(APP_RELEASE_VERSION).match(
/TWENTY_CURRENT_VERSION = '(\d+\.\d+\.\d+)'/,
) ?? [];

if (releaseVersion === undefined) {
if (sourceVersion === undefined) {
problems.push(
`cannot read the release version from ${APP_RELEASE_VERSION}`,
);
} else if (releaseVersion !== undefined && releaseVersion !== sourceVersion) {
problems.push(
`the docs are built for Upshift v${releaseVersion} but the sources carry ${sourceVersion}`,
);
}

const redirects = [
Expand All @@ -200,7 +207,7 @@ export const buildSite = (twentyDocsRoot) => {
files.set('favicon.svg', readBranding('logo.svg'));
files.set(
'docs.json',
`${JSON.stringify(buildDocsConfig(twentyDocsConfig, navigation, redirects, releaseVersion), null, 2)}\n`,
`${JSON.stringify(buildDocsConfig(twentyDocsConfig, navigation, redirects, sourceVersion), null, 2)}\n`,
);

problems.push(...auditSite(files, sitePages));
Expand Down Expand Up @@ -233,24 +240,6 @@ const listSiteFiles = (directory, prefix = '') =>
})
: [];

export const findDrift = (files, siteRoot) => {
const existing = new Set(listSiteFiles(siteRoot));

const changed = [...files]
.filter(
([relativePath, content]) =>
!existing.has(relativePath) ||
!readFileSync(path.join(siteRoot, relativePath)).equals(
Buffer.from(content),
),
)
.map(([relativePath]) => relativePath);

const extra = [...existing].filter((relativePath) => !files.has(relativePath));

return { changed, extra };
};

const writeSite = (files, siteRoot) => {
rmSync(siteRoot, { recursive: true, force: true });

Expand All @@ -262,8 +251,30 @@ const writeSite = (files, siteRoot) => {
}
};

export const parseArguments = (argv) => {
const valueOf = (flag) => {
const index = argv.indexOf(flag);

return index === -1 ? undefined : argv[index + 1];
};

return {
twentyRoot: valueOf('--root'),
releaseVersion: valueOf('--version'),
check: argv.includes('--check'),
};
};

const main = () => {
const { files, problems, summary } = buildSite(TWENTY_DOCS_ROOT);
const { twentyRoot, releaseVersion, check } = parseArguments(
process.argv.slice(2),
);
const appRoot =
twentyRoot === undefined ? LAYER_REPOSITORY_ROOT : path.resolve(twentyRoot);
const { files, problems, summary } = buildSite(
path.join(appRoot, 'packages/twenty-docs'),
{ appRoot, releaseVersion },
);

if (problems.length > 0) {
console.error(
Expand All @@ -272,25 +283,16 @@ const main = () => {
process.exit(1);
}

if (process.argv.includes('--check')) {
const { changed, extra } = findDrift(files, SITE_ROOT);
const built = `${summary.pages} pages, ${summary.images} images, ${summary.snippets} snippets, ${summary.redirects} redirects`;

if (changed.length > 0 || extra.length > 0) {
console.error(
`upshift/docs/site is out of date (${changed.length} changed or missing, ${extra.length} extra). Run node upshift/docs/import-docs.mjs and commit the result.`,
);
process.exit(1);
}

console.log('upshift/docs/site matches packages/twenty-docs.');
if (check) {
console.log(`The docs import works: ${built}.`);

return;
}

writeSite(files, SITE_ROOT);
console.log(
`Wrote upshift/docs/site: ${summary.pages} pages, ${summary.images} images, ${summary.snippets} snippets, ${summary.redirects} redirects.`,
);
console.log(`Wrote upshift/docs/site: ${built}.`);
};

if (process.argv[1] === fileURLToPath(import.meta.url)) {
Expand Down
Loading
Loading