chore(apps): move public apps to packages/twenty-apps/public and generalize CI workflow (#22096)

## What

Introduces a `packages/twenty-apps/public/` folder and moves the
publicly publishable apps into it, then generalizes the apps CI workflow
to cover both folders.

### Moves
The following apps were moved from `packages/twenty-apps/internal/` to
`packages/twenty-apps/public/` (via `git mv`, history preserved):
- `people-data-labs`
- `twenty-discord`
- `twenty-exa`
- `twenty-fireflies`
- `twenty-last-contact`
- `twenty-linear`
- `twenty-meeting-bot`
- `twenty-slack`

These remain in `internal/`: `self-hosting`, `twenty-for-twenty`,
`twenty-partners`.

### Workflow
- Renamed `.github/workflows/ci-internal-apps.yaml` →
`.github/workflows/ci-twenty-apps.yaml`.
- The discover job now scans **both** `packages/twenty-apps/internal`
and `packages/twenty-apps/public`:
  - the "no nested `.github`" guard checks both folders,
  - `changed-files` watches both globs,
- the matrix builder iterates over both roots (guarded with `existsSync`
so a missing folder is a no-op).
- Each matrix entry still carries its own `path`, so the `ci` job works
unchanged regardless of which folder an app lives in.

## Notes
- The apps are standalone packages (own `yarn.lock`, not part of the
root Nx workspaces), so no root `package.json` / `nx.json` / `tsconfig`
changes were needed.
- The companion publish workflow lives in `twentyhq/twenty-infra`
(`publish-internal-apps.yaml` → `publish-public-apps.yaml`) and is
updated in a paired PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Fmu3DWf1yTTkVW49eSkXwh)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22096?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-06-24 17:24:16 +02:00
committed by GitHub
parent 00b7d7c74a
commit c71663946e
841 changed files with 18094 additions and 29 deletions
@@ -1,4 +1,4 @@
name: CI Internal Apps
name: CI Twenty Apps
on:
push:
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
application:
description: 'Internal app folder name to test (e.g. "twenty-linear"). Leave empty to test all apps.'
description: 'App folder name to test (e.g. "twenty-linear"). Leave empty to test all apps.'
required: false
default: ''
type: string
@@ -35,9 +35,9 @@ jobs:
- name: Ensure no .github folder inside app folders
run: |
offenders=$(find packages/twenty-apps/internal -mindepth 2 -maxdepth 2 -type d -name .github)
offenders=$(find packages/twenty-apps/internal packages/twenty-apps/public -mindepth 2 -maxdepth 2 -type d -name .github)
if [ -n "$offenders" ]; then
echo "::error::Internal apps must not define their own .github folder. Offenders:"
echo "::error::Apps must not define their own .github folder. Offenders:"
echo "$offenders"
exit 1
fi
@@ -49,7 +49,9 @@ jobs:
with:
json: true
escape_json: false
files: packages/twenty-apps/internal/**
files: |
packages/twenty-apps/internal/**
packages/twenty-apps/public/**
- name: Build matrix of changed apps
id: set-matrix
@@ -61,36 +63,40 @@ jobs:
node <<'NODE'
const fs = require('fs');
const path = require('path');
const root = 'packages/twenty-apps/internal';
const roots = ['packages/twenty-apps/internal', 'packages/twenty-apps/public'];
const eventName = process.env.EVENT_NAME;
const changedFiles = JSON.parse(process.env.CHANGED_FILES || '[]');
const changedApps = new Set();
for (const file of changedFiles) {
const match = file.match(/^packages\/twenty-apps\/internal\/([^/]+)\//);
const match = file.match(/^packages\/twenty-apps\/(?:internal|public)\/([^/]+)\//);
if (match) changedApps.add(match[1]);
}
const requestedApp = (process.env.APPLICATION_INPUT || '').trim();
const matrix = fs.readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== 'node_modules')
.map((entry) => entry.name)
.filter((name) => fs.existsSync(path.join(root, name, 'package.json')))
.filter((name) => {
if (eventName === 'workflow_dispatch') {
return requestedApp ? name === requestedApp : true;
}
return changedApps.has(name);
})
.map((name) => {
const appPath = path.join(root, name);
const scripts = JSON.parse(fs.readFileSync(path.join(appPath, 'package.json'), 'utf8')).scripts || {};
return {
name,
path: appPath,
hasTypecheck: Boolean(scripts.typecheck),
hasUnit: Boolean(scripts['test:unit']),
hasIntegration: Boolean(scripts.test),
};
});
const matrix = roots
.filter((root) => fs.existsSync(root))
.flatMap((root) =>
fs.readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== 'node_modules')
.map((entry) => entry.name)
.filter((name) => fs.existsSync(path.join(root, name, 'package.json')))
.filter((name) => {
if (eventName === 'workflow_dispatch') {
return requestedApp ? name === requestedApp : true;
}
return changedApps.has(name);
})
.map((name) => {
const appPath = path.join(root, name);
const scripts = JSON.parse(fs.readFileSync(path.join(appPath, 'package.json'), 'utf8')).scripts || {};
return {
name,
path: appPath,
hasTypecheck: Boolean(scripts.typecheck),
hasUnit: Boolean(scripts['test:unit']),
hasIntegration: Boolean(scripts.test),
};
})
);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify(matrix)}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `has_apps=${matrix.length > 0}\n`);
NODE
@@ -150,7 +156,7 @@ jobs:
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
ci-internal-apps-status-check:
ci-twenty-apps-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest