Skip to content

Commit ef8a915

Browse files
authored
fix(cli): ensure network before stateful deploy, move convex to stateful compose, add convex to CI (#1526)
1 parent cd035a9 commit ef8a915

24 files changed

Lines changed: 248 additions & 104 deletions

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
- 'services/db/**'
6666
proxy:
6767
- 'services/proxy/**'
68+
convex:
69+
- 'services/convex/**'
6870
ci_tests:
6971
- 'tests/container-*'
7072
- 'compose.test.yml'
@@ -104,6 +106,8 @@ jobs:
104106
context: '.'
105107
- service: proxy
106108
context: '.'
109+
- service: convex
110+
context: '.'
107111

108112
steps:
109113
- name: Checkout repository
@@ -243,6 +247,8 @@ jobs:
243247
context: '.'
244248
- service: proxy
245249
context: '.'
250+
- service: convex
251+
context: '.'
246252

247253
steps:
248254
- name: Checkout repository

.github/workflows/release.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
run: |
6262
VERSION="${{ needs.prepare.outputs.version_number }}"
6363
REGISTRY="${{ env.REGISTRY }}/${{ github.repository }}"
64-
for svc in platform rag crawler db proxy; do
64+
for svc in platform rag crawler db proxy convex; do
6565
IMAGE="${REGISTRY}/tale-${svc}:${VERSION}"
6666
echo "Verifying manifest: ${IMAGE}"
6767
docker manifest inspect "${IMAGE}" > /dev/null 2>&1 || {
@@ -103,6 +103,8 @@ jobs:
103103
context: '.'
104104
- name: proxy
105105
context: '.'
106+
- name: convex
107+
context: '.'
106108
arch:
107109
- name: amd64
108110
runner: ubuntu-latest
@@ -116,7 +118,7 @@ jobs:
116118
uses: actions/checkout@v6
117119

118120
- name: Free disk space
119-
if: matrix.service.name == 'platform' || matrix.service.name == 'rag' || matrix.service.name == 'crawler'
121+
if: matrix.service.name == 'platform' || matrix.service.name == 'rag' || matrix.service.name == 'crawler' || matrix.service.name == 'convex'
120122
run: |
121123
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
122124
sudo docker image prune -af
@@ -188,7 +190,7 @@ jobs:
188190
run: |
189191
VERSION="${{ needs.prepare.outputs.version_number }}"
190192
ARCH="amd64"
191-
for svc in platform rag crawler db proxy; do
193+
for svc in platform rag crawler db proxy convex; do
192194
IMAGE="${{ env.REGISTRY }}/${{ github.repository }}/tale-${svc}:${VERSION}-${ARCH}"
193195
echo "Pulling ${IMAGE}..."
194196
docker pull "${IMAGE}"
@@ -226,7 +228,7 @@ jobs:
226228

227229
strategy:
228230
matrix:
229-
service: [platform, rag, crawler, db, proxy]
231+
service: [platform, rag, crawler, db, proxy, convex]
230232

231233
steps:
232234
- name: Log in to GHCR
@@ -288,7 +290,7 @@ jobs:
288290
run: |
289291
echo "## Release ${{ needs.prepare.outputs.version }} Complete" >> $GITHUB_STEP_SUMMARY
290292
echo "" >> $GITHUB_STEP_SUMMARY
291-
echo "All 5 service images have been built, tested, and pushed to GHCR (native amd64 + arm64)." >> $GITHUB_STEP_SUMMARY
293+
echo "All 6 service images have been built, tested, and pushed to GHCR (native amd64 + arm64)." >> $GITHUB_STEP_SUMMARY
292294
echo "" >> $GITHUB_STEP_SUMMARY
293295
echo "### Images" >> $GITHUB_STEP_SUMMARY
294296
echo "" >> $GITHUB_STEP_SUMMARY
@@ -299,6 +301,7 @@ jobs:
299301
echo "| Crawler | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-crawler:${{ needs.prepare.outputs.version_number }}\` |" >> $GITHUB_STEP_SUMMARY
300302
echo "| DB | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-db:${{ needs.prepare.outputs.version_number }}\` |" >> $GITHUB_STEP_SUMMARY
301303
echo "| Proxy | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-proxy:${{ needs.prepare.outputs.version_number }}\` |" >> $GITHUB_STEP_SUMMARY
304+
echo "| Convex | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-convex:${{ needs.prepare.outputs.version_number }}\` |" >> $GITHUB_STEP_SUMMARY
302305
echo "" >> $GITHUB_STEP_SUMMARY
303306
echo "### CLI" >> $GITHUB_STEP_SUMMARY
304307
echo "" >> $GITHUB_STEP_SUMMARY

tests/container-image-test.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ declare -A SIZE_BUDGETS=(
3535
[platform]=2900
3636
[db]=1200
3737
[proxy]=100
38+
[convex]=2500
3839
)
3940

4041
header() {
@@ -75,7 +76,7 @@ get_image() {
7576
cd "${PROJECT_ROOT}"
7677
header "Building all images locally"
7778

78-
SERVICES=(crawler rag platform db proxy)
79+
SERVICES=(crawler rag platform db proxy convex)
7980
declare -A IMAGES
8081

8182
echo -e " ${YELLOW}Building images using compose...${NC}"
@@ -126,9 +127,9 @@ for svc in "${SERVICES[@]}"; do
126127
# Platform runs as root initially, then drops to 'app' user via gosu in entrypoint
127128
pass "${svc}: root (expected — gosu to app at runtime)"
128129
;;
129-
db)
130-
# DB runs as root initially, then gosu to postgres — this is expected
131-
pass "${svc}: root (expected — gosu to postgres at runtime)"
130+
db|convex)
131+
# DB/Convex run as root initially, then drop privileges via gosu in entrypoint
132+
pass "${svc}: root (expected — gosu to app/postgres at runtime)"
132133
;;
133134
proxy)
134135
# Caddy Alpine image — non-root not required

tools/cli/src/commands/deploy/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { deploy } from '../../lib/actions/deploy';
44
import {
55
type ServiceName,
66
ALL_SERVICES,
7+
STATEFUL_SERVICES,
78
isValidService,
89
} from '../../lib/compose/types';
910
import { ensureEnv } from '../../lib/config/ensure-env';
@@ -17,7 +18,11 @@ export function createDeployCommand(): Command {
1718
return new Command('deploy')
1819
.description('Deploy a version to the environment')
1920
.argument('[version]', 'Version to deploy (e.g., v1.0.0 or 1.0.0)')
20-
.option('-a, --all', 'Also update infrastructure (db, proxy)', false)
21+
.option(
22+
'-a, --all',
23+
`Also update infrastructure (${STATEFUL_SERVICES.join(', ')})`,
24+
false,
25+
)
2126
.option(
2227
'-s, --services <list>',
2328
`Specific services to update (comma-separated: ${ALL_SERVICES.join(',')})`,

tools/cli/src/commands/logs.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command } from 'commander';
22

33
import { logs } from '../lib/actions/logs';
4+
import { ALL_SERVICES } from '../lib/compose/types';
45
import { requireProject } from '../lib/project/find-project';
56
import { resolveProjectContext } from '../lib/project/project-context';
67
import { loadEnv } from '../utils/load-env';
@@ -9,10 +10,7 @@ import * as logger from '../utils/logger';
910
export function createLogsCommand(): Command {
1011
return new Command('logs')
1112
.description('View logs from a service')
12-
.argument(
13-
'<service>',
14-
'Service name (platform, rag, crawler, search, db, proxy)',
15-
)
13+
.argument('<service>', `Service name (${ALL_SERVICES.join(', ')})`)
1614
.option('-c, --color <color>', 'Deployment color (blue or green)')
1715
.option('-f, --follow', 'Follow log output', false)
1816
.option('--since <duration>', 'Show logs since duration (e.g., 1h, 30m)')

tools/cli/src/commands/reset.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command } from 'commander';
22

33
import { reset } from '../lib/actions/reset';
4+
import { STATEFUL_SERVICES } from '../lib/compose/types';
45
import { ensureEnv } from '../lib/config/ensure-env';
56
import { requireProject } from '../lib/project/find-project';
67
import { resolveProjectContext } from '../lib/project/project-context';
@@ -11,7 +12,11 @@ export function createResetCommand(): Command {
1112
return new Command('reset')
1213
.description('Remove ALL blue-green containers')
1314
.option('-f, --force', 'Skip confirmation prompt', false)
14-
.option('-a, --all', 'Also remove infrastructure (db, proxy)', false)
15+
.option(
16+
'-a, --all',
17+
`Also remove infrastructure (${STATEFUL_SERVICES.join(', ')})`,
18+
false,
19+
)
1520
.option('--dry-run', 'Preview reset without making changes', false)
1621
.action(async (options) => {
1722
try {

tools/cli/src/lib/actions/deploy.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join } from 'node:path';
33

44
import { getProjectId, type DeploymentEnv } from '../../utils/load-env';
55
import * as logger from '../../utils/logger';
6+
import { REQUIRED_VOLUMES } from '../compose/generators/constants';
67
import { generateColorCompose } from '../compose/generators/generate-color-compose';
78
import { generateStatefulCompose } from '../compose/generators/generate-stateful-compose';
89
import {
@@ -32,17 +33,6 @@ import { withLock } from '../state/with-lock';
3233
import { MIGRATIONS } from '../upgrade/registry';
3334
import { runPendingMigrations } from '../upgrade/runner';
3435

35-
const REQUIRED_VOLUMES = [
36-
// platform-data is kept for upgrade scenarios where split-convex migrates
37-
// its contents into convex-data; on fresh installs it is an unused empty
38-
// volume. Removing it would break detect() for pre-0.3.0 deployments.
39-
'platform-data',
40-
'convex-data',
41-
'caddy-data',
42-
'rag-data',
43-
'crawler-data',
44-
];
45-
4636
async function ensureInfrastructure(
4737
prefix: string,
4838
dryRun: boolean,
@@ -56,7 +46,7 @@ async function ensureInfrastructure(
5646
return;
5747
}
5848

59-
const volumesCreated = await ensureVolumes(REQUIRED_VOLUMES);
49+
const volumesCreated = await ensureVolumes([...REQUIRED_VOLUMES]);
6050
if (!volumesCreated) {
6151
throw new Error('Failed to create required volumes');
6252
}
@@ -273,12 +263,18 @@ export async function deploy(options: DeployOptions): Promise<void> {
273263
}
274264
}
275265

266+
// Must run AFTER migrations (which may `docker compose down`, removing
267+
// networks) and BEFORE any `docker compose up` for stateful or rotatable
268+
// services.
269+
await ensureInfrastructure(prefix, dryRun);
270+
276271
// Deploy stateful services if any
277272
if (statefulToUpdate.length > 0) {
278273
logger.step(`${prefix}Deploying stateful services...`);
279274
const statefulCompose = generateStatefulCompose(
280275
serviceConfig,
281276
hostAlias,
277+
{ fresh: options.fresh },
282278
);
283279

284280
if (dryRun) {
@@ -341,16 +337,11 @@ export async function deploy(options: DeployOptions): Promise<void> {
341337
}
342338
}
343339

344-
await ensureInfrastructure(prefix, dryRun);
345-
346340
// Update services in current color
347341
logger.step(`${prefix}Updating ${currentColor} services...`);
348342
const colorCompose = generateColorCompose(
349343
serviceConfig,
350344
currentColor,
351-
{
352-
fresh: options.fresh,
353-
},
354345
);
355346

356347
if (dryRun) {
@@ -420,13 +411,9 @@ export async function deploy(options: DeployOptions): Promise<void> {
420411
}
421412
}
422413

423-
await ensureInfrastructure(prefix, dryRun);
424-
425414
// Deploy new color
426415
logger.step(`${prefix}Deploying ${nextColor} services...`);
427-
const colorCompose = generateColorCompose(serviceConfig, nextColor, {
428-
fresh: options.fresh,
429-
});
416+
const colorCompose = generateColorCompose(serviceConfig, nextColor);
430417

431418
if (dryRun) {
432419
for (const service of rotatableToUpdate) {

tools/cli/src/lib/actions/reset.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export async function reset(options: ResetOptions): Promise<void> {
2323

2424
logger.warn('This will remove ALL blue-green containers');
2525
if (includeStateful) {
26-
logger.warn('Including stateful services: db, proxy');
26+
logger.warn(`Including stateful services: ${STATEFUL_SERVICES.join(', ')}`);
2727
}
2828

2929
if (!force) {
@@ -136,7 +136,9 @@ export async function reset(options: ResetOptions): Promise<void> {
136136
logger.success('Reset complete! All blue-green containers removed');
137137
}
138138
if (!includeStateful) {
139-
logger.info('Stateful services (db, proxy) were preserved');
139+
logger.info(
140+
`Stateful services (${STATEFUL_SERVICES.join(', ')}) were preserved`,
141+
);
140142
logger.info('Use --all to remove them as well');
141143
}
142144
});

tools/cli/src/lib/actions/rollback.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { getProjectId, type DeploymentEnv } from '../../utils/load-env';
22
import * as logger from '../../utils/logger';
3+
import { REQUIRED_VOLUMES } from '../compose/generators/constants';
34
import { generateColorCompose } from '../compose/generators/generate-color-compose';
45
import { ROTATABLE_SERVICES } from '../compose/types';
56
import { dockerCompose } from '../docker/docker-compose';
7+
import { ensureNetwork } from '../docker/ensure-network';
8+
import { ensureVolumes } from '../docker/ensure-volumes';
69
import { getContainerVersion } from '../docker/get-container-version';
710
import { pullImage } from '../docker/pull-image';
811
import { removeContainer } from '../docker/remove-container';
@@ -87,6 +90,10 @@ export async function rollback(options: RollbackOptions): Promise<void> {
8790
await removeContainer(containerName);
8891
}
8992

93+
// Ensure infrastructure exists before compose up
94+
await ensureVolumes([...REQUIRED_VOLUMES]);
95+
await ensureNetwork('internal');
96+
9097
// Deploy rollback color
9198
logger.step(
9299
`Deploying ${rollbackColor} services with version ${rollbackVersion}...`,

tools/cli/src/lib/compose/generators/constants.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ export const DEV_VOLUME_NAMES = [
1414
'crawler-data',
1515
] as const;
1616

17+
// All volumes that must exist before any `docker compose up` in production.
18+
// Every volume declared as `external: true` in the stateful or color compose
19+
// must appear here so `ensureVolumes` pre-creates it.
20+
export const REQUIRED_VOLUMES = [
21+
// platform-data is kept for upgrade scenarios where split-convex migrates
22+
// its contents into convex-data; on fresh installs it is an unused empty
23+
// volume. Removing it would break detect() for pre-0.3.0 deployments.
24+
'platform-data',
25+
'convex-data',
26+
'caddy-data',
27+
'caddy-config',
28+
'db-data',
29+
'db-backup',
30+
'rag-data',
31+
'crawler-data',
32+
] as const;
33+
1734
// Enables containers to reach host services (e.g. Ollama on localhost:11434)
1835
// via `host.docker.internal`. `host-gateway` requires Docker 20.10+ (project
1936
// already requires 24.0+). Safe on Docker Desktop where host.docker.internal

0 commit comments

Comments
 (0)