@aurostack/stacks 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/stack.mjs +1 -1
- package/package.json +1 -1
- package/templates/nest-api/files/.bin/secrets.ts +15 -6
- package/templates/nest-api/files/_gitignore +1 -0
- package/templates/nest-api/files/src/app.setup.ts +5 -3
- package/templates/nest-api/files/src/common/constants/pino.ts +14 -0
- package/templates/nest-api/files/src/common/modules/logger.module.ts +3 -2
- package/templates/nest-api/files/src/notifications/services/notifications.service.ts +4 -2
- package/templates/nest-api/template.json +18 -4
- package/templates/node-worker/files/.bin/secrets.ts +15 -6
- package/templates/node-worker/files/_gitignore +1 -0
- package/templates/node-worker/files/package.json +1 -0
- package/templates/node-worker/files/src/index.ts +39 -16
- package/templates/node-worker/files/src/logger.ts +14 -1
- package/templates/node-worker/files/src/utils/worker.ts +1 -1
- package/templates/py-worker/files/utils/db.py +20 -27
- package/templates/py-worker/files/utils/logger.py +9 -2
- package/templates/py-worker/files/utils/worker.py +2 -2
- package/templates/react-app/files/.bin/secrets.js +8 -2
- package/templates/react-app/files/.forgejo/workflows/build-and-push.yml +3 -0
- package/templates/react-app/files/Dockerfile +10 -0
- package/templates/react-app/files/_gitignore +1 -0
- package/templates/react-app/files/build.sh +3 -1
- package/templates/react-app/template.json +2 -2
- package/templates/react-monorepo/files/.forgejo/workflows/build-and-push.yml +15 -11
- package/templates/react-monorepo/files/Dockerfile +21 -7
- package/templates/react-monorepo/files/_gitignore +1 -0
- package/templates/react-monorepo/files/apps/admin/.bin/secrets.js +8 -3
- package/templates/react-monorepo/files/apps/admin/src/app/router.tsx +11 -7
- package/templates/react-monorepo/files/apps/auth/.bin/secrets.js +8 -3
- package/templates/react-monorepo/files/apps/client/.bin/secrets.js +8 -3
- package/templates/react-monorepo/files/apps/client/src/vite-env.d.ts +1 -1
- package/templates/react-monorepo/files/apps/landing/.bin/secrets.js +8 -3
- package/templates/react-monorepo/files/build.sh +3 -1
- package/templates/react-monorepo/files/packages/types/.bin/secrets.js +8 -3
- package/templates/react-monorepo/files/turbo.json +1 -1
- package/templates/react-monorepo/template.json +41 -9
package/cli/stack.mjs
CHANGED
|
@@ -94,7 +94,7 @@ ${c.bold('extract options')}
|
|
|
94
94
|
|
|
95
95
|
${c.bold('Examples')}
|
|
96
96
|
stack new nest-api ~/Projects/acme/api --with realtime,media
|
|
97
|
-
stack new react-monorepo ~/Projects/acme/web --
|
|
97
|
+
stack new react-monorepo ~/Projects/acme/web --with app-landing,app-admin
|
|
98
98
|
stack extract nest-api --source ~/Projects/acme/web/backend
|
|
99
99
|
`;
|
|
100
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurostack/stacks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Scaffold new projects from house templates: a NestJS API, React app or Turborepo monorepo, and Node or Python workers. Subtractive templates plus a zero-dependency generator.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aurostack (https://github.com/aurostack-org)",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import shell from 'shelljs';
|
|
2
3
|
import yargs from 'yargs';
|
|
3
4
|
import { hideBin } from 'yargs/helpers';
|
|
@@ -52,9 +53,7 @@ const getCmd = (env: string) => {
|
|
|
52
53
|
if (!PROJECT_ID) {
|
|
53
54
|
throw new Error('INFISICAL_PROJECT_ID is not set — nothing to fetch from.');
|
|
54
55
|
}
|
|
55
|
-
return `infisical export --projectId="${PROJECT_ID}" --path=${SECRET_PATH} --env=${env}
|
|
56
|
-
env
|
|
57
|
-
)}`;
|
|
56
|
+
return `infisical export --projectId="${PROJECT_ID}" --path=${SECRET_PATH} --env=${env}`;
|
|
58
57
|
};
|
|
59
58
|
|
|
60
59
|
const log = (env: string) => console.log(`Fetching secrets for '${env}'...`);
|
|
@@ -62,7 +61,17 @@ const log = (env: string) => console.log(`Fetching secrets for '${env}'...`);
|
|
|
62
61
|
(async () => {
|
|
63
62
|
const env = await getArg();
|
|
64
63
|
const command = getCmd(env);
|
|
64
|
+
const file = getEnvFile(env);
|
|
65
65
|
log(env);
|
|
66
|
-
shell
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
67
|
+
// the existing file before infisical had the chance to fail.
|
|
68
|
+
const result = shell.exec(command, { silent: true });
|
|
69
|
+
if (result.code !== 0) {
|
|
70
|
+
throw new Error(result.stderr.trim() || `infisical exited with ${result.code}`);
|
|
71
|
+
}
|
|
72
|
+
fs.writeFileSync(file, result.stdout);
|
|
73
|
+
console.log(`Wrote ${file}`);
|
|
74
|
+
})().catch((err: unknown) => {
|
|
75
|
+
console.error(err instanceof Error ? err.message : err);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -120,9 +120,10 @@ export const enableHelmet = (app: INestApplication) => {
|
|
|
120
120
|
// @feature:start openapi, queue
|
|
121
121
|
/**
|
|
122
122
|
* Lock the operator-facing surfaces behind basic auth. Every path added here is
|
|
123
|
-
* something that leaks internals if left open — the API reference
|
|
124
|
-
*
|
|
125
|
-
* the same commit that
|
|
123
|
+
* something that leaks internals if left open — the API reference (Scalar at
|
|
124
|
+
* /docs, Swagger UI at /openapi, the raw document at /openapi-json) and the
|
|
125
|
+
* queue dashboard — so a new one belongs in this list on the same commit that
|
|
126
|
+
* mounts it.
|
|
126
127
|
*/
|
|
127
128
|
export const enableBasicAuth = (app: INestApplication) => {
|
|
128
129
|
const config = app.get(CustomConfigService);
|
|
@@ -131,6 +132,7 @@ export const enableBasicAuth = (app: INestApplication) => {
|
|
|
131
132
|
app.use(
|
|
132
133
|
[
|
|
133
134
|
'/docs', // @feature openapi
|
|
135
|
+
'/openapi', // @feature openapi
|
|
134
136
|
'/openapi-json', // @feature openapi
|
|
135
137
|
'/dashboard' // @feature queue
|
|
136
138
|
],
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pino-pretty is a dev dependency, so the production image doesn't have it.
|
|
3
|
+
* Pretty-print only where it's installed; anywhere else (an image run with an
|
|
4
|
+
* APP_ENV other than production, say) log JSON to stdout instead of crashing.
|
|
5
|
+
*/
|
|
6
|
+
export const PRETTY_AVAILABLE = (() => {
|
|
7
|
+
try {
|
|
8
|
+
require.resolve('pino-pretty');
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
1
15
|
export const PINO_CONSOLE_TARGET = {
|
|
2
16
|
target: 'pino-pretty',
|
|
3
17
|
options: {
|
|
@@ -7,7 +7,8 @@ import { LoggerService } from '../services';
|
|
|
7
7
|
import {
|
|
8
8
|
PINO_CONSOLE_TARGET,
|
|
9
9
|
PINO_FILE_TARGET,
|
|
10
|
-
PINO_CONSOLE_TARGET_PROD
|
|
10
|
+
PINO_CONSOLE_TARGET_PROD,
|
|
11
|
+
PRETTY_AVAILABLE
|
|
11
12
|
} from '../constants';
|
|
12
13
|
|
|
13
14
|
@Global()
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
pinoHttp: {
|
|
24
25
|
level: config.logger.level,
|
|
25
26
|
transport:
|
|
26
|
-
config.app.env === 'production'
|
|
27
|
+
config.app.env === 'production' || !PRETTY_AVAILABLE
|
|
27
28
|
? { targets: [PINO_CONSOLE_TARGET_PROD, PINO_FILE_TARGET] }
|
|
28
29
|
: { targets: [PINO_CONSOLE_TARGET, PINO_FILE_TARGET] },
|
|
29
30
|
autoLogging: {
|
|
@@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
|
|
|
2
2
|
import { NotificationType, Prisma } from '@db/client';
|
|
3
3
|
import { PrismaService, LoggerService } from 'common/services';
|
|
4
4
|
import { ok } from 'common/entity';
|
|
5
|
-
import { RealtimeService } from 'realtime/services/realtime.service';
|
|
5
|
+
import { RealtimeService } from 'realtime/services/realtime.service'; // @feature realtime
|
|
6
6
|
import {
|
|
7
7
|
NotificationEntity,
|
|
8
8
|
PaginatedNotificationEntity,
|
|
@@ -14,7 +14,7 @@ import { NotificationFilters } from '../dto';
|
|
|
14
14
|
export class NotificationsService {
|
|
15
15
|
constructor(
|
|
16
16
|
private readonly db: PrismaService,
|
|
17
|
-
private readonly realtime: RealtimeService,
|
|
17
|
+
private readonly realtime: RealtimeService, // @feature realtime
|
|
18
18
|
private readonly logger: LoggerService
|
|
19
19
|
) {}
|
|
20
20
|
|
|
@@ -36,9 +36,11 @@ export class NotificationsService {
|
|
|
36
36
|
const notification = await this.db.notification.create({
|
|
37
37
|
data: { userId, type, data }
|
|
38
38
|
});
|
|
39
|
+
// @feature:start realtime
|
|
39
40
|
// Sockets bypass the HTTP serializer; encode through the same schema so
|
|
40
41
|
// the event has exactly the shape the REST endpoints return.
|
|
41
42
|
this.realtime.notifyUser(userId, NotificationEntity.parse(notification));
|
|
43
|
+
// @feature:end
|
|
42
44
|
return notification;
|
|
43
45
|
} catch (err) {
|
|
44
46
|
this.logger.warn(
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
"regex": true,
|
|
48
48
|
"flags": "gm"
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"from": "^(SERVER_HOST|BETTER_AUTH_URL)='http://localhost:5000'$",
|
|
52
|
+
"to": "$1='http://localhost:{{port}}'",
|
|
53
|
+
"regex": true,
|
|
54
|
+
"flags": "gm"
|
|
55
|
+
},
|
|
50
56
|
{
|
|
51
57
|
"from": "^EXPOSE 5000$",
|
|
52
58
|
"to": "EXPOSE {{port}}",
|
|
@@ -171,6 +177,9 @@
|
|
|
171
177
|
"observability": {
|
|
172
178
|
"title": "Observability: OpenTelemetry and health checks",
|
|
173
179
|
"description": "OpenTelemetry traces, logs and metrics pushed over OTLP (built for OpenObserve; set OPENOBSERVE_URL/ORG/TOKEN, or any OTEL_* backend), plus Terminus health checks with a Prisma indicator. Off at runtime until connected.",
|
|
180
|
+
"requires": [
|
|
181
|
+
"http-client"
|
|
182
|
+
],
|
|
174
183
|
"default": true,
|
|
175
184
|
"files": [
|
|
176
185
|
"src/common/controllers/health.controller.ts",
|
|
@@ -217,10 +226,11 @@
|
|
|
217
226
|
"media": {
|
|
218
227
|
"title": "File uploads (S3)",
|
|
219
228
|
"description": "MediaService and S3Service for public and private uploads, with multipart decorators.",
|
|
220
|
-
"default": true,
|
|
221
229
|
"requires": [
|
|
222
|
-
"auth"
|
|
230
|
+
"auth",
|
|
231
|
+
"queue"
|
|
223
232
|
],
|
|
233
|
+
"default": true,
|
|
224
234
|
"files": [
|
|
225
235
|
"src/media/**",
|
|
226
236
|
"src/users/dto/avatar.dto.ts"
|
|
@@ -276,7 +286,7 @@
|
|
|
276
286
|
},
|
|
277
287
|
"notifications": {
|
|
278
288
|
"title": "Persistent user notifications",
|
|
279
|
-
"description": "A Notification model plus dispatch/read endpoints.
|
|
289
|
+
"description": "A Notification model plus dispatch/read endpoints. Pushes each new notification over the realtime layer when that feature is also on, and persists either way.",
|
|
280
290
|
"requires": [
|
|
281
291
|
"auth"
|
|
282
292
|
],
|
|
@@ -291,7 +301,8 @@
|
|
|
291
301
|
"description": "A session-aware FeatureFlagGuard and FeatureFlagService for gating routes on flags.",
|
|
292
302
|
"requires": [
|
|
293
303
|
"cache",
|
|
294
|
-
"auth"
|
|
304
|
+
"auth",
|
|
305
|
+
"scheduler"
|
|
295
306
|
],
|
|
296
307
|
"files": [
|
|
297
308
|
"src/common/services/feature-flag.service.ts",
|
|
@@ -408,6 +419,9 @@
|
|
|
408
419
|
"ci": {
|
|
409
420
|
"title": "Forgejo Actions CI",
|
|
410
421
|
"description": "A workflow that spins up the test stack and runs both Vitest projects on pull requests.",
|
|
422
|
+
"requires": [
|
|
423
|
+
"secrets"
|
|
424
|
+
],
|
|
411
425
|
"default": true,
|
|
412
426
|
"files": [
|
|
413
427
|
".forgejo/**"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import shell from 'shelljs';
|
|
2
3
|
import yargs from 'yargs';
|
|
3
4
|
import { hideBin } from 'yargs/helpers';
|
|
@@ -52,9 +53,7 @@ const getCmd = (env: string) => {
|
|
|
52
53
|
if (!PROJECT_ID) {
|
|
53
54
|
throw new Error('INFISICAL_PROJECT_ID is not set — nothing to fetch from.');
|
|
54
55
|
}
|
|
55
|
-
return `infisical export --projectId="${PROJECT_ID}" --path=${SECRET_PATH} --env=${env}
|
|
56
|
-
env
|
|
57
|
-
)}`;
|
|
56
|
+
return `infisical export --projectId="${PROJECT_ID}" --path=${SECRET_PATH} --env=${env}`;
|
|
58
57
|
};
|
|
59
58
|
|
|
60
59
|
const log = (env: string) => console.log(`Fetching secrets for '${env}'...`);
|
|
@@ -62,7 +61,17 @@ const log = (env: string) => console.log(`Fetching secrets for '${env}'...`);
|
|
|
62
61
|
(async () => {
|
|
63
62
|
const env = await getArg();
|
|
64
63
|
const command = getCmd(env);
|
|
64
|
+
const file = getEnvFile(env);
|
|
65
65
|
log(env);
|
|
66
|
-
shell
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
67
|
+
// the existing file before infisical had the chance to fail.
|
|
68
|
+
const result = shell.exec(command, { silent: true });
|
|
69
|
+
if (result.code !== 0) {
|
|
70
|
+
throw new Error(result.stderr.trim() || `infisical exited with ${result.code}`);
|
|
71
|
+
}
|
|
72
|
+
fs.writeFileSync(file, result.stdout);
|
|
73
|
+
console.log(`Wrote ${file}`);
|
|
74
|
+
})().catch((err: unknown) => {
|
|
75
|
+
console.error(err instanceof Error ? err.message : err);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -2,19 +2,42 @@ import logger from '#app/logger.js';
|
|
|
2
2
|
import * as Workers from '#app/workers/index.js'; // @feature queue
|
|
3
3
|
import { createTemporalWorker } from '#app/temporal/worker.js'; // @feature temporal
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
5
|
+
// Everything still running when a shutdown starts: each entry settles once its
|
|
6
|
+
// worker has stopped taking new work and finished what it had.
|
|
7
|
+
const running: Promise<unknown>[] = [];
|
|
8
|
+
|
|
9
|
+
logger.info('Worker started');
|
|
10
|
+
|
|
11
|
+
// @feature:start queue
|
|
12
|
+
// Add each new BullMQ worker to this list: it is started here and closed on
|
|
13
|
+
// shutdown (close() stops fetching and waits for the active job).
|
|
14
|
+
const queueWorkers = [Workers.example];
|
|
15
|
+
for (const worker of queueWorkers) void worker.run();
|
|
16
|
+
// @feature:end
|
|
17
|
+
|
|
18
|
+
// @feature:start temporal
|
|
19
|
+
// Temporal handles SIGINT/SIGTERM itself: it stops polling and lets running
|
|
20
|
+
// tasks finish, and then run() resolves.
|
|
21
|
+
const temporal = createTemporalWorker().then((worker) => worker.run());
|
|
22
|
+
temporal.catch((err: unknown) => {
|
|
23
|
+
logger.fatal({ err }, 'Temporal worker failed');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
running.push(temporal);
|
|
27
|
+
// @feature:end
|
|
28
|
+
|
|
29
|
+
let stopping = false;
|
|
30
|
+
const shutdown = (signal: NodeJS.Signals) => {
|
|
31
|
+
// `on` rather than `once`, and idempotent: the telemetry preload re-raises
|
|
32
|
+
// the signal after flushing, and that second delivery must not kill the
|
|
33
|
+
// process while jobs are still finishing.
|
|
34
|
+
if (stopping) return;
|
|
35
|
+
stopping = true;
|
|
36
|
+
logger.info({ signal }, 'Shutting down');
|
|
37
|
+
void Promise.allSettled([
|
|
38
|
+
...queueWorkers.map((worker) => worker.close()), // @feature queue
|
|
39
|
+
...running
|
|
40
|
+
]).then(() => process.exit(0));
|
|
41
|
+
};
|
|
42
|
+
process.on('SIGTERM', shutdown);
|
|
43
|
+
process.on('SIGINT', shutdown);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
1
2
|
import pino, { stdTimeFunctions, levels } from 'pino';
|
|
2
3
|
import config from '#app/config.js';
|
|
3
4
|
|
|
@@ -26,10 +27,22 @@ const PINO_CONSOLE_TARGET_PROD = {
|
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
// pino-pretty is a dev dependency, so the production image doesn't have it.
|
|
31
|
+
// Pretty-print only where it's installed; anywhere else (an image run with an
|
|
32
|
+
// APP_ENV other than production, say) log JSON to stdout instead of crashing.
|
|
33
|
+
const prettyAvailable = (() => {
|
|
34
|
+
try {
|
|
35
|
+
createRequire(import.meta.url).resolve('pino-pretty');
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
|
|
29
42
|
const logger = pino({
|
|
30
43
|
level: config.app.logLevel,
|
|
31
44
|
transport:
|
|
32
|
-
config.app.env === 'production'
|
|
45
|
+
config.app.env === 'production' || !prettyAvailable
|
|
33
46
|
? { targets: [PINO_CONSOLE_TARGET_PROD, PINO_FILE_TARGET] }
|
|
34
47
|
: { targets: [PINO_CONSOLE_TARGET, PINO_FILE_TARGET] },
|
|
35
48
|
base: {
|
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
Database access over SQLAlchemy, with the connection settings from config
|
|
3
|
+
(DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Usage:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
using credentials from the .env file (user, password, host, port).
|
|
7
|
+
from utils.db import DB
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from utils import DB
|
|
13
|
-
|
|
14
|
-
db = DB('my_database')
|
|
15
|
-
|
|
16
|
-
# Read data
|
|
17
|
-
df = db.read_sql('SELECT * FROM my_table')
|
|
18
|
-
|
|
19
|
-
# Write data
|
|
20
|
-
db.write_sql(df, 'my_table', if_exists='append')
|
|
21
|
-
|
|
22
|
-
# Use engine or session without having to create a new connection or variable
|
|
23
|
-
db.session.query(...)
|
|
24
|
-
db.engine.execute(...)
|
|
25
|
-
# ... perform operations ...
|
|
9
|
+
db = DB()
|
|
10
|
+
rows = db.session.execute(select(MyModel)).scalars().all()
|
|
26
11
|
db.session.commit()
|
|
27
|
-
|
|
28
|
-
# Close the connection when done
|
|
29
12
|
db.close()
|
|
30
13
|
|
|
14
|
+
The models under models/ are hand-written to match the API's Prisma schema:
|
|
15
|
+
the API owns the tables and their migrations.
|
|
31
16
|
"""
|
|
32
17
|
|
|
33
18
|
from sqlalchemy import create_engine
|
|
34
|
-
from sqlalchemy.engine import Connection
|
|
19
|
+
from sqlalchemy.engine import URL, Connection
|
|
35
20
|
from sqlalchemy.orm import sessionmaker, Session
|
|
36
21
|
|
|
37
22
|
from config import CONFIG
|
|
@@ -40,9 +25,17 @@ from config import CONFIG
|
|
|
40
25
|
class DB:
|
|
41
26
|
def __init__(self):
|
|
42
27
|
db = CONFIG.env.db
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
# URL.create escapes the credentials, so a password with @, / or : in it
|
|
29
|
+
# can't break the connection string.
|
|
30
|
+
url = URL.create(
|
|
31
|
+
"postgresql",
|
|
32
|
+
username=db.user,
|
|
33
|
+
password=db.password,
|
|
34
|
+
host=db.host,
|
|
35
|
+
port=db.port,
|
|
36
|
+
database=db.name,
|
|
37
|
+
)
|
|
38
|
+
self._engine = create_engine(url).connect()
|
|
46
39
|
Session = sessionmaker(bind=self.engine)
|
|
47
40
|
self._session = Session()
|
|
48
41
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
@File: logger.py
|
|
3
3
|
@Version: 1.1
|
|
4
4
|
|
|
5
|
-
Logger wrapper to manage logs in one place with log rotate
|
|
5
|
+
Logger wrapper to manage logs in one place with log rotate. Records also go to
|
|
6
|
+
stdout, so `docker logs` shows them.
|
|
6
7
|
|
|
7
8
|
Logger('daily') will create daily.log file in the logs folder
|
|
8
9
|
Logger('my.daily') will create my.daily.log file in the logs folder
|
|
@@ -35,6 +36,7 @@ except Exception as e:
|
|
|
35
36
|
import logging
|
|
36
37
|
import os
|
|
37
38
|
import pathlib
|
|
39
|
+
import sys
|
|
38
40
|
from logging import handlers
|
|
39
41
|
|
|
40
42
|
from config import CONFIG
|
|
@@ -49,7 +51,7 @@ class Logger(object):
|
|
|
49
51
|
if module is not None:
|
|
50
52
|
directory = f"{directory}/{module}"
|
|
51
53
|
logger_name = f"{module}_{name}"
|
|
52
|
-
|
|
54
|
+
pathlib.Path(directory).mkdir(parents=True, exist_ok=True)
|
|
53
55
|
|
|
54
56
|
filename = f"{directory}/{name}.log"
|
|
55
57
|
formatter = "%(asctime)s %(levelname)-10s %(message)s"
|
|
@@ -74,6 +76,11 @@ class Logger(object):
|
|
|
74
76
|
handler.setFormatter(formatter)
|
|
75
77
|
self.logger.addHandler(handler)
|
|
76
78
|
|
|
79
|
+
console = logging.StreamHandler(sys.stdout)
|
|
80
|
+
console.setLevel(logging.INFO)
|
|
81
|
+
console.setFormatter(formatter)
|
|
82
|
+
self.logger.addHandler(console)
|
|
83
|
+
|
|
77
84
|
def debug(self, message, exc_info=None):
|
|
78
85
|
self.__call(self.logger.debug, message, exc_info)
|
|
79
86
|
|
|
@@ -3,7 +3,7 @@ from bullmq import Worker, Job
|
|
|
3
3
|
from urllib.parse import urlparse, quote, urlencode
|
|
4
4
|
|
|
5
5
|
from config import CONFIG, Redis
|
|
6
|
-
from utils.db import DB
|
|
6
|
+
from utils.db import DB # @feature database
|
|
7
7
|
from utils.logger import Logger
|
|
8
8
|
from utils.telemetry import job_span # @feature telemetry
|
|
9
9
|
|
|
@@ -84,7 +84,7 @@ class JobUtil:
|
|
|
84
84
|
await self._job.updateProgress(progress)
|
|
85
85
|
await self._job.log(message)
|
|
86
86
|
|
|
87
|
-
async def finalize(self, db: DB | None = None):
|
|
87
|
+
async def finalize(self, db: "DB | None" = None):
|
|
88
88
|
if db:
|
|
89
89
|
db.close()
|
|
90
90
|
await self._job.updateProgress(100)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
2
3
|
|
|
3
4
|
// Pulls this app's env from Infisical into the matching .env file.
|
|
4
5
|
//
|
|
@@ -42,6 +43,11 @@ const env = normalizeEnv(parseEnvArg());
|
|
|
42
43
|
const envFile = ENV_FILES[env];
|
|
43
44
|
|
|
44
45
|
console.log(`Fetching secrets (${env}) -> ${envFile}...`);
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
47
|
+
// the existing file before infisical had the chance to fail. execSync throws
|
|
48
|
+
// (and the process exits non-zero) when infisical fails.
|
|
49
|
+
const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${SECRET_PATH}" --env=${env}`, {
|
|
50
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
47
51
|
});
|
|
52
|
+
writeFileSync(envFile, output);
|
|
53
|
+
console.log(`Wrote ${envFile}`);
|
|
@@ -47,6 +47,9 @@ jobs:
|
|
|
47
47
|
VITE_APP_API_URL=${{ secrets.VITE_APP_API_URL }}
|
|
48
48
|
VITE_APP_SITE_URL=${{ secrets.VITE_APP_SITE_URL }}
|
|
49
49
|
VITE_APP_SUPPORT_EMAIL=${{ secrets.VITE_APP_SUPPORT_EMAIL }}
|
|
50
|
+
VITE_OPENOBSERVE_URL=${{ secrets.VITE_OPENOBSERVE_URL }} # @feature telemetry
|
|
51
|
+
VITE_OPENOBSERVE_ORG=${{ secrets.VITE_OPENOBSERVE_ORG }} # @feature telemetry
|
|
52
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=${{ secrets.VITE_OPENOBSERVE_CLIENT_TOKEN }} # @feature telemetry
|
|
50
53
|
tags: |
|
|
51
54
|
${{ secrets.REGISTRY }}/${{ steps.tag.outputs.OWNER }}/web:latest
|
|
52
55
|
${{ secrets.REGISTRY }}/${{ steps.tag.outputs.OWNER }}/web:${{ steps.tag.outputs.TAG }}
|
|
@@ -35,6 +35,16 @@ ARG VITE_APP_SUPPORT_EMAIL
|
|
|
35
35
|
ENV VITE_APP_API_URL=$VITE_APP_API_URL \
|
|
36
36
|
VITE_APP_SITE_URL=$VITE_APP_SITE_URL \
|
|
37
37
|
VITE_APP_SUPPORT_EMAIL=$VITE_APP_SUPPORT_EMAIL
|
|
38
|
+
# @feature:start telemetry
|
|
39
|
+
# Browser monitoring. Leave unset and the bundle ships with telemetry off. The
|
|
40
|
+
# client token is a RUM token, write-only and meant to be public.
|
|
41
|
+
ARG VITE_OPENOBSERVE_URL
|
|
42
|
+
ARG VITE_OPENOBSERVE_ORG
|
|
43
|
+
ARG VITE_OPENOBSERVE_CLIENT_TOKEN
|
|
44
|
+
ENV VITE_OPENOBSERVE_URL=$VITE_OPENOBSERVE_URL \
|
|
45
|
+
VITE_OPENOBSERVE_ORG=$VITE_OPENOBSERVE_ORG \
|
|
46
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=$VITE_OPENOBSERVE_CLIENT_TOKEN
|
|
47
|
+
# @feature:end
|
|
38
48
|
|
|
39
49
|
# Fail loudly rather than silently producing a bundle with `undefined` URLs baked
|
|
40
50
|
# in — that failure is invisible until the app 404s at runtime.
|
|
@@ -38,7 +38,9 @@ read_var() {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
BUILD_ARGS=""
|
|
41
|
-
|
|
41
|
+
VARS="VITE_APP_API_URL VITE_APP_SITE_URL VITE_APP_SUPPORT_EMAIL"
|
|
42
|
+
VARS="$VARS VITE_OPENOBSERVE_URL VITE_OPENOBSERVE_ORG VITE_OPENOBSERVE_CLIENT_TOKEN" # @feature telemetry
|
|
43
|
+
for VAR in $VARS; do
|
|
42
44
|
VALUE="$(read_var "$VAR")"
|
|
43
45
|
[ -n "$VALUE" ] && BUILD_ARGS="$BUILD_ARGS --build-arg $VAR=$VALUE"
|
|
44
46
|
done
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
},
|
|
128
128
|
"pwa": {
|
|
129
129
|
"title": "Installable PWA",
|
|
130
|
-
"description": "vite-plugin-pwa: app-shell offline caching and
|
|
130
|
+
"description": "vite-plugin-pwa: app-shell offline caching, and registerType 'prompt' so a new build waits for the user instead of swapping assets under a running app. The update prompt and install button are yours to build with the plugin's useRegisterSW.",
|
|
131
131
|
"packageJson": {
|
|
132
132
|
"devDependencies": [
|
|
133
133
|
"vite-plugin-pwa"
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
},
|
|
137
137
|
"charts": {
|
|
138
138
|
"title": "Charts",
|
|
139
|
-
"description": "ApexCharts
|
|
139
|
+
"description": "ApexCharts and react-apexcharts, with a theme-aware chart palette (the --chart-* tokens in the UI kit). No chart components ship.",
|
|
140
140
|
"packageJson": {
|
|
141
141
|
"dependencies": [
|
|
142
142
|
"apexcharts",
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
# Builds and pushes
|
|
1
|
+
# Builds and pushes every app's image from one Dockerfile.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
# tag is generated once in a separate job and shared by every matrix leg —
|
|
3
|
+
# The tag is generated once in a separate job and shared by every matrix leg;
|
|
5
4
|
# otherwise each app would get its own random tag and there would be no way to
|
|
6
5
|
# deploy a matching set.
|
|
7
6
|
#
|
|
8
7
|
# Vite compiles the VITE_* URLs into each bundle at build time, so these images
|
|
9
|
-
# are specific to one environment.
|
|
8
|
+
# are specific to one environment.
|
|
10
9
|
|
|
11
10
|
name: Build and Push
|
|
12
11
|
|
|
@@ -33,12 +32,10 @@ jobs:
|
|
|
33
32
|
DATE=$(date +'%y_%m_%d')
|
|
34
33
|
RAND=$(printf '%05d' "$((RANDOM % 100000))")
|
|
35
34
|
echo "TAG=${DATE}_${RAND}" >> "$GITHUB_OUTPUT"
|
|
36
|
-
# Derived from `github.repository`
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# images land as <registry>/acme-corp/web-<app>, matching
|
|
41
|
-
# stack/frontend.compose.yml.
|
|
35
|
+
# Derived from `github.repository` rather than
|
|
36
|
+
# `github.repository_owner`, so this doesn't depend on a context
|
|
37
|
+
# variable Forgejo may not populate. Images land as
|
|
38
|
+
# <registry>/<owner>/web-<app>.
|
|
42
39
|
echo "OWNER=$(echo '${{ github.repository }}' | cut -d/ -f1)" >> "$GITHUB_OUTPUT"
|
|
43
40
|
|
|
44
41
|
build-and-push:
|
|
@@ -49,7 +46,11 @@ jobs:
|
|
|
49
46
|
# failure. Note this means a partial set can be pushed — re-run the job.
|
|
50
47
|
fail-fast: false
|
|
51
48
|
matrix:
|
|
52
|
-
app:
|
|
49
|
+
app:
|
|
50
|
+
- landing # @feature app-landing
|
|
51
|
+
- auth # @feature app-auth
|
|
52
|
+
- client # @feature app-client
|
|
53
|
+
- admin # @feature app-admin
|
|
53
54
|
|
|
54
55
|
steps:
|
|
55
56
|
- name: Checkout repository
|
|
@@ -83,6 +84,9 @@ jobs:
|
|
|
83
84
|
VITE_APP_LANDING=${{ secrets.VITE_APP_LANDING }}
|
|
84
85
|
VITE_APP_SUPPORT_EMAIL=${{ secrets.VITE_APP_SUPPORT_EMAIL }}
|
|
85
86
|
VITE_APP_SITE_URL=${{ secrets.VITE_APP_SITE_URL }}
|
|
87
|
+
VITE_OPENOBSERVE_URL=${{ secrets.VITE_OPENOBSERVE_URL }} # @feature telemetry
|
|
88
|
+
VITE_OPENOBSERVE_ORG=${{ secrets.VITE_OPENOBSERVE_ORG }} # @feature telemetry
|
|
89
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=${{ secrets.VITE_OPENOBSERVE_CLIENT_TOKEN }} # @feature telemetry
|
|
86
90
|
tags: |
|
|
87
91
|
${{ secrets.REGISTRY }}/${{ needs.meta.outputs.owner }}/web-${{ matrix.app }}:latest
|
|
88
92
|
${{ secrets.REGISTRY }}/${{ needs.meta.outputs.owner }}/web-${{ matrix.app }}:${{ needs.meta.outputs.tag }}
|
|
@@ -9,24 +9,28 @@
|
|
|
9
9
|
# IMPORTANT: Vite inlines `VITE_*` values into the JS bundle at BUILD time, so a
|
|
10
10
|
# built image is pinned to one environment. The URLs below are build args, not
|
|
11
11
|
# runtime env — setting them in compose does nothing. Build one image per
|
|
12
|
-
# environment
|
|
12
|
+
# environment.
|
|
13
13
|
|
|
14
14
|
# ---------- deps ----------
|
|
15
15
|
# Manifests only, so `yarn install` re-runs solely when a dependency changes.
|
|
16
16
|
FROM aurostack.dev/wesnetech/nodejs:24 AS deps
|
|
17
17
|
WORKDIR /repo
|
|
18
18
|
|
|
19
|
+
# One line per workspace: every workspace in the project must be here (yarn
|
|
20
|
+
# install --immutable fails on a lockfile naming a missing one), and nothing
|
|
21
|
+
# else (COPY of a missing file fails the build).
|
|
19
22
|
COPY package.json yarn.lock .yarnrc.yml turbo.json ./
|
|
20
|
-
COPY apps/admin/package.json apps/admin/
|
|
21
|
-
COPY apps/auth/package.json apps/auth/
|
|
22
|
-
COPY apps/client/package.json apps/client/
|
|
23
|
-
COPY apps/landing/package.json apps/landing/
|
|
23
|
+
COPY apps/admin/package.json apps/admin/ # @feature app-admin
|
|
24
|
+
COPY apps/auth/package.json apps/auth/ # @feature app-auth
|
|
25
|
+
COPY apps/client/package.json apps/client/ # @feature app-client
|
|
26
|
+
COPY apps/landing/package.json apps/landing/ # @feature app-landing
|
|
24
27
|
COPY packages/api/package.json packages/api/
|
|
25
28
|
COPY packages/auth/package.json packages/auth/
|
|
26
29
|
COPY packages/config/package.json packages/config/
|
|
27
|
-
COPY packages/hooks/package.json packages/hooks/
|
|
30
|
+
COPY packages/hooks/package.json packages/hooks/ # @feature hooks
|
|
28
31
|
COPY packages/layouts/package.json packages/layouts/
|
|
29
|
-
COPY packages/
|
|
32
|
+
COPY packages/telemetry/package.json packages/telemetry/ # @feature telemetry
|
|
33
|
+
COPY packages/types/package.json packages/types/ # @feature types
|
|
30
34
|
COPY packages/ui/package.json packages/ui/
|
|
31
35
|
|
|
32
36
|
RUN corepack enable && yarn install --immutable
|
|
@@ -56,6 +60,16 @@ ARG VITE_APP_SUPPORT_EMAIL
|
|
|
56
60
|
# generated sitemap.xml. Must be a build arg — .dockerignore excludes .env, so a
|
|
57
61
|
# local value never reaches the image.
|
|
58
62
|
ARG VITE_APP_SITE_URL
|
|
63
|
+
# @feature:start telemetry
|
|
64
|
+
# Browser monitoring. Leave unset and the bundle ships with telemetry off. The
|
|
65
|
+
# client token is a RUM token, write-only and meant to be public.
|
|
66
|
+
ARG VITE_OPENOBSERVE_URL
|
|
67
|
+
ARG VITE_OPENOBSERVE_ORG
|
|
68
|
+
ARG VITE_OPENOBSERVE_CLIENT_TOKEN
|
|
69
|
+
ENV VITE_OPENOBSERVE_URL=$VITE_OPENOBSERVE_URL \
|
|
70
|
+
VITE_OPENOBSERVE_ORG=$VITE_OPENOBSERVE_ORG \
|
|
71
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=$VITE_OPENOBSERVE_CLIENT_TOKEN
|
|
72
|
+
# @feature:end
|
|
59
73
|
ENV VITE_APP_API_URL=$VITE_APP_API_URL \
|
|
60
74
|
VITE_APP_AUTH_HOST=$VITE_APP_AUTH_HOST \
|
|
61
75
|
VITE_APP_CLIENT_HOST=$VITE_APP_CLIENT_HOST \
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
|
|
|
52
52
|
const envFile = ENV_FILES[env];
|
|
53
53
|
|
|
54
54
|
console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
56
|
+
// the existing file before infisical had the chance to fail. execSync throws
|
|
57
|
+
// (and the process exits non-zero) when infisical fails.
|
|
58
|
+
const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
|
|
59
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
57
60
|
});
|
|
61
|
+
writeFileSync(envFile, output);
|
|
62
|
+
console.log(`Wrote ${envFile}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Suspense, lazy, type ReactNode } from 'react';
|
|
2
2
|
import { createBrowserRouter } from 'react-router';
|
|
3
|
-
import {
|
|
4
|
-
import { AppShell, AppShellSkeleton, ErrorBoundary, NotFound, RouteFallback } from '@acme/layouts';
|
|
3
|
+
import { RoleRoute, signOut, useAuth } from '@acme/auth';
|
|
4
|
+
import { AppShell, AppShellSkeleton, ErrorBoundary, Forbidden, NotFound, RouteFallback } from '@acme/layouts';
|
|
5
5
|
import { NAV_ITEMS } from './nav';
|
|
6
6
|
import { HomeRoute } from '../routes/home';
|
|
7
7
|
|
|
@@ -15,8 +15,8 @@ const lazyRoute = (element: ReactNode) => <Suspense fallback={<RouteFallback />}
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The shell reads the session itself rather than taking it as a prop —
|
|
18
|
-
* `
|
|
19
|
-
* time this renders.
|
|
18
|
+
* `RoleRoute` has already established the user is signed in with an admin role
|
|
19
|
+
* by the time this renders.
|
|
20
20
|
*
|
|
21
21
|
* `ErrorBoundary` wraps the shell rather than being passed as the route's
|
|
22
22
|
* `errorElement`: it is a plain React error boundary taking `children`, so it
|
|
@@ -35,11 +35,15 @@ export const router = createBrowserRouter([
|
|
|
35
35
|
{
|
|
36
36
|
// `fallback` is required and must not be blank: the session round trip is
|
|
37
37
|
// cross-origin and takes most of a second, and rendering nothing for that
|
|
38
|
-
// long reads as a broken app.
|
|
38
|
+
// long reads as a broken app. Signed-out visitors go to the auth app;
|
|
39
|
+
// signed-in users without an admin role see Forbidden.
|
|
40
|
+
//
|
|
41
|
+
// The gate is UI, not security: every endpoint this console calls must
|
|
42
|
+
// enforce the same role on the API.
|
|
39
43
|
element: (
|
|
40
|
-
<
|
|
44
|
+
<RoleRoute allow={['admin', 'superuser']} fallback={<AppShellSkeleton />} forbidden={<Forbidden />}>
|
|
41
45
|
<Shell />
|
|
42
|
-
</
|
|
46
|
+
</RoleRoute>
|
|
43
47
|
),
|
|
44
48
|
children: [
|
|
45
49
|
{ index: true, element: <HomeRoute /> },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
|
|
|
52
52
|
const envFile = ENV_FILES[env];
|
|
53
53
|
|
|
54
54
|
console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
56
|
+
// the existing file before infisical had the chance to fail. execSync throws
|
|
57
|
+
// (and the process exits non-zero) when infisical fails.
|
|
58
|
+
const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
|
|
59
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
57
60
|
});
|
|
61
|
+
writeFileSync(envFile, output);
|
|
62
|
+
console.log(`Wrote ${envFile}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
|
|
|
52
52
|
const envFile = ENV_FILES[env];
|
|
53
53
|
|
|
54
54
|
console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
56
|
+
// the existing file before infisical had the chance to fail. execSync throws
|
|
57
|
+
// (and the process exits non-zero) when infisical fails.
|
|
58
|
+
const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
|
|
59
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
57
60
|
});
|
|
61
|
+
writeFileSync(envFile, output);
|
|
62
|
+
console.log(`Wrote ${envFile}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
|
|
|
52
52
|
const envFile = ENV_FILES[env];
|
|
53
53
|
|
|
54
54
|
console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
56
|
+
// the existing file before infisical had the chance to fail. execSync throws
|
|
57
|
+
// (and the process exits non-zero) when infisical fails.
|
|
58
|
+
const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
|
|
59
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
57
60
|
});
|
|
61
|
+
writeFileSync(envFile, output);
|
|
62
|
+
console.log(`Wrote ${envFile}`);
|
|
@@ -46,7 +46,9 @@ read_var() {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
BUILD_ARGS="--build-arg APP=$APP"
|
|
49
|
-
|
|
49
|
+
VARS="VITE_APP_API_URL VITE_APP_AUTH_HOST VITE_APP_CLIENT_HOST VITE_APP_ADMIN_HOST VITE_APP_LANDING VITE_APP_SUPPORT_EMAIL VITE_APP_SITE_URL"
|
|
50
|
+
VARS="$VARS VITE_OPENOBSERVE_URL VITE_OPENOBSERVE_ORG VITE_OPENOBSERVE_CLIENT_TOKEN" # @feature telemetry
|
|
51
|
+
for VAR in $VARS; do
|
|
50
52
|
VALUE="$(read_var "$VAR")"
|
|
51
53
|
[ -n "$VALUE" ] && BUILD_ARGS="$BUILD_ARGS --build-arg $VAR=$VALUE"
|
|
52
54
|
done
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -52,6 +52,11 @@ const env = normalizeEnv(parseEnvArg());
|
|
|
52
52
|
const envFile = ENV_FILES[env];
|
|
53
53
|
|
|
54
54
|
console.log(`Fetching secrets for '${slug}' (${env}) -> ${envFile}...`);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Export first and write only on success: a shell `>` redirect would empty
|
|
56
|
+
// the existing file before infisical had the chance to fail. execSync throws
|
|
57
|
+
// (and the process exits non-zero) when infisical fails.
|
|
58
|
+
const output = execSync(`infisical export --projectId="${PROJECT_ID}" --path="${secretPath}" --env=${env}`, {
|
|
59
|
+
stdio: ['inherit', 'pipe', 'inherit']
|
|
57
60
|
});
|
|
61
|
+
writeFileSync(envFile, output);
|
|
62
|
+
console.log(`Wrote ${envFile}`);
|
|
@@ -145,14 +145,14 @@
|
|
|
145
145
|
},
|
|
146
146
|
"app-admin": {
|
|
147
147
|
"title": "Admin console",
|
|
148
|
-
"description": "
|
|
148
|
+
"description": "Internal console over the same data layer, gated to the admin and superuser roles in the UI. The API must enforce the role on every endpoint it calls.",
|
|
149
149
|
"files": [
|
|
150
150
|
"apps/admin/**"
|
|
151
151
|
]
|
|
152
152
|
},
|
|
153
153
|
"app-landing": {
|
|
154
154
|
"title": "Marketing site",
|
|
155
|
-
"description": "Public
|
|
155
|
+
"description": "Public marketing site: a home page, an SEO route table, and build-time robots.txt, sitemap.xml and security.txt.",
|
|
156
156
|
"files": [
|
|
157
157
|
"apps/landing/**"
|
|
158
158
|
]
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
},
|
|
174
174
|
"pwa": {
|
|
175
175
|
"title": "Installable PWA (client app)",
|
|
176
|
-
"description": "vite-plugin-pwa: app-shell offline caching,
|
|
176
|
+
"description": "vite-plugin-pwa configured for the client app: app-shell offline caching, and registerType 'prompt' so a new build waits for the user instead of swapping assets under a running app. The update prompt and install button are yours to build with the plugin's useRegisterSW.",
|
|
177
177
|
"requires": [
|
|
178
178
|
"app-client"
|
|
179
179
|
],
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
},
|
|
187
187
|
"charts": {
|
|
188
188
|
"title": "Charts (client app)",
|
|
189
|
-
"description": "ApexCharts
|
|
189
|
+
"description": "ApexCharts and react-apexcharts in the client app, with a theme-aware chart palette (the --chart-* tokens in the UI kit). No chart components ship.",
|
|
190
190
|
"requires": [
|
|
191
191
|
"app-client"
|
|
192
192
|
],
|
|
@@ -239,11 +239,43 @@
|
|
|
239
239
|
"apps/*/.bin/**",
|
|
240
240
|
"packages/types/.bin/**"
|
|
241
241
|
],
|
|
242
|
-
"packageJson":
|
|
243
|
-
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
"packageJson": [
|
|
243
|
+
{
|
|
244
|
+
"scripts": [
|
|
245
|
+
"secrets"
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"file": "apps/admin/package.json",
|
|
250
|
+
"scripts": [
|
|
251
|
+
"secrets"
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"file": "apps/auth/package.json",
|
|
256
|
+
"scripts": [
|
|
257
|
+
"secrets"
|
|
258
|
+
]
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"file": "apps/client/package.json",
|
|
262
|
+
"scripts": [
|
|
263
|
+
"secrets"
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"file": "apps/landing/package.json",
|
|
268
|
+
"scripts": [
|
|
269
|
+
"secrets"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"file": "packages/types/package.json",
|
|
274
|
+
"scripts": [
|
|
275
|
+
"secrets"
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
]
|
|
247
279
|
}
|
|
248
280
|
},
|
|
249
281
|
"hooks": [
|