forgeos 0.1.0-alpha.44 → 0.1.0-alpha.45
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/AGENTS.md +1 -1
- package/CHANGELOG.md +23 -0
- package/docs/changelog.md +15 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/cli/deploy.ts +88 -7
- package/src/forge/cli/field-test.ts +52 -7
- package/src/forge/cli/parse.ts +1 -0
- package/src/forge/cli/workos.ts +14 -1
- package/src/forge/version.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.45 input=bd2a19728a6c6eef99a21730384e4e1f16b60d6e561897a0b6f307c136f9555c content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.45
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Smooth the WorkOS, field-test, and deploy-readiness DX discovered during real
|
|
8
|
+
app field testing.
|
|
9
|
+
|
|
10
|
+
- Accept `--real` through the top-level unknown-option guard so documented
|
|
11
|
+
commands like `forge workos setup --real --file workos-seed.yml --json` and
|
|
12
|
+
`forge workos prove --real --file workos-seed.yml --json` run instead of
|
|
13
|
+
being rejected before parsing.
|
|
14
|
+
- Treat known WorkOS CLI duplicate-resource seed responses as explicit clean
|
|
15
|
+
idempotency: Forge now records `seedAlreadyAppliedReason`, suppresses the
|
|
16
|
+
scary duplicate stderr from the successful JSON result, and writes seed
|
|
17
|
+
state with the already-applied status.
|
|
18
|
+
- Run `forge field-test run` through an async harness process and emit
|
|
19
|
+
periodic stderr heartbeats so long realistic probes no longer look frozen
|
|
20
|
+
while preserving machine-readable JSON output.
|
|
21
|
+
- Add a `deploy-env-sources` check to `forge deploy check --production` that
|
|
22
|
+
reports which env sources were inspected, which keys were present, and which
|
|
23
|
+
production keys are still missing without printing secret values; `.env` and
|
|
24
|
+
`.env.local` are shown as local guidance, not production deploy evidence.
|
|
25
|
+
|
|
3
26
|
## 0.1.0-alpha.44
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,21 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.45
|
|
10
|
+
|
|
11
|
+
- `forge workos setup/prove --real` now passes the top-level unknown-option
|
|
12
|
+
guard, matching the documented no-dashboard WorkOS apply flow.
|
|
13
|
+
- `forge workos seed --file workos-seed.yml --json` now treats known WorkOS
|
|
14
|
+
duplicate-resource responses as explicit idempotency, suppressing duplicate
|
|
15
|
+
stderr in successful JSON output while recording `seedAlreadyAppliedReason`
|
|
16
|
+
and seed state.
|
|
17
|
+
- `forge field-test run` now uses an async harness process with periodic
|
|
18
|
+
progress heartbeats on stderr so realistic probes do not appear stuck.
|
|
19
|
+
- `forge deploy check --production` now reports sanitized env-source evidence,
|
|
20
|
+
including which production keys were found or missing, while keeping `.env`
|
|
21
|
+
and `.env.local` clearly marked as local guidance rather than production
|
|
22
|
+
deploy evidence.
|
|
23
|
+
|
|
9
24
|
## 0.1.0-alpha.44
|
|
10
25
|
|
|
11
26
|
- `forge inspect ui --ergonomics` now treats helper-based seed recovery inside
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.45","releaseId":"forgeos@0.1.0-alpha.45+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.45 input=bd2a19728a6c6eef99a21730384e4e1f16b60d6e561897a0b6f307c136f9555c content=ddd1dcdfbef3bde6b2457bcfc5fd270a97d6d97ec6477305dc9c47c9f85f7c05
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.45",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.45+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/deploy.ts
CHANGED
|
@@ -120,6 +120,23 @@ function fieldTestReportCandidates(workspaceRoot: string): Array<{ path: string;
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const FIELD_TEST_PRODUCTION_COMMAND = "forge field-test run --realistic --json";
|
|
123
|
+
const DEPLOY_ENV_FILE = "deploy/.env.production";
|
|
124
|
+
const LOCAL_ENV_FILES = [".env", ".env.local"];
|
|
125
|
+
const DEPLOY_ENV_KEYS = [
|
|
126
|
+
"DATABASE_URL",
|
|
127
|
+
"FORGE_AUTH_MODE",
|
|
128
|
+
"FORGE_AUTH_ISSUER",
|
|
129
|
+
"FORGE_AUTH_AUDIENCE",
|
|
130
|
+
"FORGE_AUTH_JWKS_URI",
|
|
131
|
+
"FORGE_AUTH_DISCOVERY_URL",
|
|
132
|
+
"WORKOS_API_KEY",
|
|
133
|
+
"WORKOS_CLIENT_ID",
|
|
134
|
+
"WORKOS_COOKIE_PASSWORD",
|
|
135
|
+
"WORKOS_REDIRECT_URI",
|
|
136
|
+
"WORKOS_POST_LOGIN_REDIRECT_URI",
|
|
137
|
+
"WORKOS_POST_LOGOUT_REDIRECT_URI",
|
|
138
|
+
"WORKOS_WEBHOOK_SECRET",
|
|
139
|
+
];
|
|
123
140
|
|
|
124
141
|
function summarizeFieldTestReport(data: Record<string, unknown>) {
|
|
125
142
|
const results = Array.isArray(data.results) ? data.results as Array<Record<string, unknown>> : [];
|
|
@@ -428,7 +445,7 @@ function renderEnvExample(workspaceRoot: string): string {
|
|
|
428
445
|
}
|
|
429
446
|
|
|
430
447
|
function hasProductionEnvFile(workspaceRoot: string): boolean {
|
|
431
|
-
return nodeFileSystem.exists(join(workspaceRoot,
|
|
448
|
+
return nodeFileSystem.exists(join(workspaceRoot, DEPLOY_ENV_FILE));
|
|
432
449
|
}
|
|
433
450
|
|
|
434
451
|
function parseEnvValue(value: string): string {
|
|
@@ -442,8 +459,8 @@ function parseEnvValue(value: string): string {
|
|
|
442
459
|
return trimmed;
|
|
443
460
|
}
|
|
444
461
|
|
|
445
|
-
function
|
|
446
|
-
const absolute = join(workspaceRoot,
|
|
462
|
+
function readEnvFile(workspaceRoot: string, relative: string): Record<string, string> {
|
|
463
|
+
const absolute = join(workspaceRoot, relative);
|
|
447
464
|
if (!nodeFileSystem.exists(absolute)) return {};
|
|
448
465
|
const text = nodeFileSystem.readText(absolute) ?? "";
|
|
449
466
|
const values: Record<string, string> = {};
|
|
@@ -457,6 +474,63 @@ function readDeployEnvFile(workspaceRoot: string): Record<string, string> {
|
|
|
457
474
|
return values;
|
|
458
475
|
}
|
|
459
476
|
|
|
477
|
+
function readDeployEnvFile(workspaceRoot: string): Record<string, string> {
|
|
478
|
+
return readEnvFile(workspaceRoot, DEPLOY_ENV_FILE);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function envKeys(values: Record<string, string>): string[] {
|
|
482
|
+
return DEPLOY_ENV_KEYS.filter((key) => Boolean(values[key])).sort();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function processDeployEnv(): Record<string, string> {
|
|
486
|
+
return Object.fromEntries(
|
|
487
|
+
DEPLOY_ENV_KEYS
|
|
488
|
+
.filter((key) => Boolean(process.env[key]))
|
|
489
|
+
.map((key) => [key, process.env[key]!]),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
|
|
494
|
+
const processEnv = processDeployEnv();
|
|
495
|
+
const deployEnv = readDeployEnvFile(workspaceRoot);
|
|
496
|
+
const localSources = LOCAL_ENV_FILES.map((path) => {
|
|
497
|
+
const values = readEnvFile(workspaceRoot, path);
|
|
498
|
+
return {
|
|
499
|
+
path,
|
|
500
|
+
present: nodeFileSystem.exists(join(workspaceRoot, path)),
|
|
501
|
+
role: "local-guidance",
|
|
502
|
+
readForProduction: false,
|
|
503
|
+
keys: envKeys(values),
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
const productionEvidence = {
|
|
507
|
+
...deployEnv,
|
|
508
|
+
...processEnv,
|
|
509
|
+
};
|
|
510
|
+
return {
|
|
511
|
+
readOrder: ["process.env", DEPLOY_ENV_FILE],
|
|
512
|
+
note: `${DEPLOY_ENV_FILE} and current process env are production deploy evidence; .env/.env.local are reported only as local guidance and are not used to pass production gates.`,
|
|
513
|
+
sources: [
|
|
514
|
+
{
|
|
515
|
+
path: "process.env",
|
|
516
|
+
present: envKeys(processEnv).length > 0,
|
|
517
|
+
role: "production-evidence",
|
|
518
|
+
readForProduction: true,
|
|
519
|
+
keys: envKeys(processEnv),
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
path: DEPLOY_ENV_FILE,
|
|
523
|
+
present: hasProductionEnvFile(workspaceRoot),
|
|
524
|
+
role: "production-evidence",
|
|
525
|
+
readForProduction: true,
|
|
526
|
+
keys: envKeys(deployEnv),
|
|
527
|
+
},
|
|
528
|
+
...localSources,
|
|
529
|
+
],
|
|
530
|
+
missingProductionKeys: DEPLOY_ENV_KEYS.filter((key) => !productionEvidence[key]),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
460
534
|
function deployEnvValue(workspaceRoot: string, name: string): string | undefined {
|
|
461
535
|
return process.env[name] || readDeployEnvFile(workspaceRoot)[name];
|
|
462
536
|
}
|
|
@@ -494,15 +568,15 @@ function databaseReady(options: DeployCommandOptions): boolean {
|
|
|
494
568
|
|
|
495
569
|
function databaseReadyMessage(options: DeployCommandOptions): string {
|
|
496
570
|
if (process.env.DATABASE_URL) return "DATABASE_URL is set in the current environment";
|
|
497
|
-
if (readDeployEnvFile(options.workspaceRoot).DATABASE_URL) return
|
|
498
|
-
if (hasProductionEnvFile(options.workspaceRoot)) return
|
|
571
|
+
if (readDeployEnvFile(options.workspaceRoot).DATABASE_URL) return `DATABASE_URL is set in ${DEPLOY_ENV_FILE}`;
|
|
572
|
+
if (hasProductionEnvFile(options.workspaceRoot)) return `${DEPLOY_ENV_FILE} is present but does not set DATABASE_URL`;
|
|
499
573
|
if (hasRenderedProductionEnvExample(options.workspaceRoot)) {
|
|
500
574
|
return options.production
|
|
501
|
-
?
|
|
575
|
+
? `deploy/.env.production.example is only a template; copy it to ${DEPLOY_ENV_FILE} with DATABASE_URL or set DATABASE_URL`
|
|
502
576
|
: "deploy/.env.production.example is present";
|
|
503
577
|
}
|
|
504
578
|
return options.production
|
|
505
|
-
?
|
|
579
|
+
? `production deploy requires DATABASE_URL or ${DEPLOY_ENV_FILE}`
|
|
506
580
|
: "deploy readiness requires DATABASE_URL or rendered deploy/.env.production.example";
|
|
507
581
|
}
|
|
508
582
|
|
|
@@ -650,6 +724,13 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
650
724
|
found: lockfiles,
|
|
651
725
|
},
|
|
652
726
|
});
|
|
727
|
+
checks.push({
|
|
728
|
+
name: "deploy-env-sources",
|
|
729
|
+
ok: true,
|
|
730
|
+
severity: "warning",
|
|
731
|
+
message: `${DEPLOY_ENV_FILE} and process.env are production deploy evidence; .env/.env.local are local guidance only`,
|
|
732
|
+
details: deployEnvSources(options.workspaceRoot),
|
|
733
|
+
});
|
|
653
734
|
|
|
654
735
|
const auth = await withDeployEnv(options.workspaceRoot, async () => loadAuthConfigFromEnv(options.workspaceRoot));
|
|
655
736
|
const productionAuth = auth.mode === "jwt" || auth.mode === "oidc";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -400,7 +400,56 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
400
400
|
});
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
function
|
|
403
|
+
function shouldEmitProgress(): boolean {
|
|
404
|
+
return process.env.FORGE_FIELD_TEST_PROGRESS !== "0";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function runHarnessProcess(
|
|
408
|
+
options: FieldTestCommandOptions,
|
|
409
|
+
args: string[],
|
|
410
|
+
): Promise<{ status: number | null; stdout: string; stderr: string }> {
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
const startedAt = Date.now();
|
|
413
|
+
const emitProgress = shouldEmitProgress();
|
|
414
|
+
const child = spawn(process.execPath, args, {
|
|
415
|
+
cwd: options.workspaceRoot,
|
|
416
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
417
|
+
windowsHide: true,
|
|
418
|
+
});
|
|
419
|
+
let stdout = "";
|
|
420
|
+
let stderr = "";
|
|
421
|
+
let settled = false;
|
|
422
|
+
child.stdout?.setEncoding("utf8");
|
|
423
|
+
child.stderr?.setEncoding("utf8");
|
|
424
|
+
child.stdout?.on("data", (chunk) => {
|
|
425
|
+
stdout += chunk;
|
|
426
|
+
});
|
|
427
|
+
child.stderr?.on("data", (chunk) => {
|
|
428
|
+
stderr += chunk;
|
|
429
|
+
});
|
|
430
|
+
const heartbeat = emitProgress
|
|
431
|
+
? setInterval(() => {
|
|
432
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
433
|
+
process.stderr.write(`[forge] field-test still running after ${elapsed}s; timeout=${options.timeoutMs}ms\n`);
|
|
434
|
+
}, 30_000)
|
|
435
|
+
: undefined;
|
|
436
|
+
const finish = (status: number | null, extraStderr = "") => {
|
|
437
|
+
if (settled) return;
|
|
438
|
+
settled = true;
|
|
439
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
440
|
+
if (extraStderr) stderr += extraStderr;
|
|
441
|
+
resolve({ status, stdout, stderr });
|
|
442
|
+
};
|
|
443
|
+
child.on("error", (error) => {
|
|
444
|
+
finish(1, `${error.message}\n`);
|
|
445
|
+
});
|
|
446
|
+
child.on("close", (status) => {
|
|
447
|
+
finish(status);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function runHarness(options: FieldTestCommandOptions): Promise<FieldTestCommandResult> {
|
|
404
453
|
const script = scriptPath(options.workspaceRoot);
|
|
405
454
|
if (!script) {
|
|
406
455
|
return {
|
|
@@ -433,11 +482,7 @@ function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
|
433
482
|
if (options.forgeSpec) args.push("--forge-spec", options.forgeSpec);
|
|
434
483
|
if (reportPath) args.push("--write-report", reportPath);
|
|
435
484
|
if (options.json) args.push("--json");
|
|
436
|
-
const result =
|
|
437
|
-
cwd: options.workspaceRoot,
|
|
438
|
-
encoding: "utf8",
|
|
439
|
-
windowsHide: true,
|
|
440
|
-
});
|
|
485
|
+
const result = await runHarnessProcess(options, args);
|
|
441
486
|
const reportAbsolute = reportPath ? resolveReportPath(options.workspaceRoot, reportPath) : null;
|
|
442
487
|
const reportData = reportAbsolute && existsSync(reportAbsolute)
|
|
443
488
|
? JSON.parse(readFileSync(reportAbsolute, "utf8")) as unknown
|
package/src/forge/cli/parse.ts
CHANGED
package/src/forge/cli/workos.ts
CHANGED
|
@@ -703,7 +703,9 @@ function seedData(input: {
|
|
|
703
703
|
dryRun?: boolean;
|
|
704
704
|
seedFileSanitized?: boolean;
|
|
705
705
|
seedAlreadyApplied?: boolean;
|
|
706
|
+
seedAlreadyAppliedReason?: string;
|
|
706
707
|
seedStateFile?: string;
|
|
708
|
+
workosCli?: Record<string, unknown>;
|
|
707
709
|
configActions?: WorkOSConfigActionResult[];
|
|
708
710
|
nextCommand?: string;
|
|
709
711
|
}): Record<string, unknown> {
|
|
@@ -716,7 +718,9 @@ function seedData(input: {
|
|
|
716
718
|
dryRun: input.dryRun ?? false,
|
|
717
719
|
seedFileSanitized: input.seedFileSanitized ?? false,
|
|
718
720
|
seedAlreadyApplied: input.seedAlreadyApplied ?? false,
|
|
721
|
+
...(input.seedAlreadyAppliedReason ? { seedAlreadyAppliedReason: input.seedAlreadyAppliedReason } : {}),
|
|
719
722
|
...(input.seedStateFile ? { seedStateFile: input.seedStateFile } : {}),
|
|
723
|
+
...(input.workosCli ? { workosCli: input.workosCli } : {}),
|
|
720
724
|
configActions: input.configActions ?? [],
|
|
721
725
|
...(input.nextCommand ? { nextCommand: input.nextCommand } : {}),
|
|
722
726
|
};
|
|
@@ -1087,6 +1091,9 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
|
|
|
1087
1091
|
const child = runExternalCommand(delegatedCommand, options);
|
|
1088
1092
|
const seedAlreadyApplied =
|
|
1089
1093
|
child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr);
|
|
1094
|
+
const seedAlreadyAppliedReason = seedAlreadyApplied
|
|
1095
|
+
? "workos-cli-existing-resource"
|
|
1096
|
+
: undefined;
|
|
1090
1097
|
const seedStateFile = child.status === 0 || seedAlreadyApplied
|
|
1091
1098
|
? writeWorkOSSeedState({
|
|
1092
1099
|
workspaceRoot: options.workspaceRoot,
|
|
@@ -1113,11 +1120,17 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
|
|
|
1113
1120
|
seedState: latestSeedState,
|
|
1114
1121
|
seedFileSanitized: preparedSeed.sanitized,
|
|
1115
1122
|
seedAlreadyApplied,
|
|
1123
|
+
seedAlreadyAppliedReason,
|
|
1116
1124
|
seedStateFile,
|
|
1125
|
+
workosCli: {
|
|
1126
|
+
status: child.status,
|
|
1127
|
+
idempotentConflict: seedAlreadyApplied,
|
|
1128
|
+
stderrSuppressed: seedAlreadyApplied && Boolean(child.stderr.trim()),
|
|
1129
|
+
},
|
|
1117
1130
|
configActions,
|
|
1118
1131
|
}),
|
|
1119
1132
|
stdout: child.stdout,
|
|
1120
|
-
stderr: child.stderr,
|
|
1133
|
+
stderr: seedAlreadyApplied ? undefined : child.stderr,
|
|
1121
1134
|
exitCode: child.status === 0 || seedAlreadyApplied ? 0 : 1,
|
|
1122
1135
|
};
|
|
1123
1136
|
} finally {
|
package/src/forge/version.ts
CHANGED