forgeos 0.1.0-alpha.36 → 0.1.0-alpha.38
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 +11 -0
- package/docs/changelog.md +43 -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/auth.ts +171 -13
- package/src/forge/cli/commands.ts +73 -5
- package/src/forge/cli/deploy.ts +887 -0
- package/src/forge/cli/deps.ts +3 -1
- package/src/forge/cli/docs.ts +1 -1
- package/src/forge/cli/doctor.ts +216 -0
- package/src/forge/cli/field-test.ts +328 -0
- package/src/forge/cli/handoff.ts +13 -5
- package/src/forge/cli/main.ts +10 -1
- package/src/forge/cli/new.ts +4 -0
- package/src/forge/cli/parse.ts +142 -4
- package/src/forge/cli/workos.ts +5 -5
- package/src/forge/compiler/package-graph/compiler.ts +3 -1
- package/src/forge/compiler/package-graph/constants.ts +1 -1
- package/src/forge/compiler/package-upgrades/planner.ts +27 -10
- package/src/forge/compiler/types/package-graph.ts +6 -0
- 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.38 input=1b961b8fed6ae0e07fb70ef025922e4a191655d9aca0f6c8617be2f88c204192 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.37
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix `forge deps upgrade-plan` for npm alias installs used by ForgeOS apps.
|
|
8
|
+
|
|
9
|
+
- Record the real package name from package metadata in the generated package graph when a dependency is installed through an alias such as `forge: npm:forgeos@...`.
|
|
10
|
+
- Resolve upgrade plans by either the real package name (`forgeos`) or the dependency alias (`forge`) while preserving install specs like `forge@npm:forgeos@0.1.0-alpha.37`.
|
|
11
|
+
- Avoid attempting to resolve the unrelated public `forge@alpha` package when an app asks to upgrade its ForgeOS alias dependency.
|
|
12
|
+
- Add regression coverage for aliased package graph entries where the import name and real npm package name differ.
|
|
13
|
+
|
|
3
14
|
## 0.1.0-alpha.36
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,49 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.38
|
|
10
|
+
|
|
11
|
+
- Added first-class production deployment commands:
|
|
12
|
+
`forge deploy plan`, `forge deploy render docker`,
|
|
13
|
+
`forge deploy check --production`, and
|
|
14
|
+
`forge deploy verify --production`.
|
|
15
|
+
- Generated Docker deployment files now use the app's detected package manager,
|
|
16
|
+
require the matching lockfile for production checks, read
|
|
17
|
+
`deploy/.env.production`, and avoid hidden hard-coded `DATABASE_URL`
|
|
18
|
+
overrides.
|
|
19
|
+
- Added `forge field-test create`, `forge field-test run`, and
|
|
20
|
+
`forge field-test report`, including runtime/auth probe summaries and
|
|
21
|
+
`productionEvidence` so field-test reports can feed
|
|
22
|
+
`forge deploy check --production`.
|
|
23
|
+
- Split `forge release doctor` into npm publish readiness and production deploy
|
|
24
|
+
readiness, so package publishing is not confused with a production-ready app
|
|
25
|
+
claim.
|
|
26
|
+
- Made `forge handoff --json` include commit-ready files by default, excluding
|
|
27
|
+
generated and operational artifacts while reporting how much noise was
|
|
28
|
+
filtered out.
|
|
29
|
+
- Clarified local auth versus production auth with `forge auth status` posture
|
|
30
|
+
classifications, local `x-forge-*` header reporting, and production proof
|
|
31
|
+
next actions.
|
|
32
|
+
- Added `dbGuide` to `forge doctor runtime` and `forge doctor pglite` so agents
|
|
33
|
+
know when to use `--db memory`, `--db pglite`, stop an active process, or run
|
|
34
|
+
local PGlite repair.
|
|
35
|
+
- Updated production, self-host, field-test, security, troubleshooting, and CLI
|
|
36
|
+
docs to reflect the new deploy/auth/database workflow.
|
|
37
|
+
|
|
38
|
+
## 0.1.0-alpha.37
|
|
39
|
+
|
|
40
|
+
- Fixed `forge deps upgrade-plan` for npm alias installs used by ForgeOS apps.
|
|
41
|
+
- Recorded the real package name from package metadata in generated package
|
|
42
|
+
graph entries when a dependency is installed through an alias such as
|
|
43
|
+
`forge: npm:forgeos@...`.
|
|
44
|
+
- Resolved upgrade plans by either the real package name (`forgeos`) or the
|
|
45
|
+
dependency alias (`forge`) while preserving install specs like
|
|
46
|
+
`forge@npm:forgeos@0.1.0-alpha.37`.
|
|
47
|
+
- Avoided attempting to resolve the unrelated public `forge@alpha` package when
|
|
48
|
+
an app asks to upgrade its ForgeOS alias dependency.
|
|
49
|
+
- Added regression coverage for aliased package graph entries where the import
|
|
50
|
+
name and real npm package name differ.
|
|
51
|
+
|
|
9
52
|
## 0.1.0-alpha.36
|
|
10
53
|
|
|
11
54
|
- Smoothed the alpha field-test loop after the Vendor Access app exercise.
|
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.38","releaseId":"forgeos@0.1.0-alpha.38+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.38 input=1b961b8fed6ae0e07fb70ef025922e4a191655d9aca0f6c8617be2f88c204192 content=71d83a9364c458ef2ffea0838d343e066e959d81819610519960a33118827078
|
|
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.38",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.38+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -12,6 +12,12 @@ import { loadSecretRegistry } from "../runtime/secrets/check.ts";
|
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
|
|
15
|
+
import {
|
|
16
|
+
collectExpectedResourceTypes,
|
|
17
|
+
collectPolicyPermissions,
|
|
18
|
+
missingValues,
|
|
19
|
+
parseSeedFile,
|
|
20
|
+
} from "./workos.ts";
|
|
15
21
|
|
|
16
22
|
export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
|
|
17
23
|
|
|
@@ -92,14 +98,59 @@ function buildAuthPosture(workspaceRoot: string) {
|
|
|
92
98
|
const productionMode = config.mode === "jwt" || config.mode === "oidc";
|
|
93
99
|
const errors = configErrors(config);
|
|
94
100
|
const configReady = errors.length === 0;
|
|
101
|
+
const classification = config.mode === "dev-headers"
|
|
102
|
+
? "local-dev"
|
|
103
|
+
: config.mode === "disabled"
|
|
104
|
+
? "unauthenticated"
|
|
105
|
+
: configReady
|
|
106
|
+
? "production-ready"
|
|
107
|
+
: "production-incomplete";
|
|
108
|
+
const reason = productionMode
|
|
109
|
+
? configReady
|
|
110
|
+
? "jwt/oidc production auth configuration is present"
|
|
111
|
+
: "jwt/oidc production auth is selected but required settings are missing"
|
|
112
|
+
: config.mode === "disabled"
|
|
113
|
+
? "auth is disabled; this is not suitable for public production traffic"
|
|
114
|
+
: "dev-headers auth is local-only and is not real production authentication";
|
|
95
115
|
return normalizeForgeCliCommandsInValue(workspaceRoot, {
|
|
96
116
|
schemaVersion: "0.1.0",
|
|
97
117
|
mode: config.mode,
|
|
118
|
+
classification,
|
|
98
119
|
localOnly: config.mode === "dev-headers",
|
|
99
120
|
productionReady: productionMode && configReady,
|
|
100
121
|
requiresTenant: config.requiresTenant,
|
|
101
122
|
bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
|
|
102
123
|
tenantClaim: config.claims.tenantId ?? "tenant_id",
|
|
124
|
+
environment: {
|
|
125
|
+
current: classification,
|
|
126
|
+
localDevelopment: config.mode === "dev-headers",
|
|
127
|
+
publicProduction: productionMode && configReady,
|
|
128
|
+
warning: reason,
|
|
129
|
+
},
|
|
130
|
+
localDevHeaders: config.mode === "dev-headers"
|
|
131
|
+
? {
|
|
132
|
+
acceptedHeaders: [
|
|
133
|
+
"x-forge-user-id",
|
|
134
|
+
"x-forge-tenant-id",
|
|
135
|
+
"x-forge-role",
|
|
136
|
+
"x-forge-roles",
|
|
137
|
+
"x-forge-permissions",
|
|
138
|
+
],
|
|
139
|
+
scope: "forge dev, tests, and local agent workflows only",
|
|
140
|
+
neverProduction: true,
|
|
141
|
+
}
|
|
142
|
+
: null,
|
|
143
|
+
productionRequirements: {
|
|
144
|
+
requiredMode: "jwt or oidc",
|
|
145
|
+
bearerHeader: "Authorization: Bearer <token>",
|
|
146
|
+
requiredEnv: [
|
|
147
|
+
"FORGE_AUTH_MODE",
|
|
148
|
+
"FORGE_AUTH_ISSUER",
|
|
149
|
+
"FORGE_AUTH_AUDIENCE",
|
|
150
|
+
config.mode === "oidc" ? "OIDC discovery at FORGE_AUTH_ISSUER" : "FORGE_AUTH_JWKS_URI",
|
|
151
|
+
],
|
|
152
|
+
proofCommand: "forge auth prove --prod --token <jwt> --json",
|
|
153
|
+
},
|
|
103
154
|
productionChecklist: [
|
|
104
155
|
{ item: "auth mode is jwt or oidc", ok: productionMode },
|
|
105
156
|
{ item: "FORGE_AUTH_ISSUER configured", ok: Boolean(config.issuer) },
|
|
@@ -109,11 +160,7 @@ function buildAuthPosture(workspaceRoot: string) {
|
|
|
109
160
|
{ item: "permission claim mapped", ok: Boolean(config.claims.permissions) },
|
|
110
161
|
{ item: "dev-headers disabled for public runtime", ok: config.mode !== "dev-headers" },
|
|
111
162
|
],
|
|
112
|
-
reason
|
|
113
|
-
? configReady
|
|
114
|
-
? "jwt/oidc production auth configuration is present"
|
|
115
|
-
: "jwt/oidc production auth is selected but required settings are missing"
|
|
116
|
-
: "dev-headers auth is local-only and is not real production authentication",
|
|
163
|
+
reason,
|
|
117
164
|
nextActions: productionMode
|
|
118
165
|
? configReady
|
|
119
166
|
? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
|
|
@@ -122,12 +169,26 @@ function buildAuthPosture(workspaceRoot: string) {
|
|
|
122
169
|
});
|
|
123
170
|
}
|
|
124
171
|
|
|
172
|
+
function permissionSubsetForRestrictedTenant(permissions: string[]): string[] {
|
|
173
|
+
const readonly = permissions.filter((permission) =>
|
|
174
|
+
/(^|[:._-])(read|list|view|inspect|search)($|[:._-])/.test(permission)
|
|
175
|
+
);
|
|
176
|
+
if (readonly.length > 0) return readonly;
|
|
177
|
+
return permissions.slice(0, Math.max(1, Math.floor(permissions.length / 2)));
|
|
178
|
+
}
|
|
179
|
+
|
|
125
180
|
function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
|
|
126
181
|
const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
|
|
127
182
|
const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
|
|
128
183
|
const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
|
|
129
184
|
const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
|
|
130
|
-
const
|
|
185
|
+
const activePermissions = collectPolicyPermissions(workspaceRoot);
|
|
186
|
+
const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
|
|
187
|
+
const seed = parseSeedFile(workspaceRoot);
|
|
188
|
+
const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
|
|
189
|
+
const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
|
|
190
|
+
const acmePermissions = activePermissions;
|
|
191
|
+
const globexPermissions = permissionSubsetForRestrictedTenant(activePermissions);
|
|
131
192
|
const permissionVocabularyPresent = workos.detected && workos.claimStatus.some((claim) => claim.name === "permissions" && claim.ok);
|
|
132
193
|
const checks = [
|
|
133
194
|
{
|
|
@@ -137,13 +198,35 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
137
198
|
},
|
|
138
199
|
{
|
|
139
200
|
id: "permission-claim",
|
|
140
|
-
ok: permissionVocabularyPresent,
|
|
141
|
-
evidence:
|
|
201
|
+
ok: activePermissions.length > 0 && permissionVocabularyPresent,
|
|
202
|
+
evidence: activePermissions.length > 0
|
|
203
|
+
? `Forge permissions claim maps to WorkOS permissions: ${activePermissions.join(", ")}.`
|
|
204
|
+
: "No active policy permissions were found in the generated policy registry.",
|
|
142
205
|
},
|
|
143
206
|
{
|
|
144
207
|
id: "seed-organizations",
|
|
145
208
|
ok: rootSeedPresent || generatedSeedPresent,
|
|
146
|
-
evidence: rootSeedPresent
|
|
209
|
+
evidence: rootSeedPresent
|
|
210
|
+
? "workos-seed.yml exists at app root."
|
|
211
|
+
: generatedSeedPresent
|
|
212
|
+
? "generated WorkOS seed exists."
|
|
213
|
+
: "no WorkOS seed file found at app root or in generated integrations.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "seed-permission-coverage",
|
|
217
|
+
ok: activePermissions.length > 0 && missingSeedPermissions.length === 0,
|
|
218
|
+
evidence: activePermissions.length === 0
|
|
219
|
+
? "No active policy permissions were found in the generated policy registry."
|
|
220
|
+
: missingSeedPermissions.length === 0
|
|
221
|
+
? `seed covers active app permission(s): ${activePermissions.join(", ")}`
|
|
222
|
+
: `seed missing active app permission(s): ${missingSeedPermissions.join(", ")}`,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "seed-resource-coverage",
|
|
226
|
+
ok: missingSeedResources.length === 0,
|
|
227
|
+
evidence: missingSeedResources.length === 0
|
|
228
|
+
? `seed covers expected app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
|
|
229
|
+
: `seed missing expected app resource type(s): ${missingSeedResources.join(", ")}`,
|
|
147
230
|
},
|
|
148
231
|
{
|
|
149
232
|
id: "agent-auth-metadata",
|
|
@@ -155,14 +238,21 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
155
238
|
scenario: "multi-tenant",
|
|
156
239
|
ok: checks.every((check) => check.ok),
|
|
157
240
|
claims: {
|
|
158
|
-
acme: { organization_id: "org_acme", role: "owner", permissions },
|
|
159
|
-
globex: { organization_id: "org_globex", role: "member", permissions:
|
|
241
|
+
acme: { organization_id: "org_acme", role: "owner", permissions: acmePermissions },
|
|
242
|
+
globex: { organization_id: "org_globex", role: "member", permissions: globexPermissions },
|
|
243
|
+
},
|
|
244
|
+
appContract: {
|
|
245
|
+
activePermissions,
|
|
246
|
+
expectedResourceTypes,
|
|
247
|
+
seedPath: seed.path,
|
|
248
|
+
seedPermissions: seed.permissions,
|
|
249
|
+
seedResourceTypes: seed.resourceTypes,
|
|
160
250
|
},
|
|
161
251
|
invariants: [
|
|
162
252
|
"Acme and Globex must use different organization_id claim values.",
|
|
163
253
|
"Tenant-scoped reads must include the active organization_id.",
|
|
164
254
|
"Tenant-scoped writes must verify the resource tenant before mutation.",
|
|
165
|
-
"Role-only UI affordances are not sufficient; policies must use
|
|
255
|
+
"Role-only UI affordances are not sufficient; policies must use the app's generated permission vocabulary.",
|
|
166
256
|
],
|
|
167
257
|
checks,
|
|
168
258
|
nextActions: [
|
|
@@ -304,7 +394,31 @@ export async function runAuthCommand(
|
|
|
304
394
|
};
|
|
305
395
|
}
|
|
306
396
|
if (options.subcommand === "check") {
|
|
307
|
-
|
|
397
|
+
const checked = validateConfig(options.workspaceRoot);
|
|
398
|
+
if (!options.prod) {
|
|
399
|
+
return checked;
|
|
400
|
+
}
|
|
401
|
+
const posture = buildAuthPosture(options.workspaceRoot);
|
|
402
|
+
const productionReady = posture.productionReady === true;
|
|
403
|
+
return {
|
|
404
|
+
ok: checked.ok && productionReady,
|
|
405
|
+
mode: checked.mode,
|
|
406
|
+
data: {
|
|
407
|
+
...(checked.data && typeof checked.data === "object" ? checked.data as Record<string, unknown> : {}),
|
|
408
|
+
production: true,
|
|
409
|
+
authPosture: posture,
|
|
410
|
+
productionCheck: {
|
|
411
|
+
ok: productionReady,
|
|
412
|
+
reason: posture.reason,
|
|
413
|
+
requiredMode: "jwt or oidc",
|
|
414
|
+
localOnlyModeRejected: posture.localOnly === true,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
error: productionReady
|
|
418
|
+
? checked.error
|
|
419
|
+
: { code: "FORGE_AUTH_MODE_INVALID", message: "production auth requires FORGE_AUTH_MODE=jwt or oidc; dev-headers is local-only" },
|
|
420
|
+
exitCode: checked.ok && productionReady ? 0 : 1,
|
|
421
|
+
};
|
|
308
422
|
}
|
|
309
423
|
if (options.subcommand === "prove") {
|
|
310
424
|
const checked = validateConfig(options.workspaceRoot);
|
|
@@ -405,7 +519,51 @@ export function formatAuthJson(result: AuthCommandResult): string {
|
|
|
405
519
|
return `${JSON.stringify(result, null, 2)}\n`;
|
|
406
520
|
}
|
|
407
521
|
|
|
522
|
+
function authPostureFromResult(result: AuthCommandResult): Record<string, unknown> | null {
|
|
523
|
+
const data = result.data && typeof result.data === "object" ? result.data as Record<string, unknown> : {};
|
|
524
|
+
const direct = data.schemaVersion === "0.1.0" && "classification" in data ? data : null;
|
|
525
|
+
const nested = data.authPosture && typeof data.authPosture === "object"
|
|
526
|
+
? data.authPosture as Record<string, unknown>
|
|
527
|
+
: null;
|
|
528
|
+
return direct ?? nested;
|
|
529
|
+
}
|
|
530
|
+
|
|
408
531
|
export function formatAuthHuman(result: AuthCommandResult): string {
|
|
532
|
+
const posture = authPostureFromResult(result);
|
|
533
|
+
if (posture) {
|
|
534
|
+
const classification = typeof posture.classification === "string" ? posture.classification : result.mode;
|
|
535
|
+
const reason = typeof posture.reason === "string" ? posture.reason : result.error?.message;
|
|
536
|
+
const nextActions = Array.isArray(posture.nextActions) ? posture.nextActions.filter((item): item is string => typeof item === "string") : [];
|
|
537
|
+
const lines = [
|
|
538
|
+
`Auth ${result.mode}: ${result.ok ? "ok" : "failed"} (${classification})`,
|
|
539
|
+
`Production ready: ${posture.productionReady === true ? "yes" : "no"}`,
|
|
540
|
+
...(reason ? [`Reason: ${reason}`] : []),
|
|
541
|
+
];
|
|
542
|
+
const localDevHeaders = posture.localDevHeaders && typeof posture.localDevHeaders === "object"
|
|
543
|
+
? posture.localDevHeaders as Record<string, unknown>
|
|
544
|
+
: null;
|
|
545
|
+
const acceptedHeaders = Array.isArray(localDevHeaders?.acceptedHeaders)
|
|
546
|
+
? localDevHeaders.acceptedHeaders.filter((item): item is string => typeof item === "string")
|
|
547
|
+
: posture.localOnly === true || result.mode === "dev-headers"
|
|
548
|
+
? [
|
|
549
|
+
"x-forge-user-id",
|
|
550
|
+
"x-forge-tenant-id",
|
|
551
|
+
"x-forge-role",
|
|
552
|
+
"x-forge-roles",
|
|
553
|
+
"x-forge-permissions",
|
|
554
|
+
]
|
|
555
|
+
: [];
|
|
556
|
+
if (acceptedHeaders.length > 0) {
|
|
557
|
+
lines.push("Local dev headers:", ...acceptedHeaders.map((header) => ` ${header}`));
|
|
558
|
+
}
|
|
559
|
+
if (!result.ok && result.error) {
|
|
560
|
+
lines.push(`${result.error.code}: ${result.error.message}`);
|
|
561
|
+
}
|
|
562
|
+
if (nextActions.length > 0) {
|
|
563
|
+
lines.push("", "Next:", ...nextActions.map((action) => ` ${action}`));
|
|
564
|
+
}
|
|
565
|
+
return `${lines.join("\n")}\n`;
|
|
566
|
+
}
|
|
409
567
|
if (!result.ok) {
|
|
410
568
|
return `Auth ${result.mode}: failed\n${result.error?.code}: ${result.error?.message}\n`;
|
|
411
569
|
}
|
|
@@ -188,8 +188,11 @@ import {
|
|
|
188
188
|
formatDoctorJson,
|
|
189
189
|
formatPgliteDoctorHuman,
|
|
190
190
|
formatPgliteDoctorJson,
|
|
191
|
+
formatRuntimeDoctorHuman,
|
|
192
|
+
formatRuntimeDoctorJson,
|
|
191
193
|
runPgliteDoctorCommand,
|
|
192
194
|
runDoctorCommand,
|
|
195
|
+
runRuntimeDoctorCommand,
|
|
193
196
|
} from "./doctor.ts";
|
|
194
197
|
import {
|
|
195
198
|
formatWindowsDoctorHuman,
|
|
@@ -210,6 +213,17 @@ import {
|
|
|
210
213
|
formatWorkOSJson,
|
|
211
214
|
runWorkOSCommand,
|
|
212
215
|
} from "./workos.ts";
|
|
216
|
+
import {
|
|
217
|
+
formatDeployHuman,
|
|
218
|
+
formatDeployJson,
|
|
219
|
+
type DeployCommandResult,
|
|
220
|
+
runDeployCommand,
|
|
221
|
+
} from "./deploy.ts";
|
|
222
|
+
import {
|
|
223
|
+
formatFieldTestHuman,
|
|
224
|
+
formatFieldTestJson,
|
|
225
|
+
runFieldTestCommand,
|
|
226
|
+
} from "./field-test.ts";
|
|
213
227
|
import {
|
|
214
228
|
formatLastHuman,
|
|
215
229
|
formatLastJson,
|
|
@@ -1128,8 +1142,9 @@ interface ReleaseDoctorCheck {
|
|
|
1128
1142
|
name: string;
|
|
1129
1143
|
ok: boolean;
|
|
1130
1144
|
requiredForPublish: boolean;
|
|
1145
|
+
requiredForProduction?: boolean;
|
|
1131
1146
|
state?: string;
|
|
1132
|
-
result: ReleaseCommandResult | SelfHostCommandResult | DocsCheckResult | PackagePackCheckResult;
|
|
1147
|
+
result: ReleaseCommandResult | SelfHostCommandResult | DocsCheckResult | DeployCommandResult | PackagePackCheckResult;
|
|
1133
1148
|
}
|
|
1134
1149
|
|
|
1135
1150
|
interface PackagePackCheckResult {
|
|
@@ -1150,10 +1165,13 @@ interface ReleaseDoctorResult {
|
|
|
1150
1165
|
schemaVersion: "0.1.0";
|
|
1151
1166
|
ok: boolean;
|
|
1152
1167
|
readyToPublish: boolean;
|
|
1168
|
+
readyForProductionDeploy: boolean;
|
|
1153
1169
|
summary: {
|
|
1154
1170
|
checks: number;
|
|
1155
1171
|
failed: string[];
|
|
1156
1172
|
notPrepared: string[];
|
|
1173
|
+
publishBlockers: string[];
|
|
1174
|
+
productionBlockers: string[];
|
|
1157
1175
|
};
|
|
1158
1176
|
checks: ReleaseDoctorCheck[];
|
|
1159
1177
|
nextActions: string[];
|
|
@@ -1269,6 +1287,13 @@ export async function runReleaseDoctorCommand(command: Extract<ForgeCommand, { k
|
|
|
1269
1287
|
workspaceRoot: command.workspaceRoot,
|
|
1270
1288
|
json: command.json,
|
|
1271
1289
|
});
|
|
1290
|
+
const deployProduction = await runDeployCommand({
|
|
1291
|
+
subcommand: "check",
|
|
1292
|
+
workspaceRoot: command.workspaceRoot,
|
|
1293
|
+
json: command.json,
|
|
1294
|
+
target: "docker",
|
|
1295
|
+
production: true,
|
|
1296
|
+
});
|
|
1272
1297
|
const packagePack = runPackagePackDryRun(command.workspaceRoot);
|
|
1273
1298
|
const checks: ReleaseDoctorCheck[] = [
|
|
1274
1299
|
{
|
|
@@ -1291,6 +1316,13 @@ export async function runReleaseDoctorCommand(command: Extract<ForgeCommand, { k
|
|
|
1291
1316
|
state: selfHost.state,
|
|
1292
1317
|
result: selfHost,
|
|
1293
1318
|
},
|
|
1319
|
+
{
|
|
1320
|
+
name: "deploy-production",
|
|
1321
|
+
ok: deployProduction.ok,
|
|
1322
|
+
requiredForPublish: false,
|
|
1323
|
+
requiredForProduction: true,
|
|
1324
|
+
result: deployProduction,
|
|
1325
|
+
},
|
|
1294
1326
|
{
|
|
1295
1327
|
name: "docs",
|
|
1296
1328
|
ok: docs.ok,
|
|
@@ -1308,19 +1340,32 @@ export async function runReleaseDoctorCommand(command: Extract<ForgeCommand, { k
|
|
|
1308
1340
|
const notPrepared = checks
|
|
1309
1341
|
.filter((check) => check.state === "missing-prepared-release" || check.state === "not-prepared")
|
|
1310
1342
|
.map((check) => check.name);
|
|
1311
|
-
const
|
|
1343
|
+
const publishBlockers = checks
|
|
1344
|
+
.filter((check) => check.requiredForPublish && !check.ok)
|
|
1345
|
+
.map((check) => check.name);
|
|
1346
|
+
const productionBlockers = checks
|
|
1347
|
+
.filter((check) => check.requiredForProduction && !check.ok)
|
|
1348
|
+
.map((check) => check.name);
|
|
1349
|
+
const notPreparedPublishBlockers = checks
|
|
1350
|
+
.filter((check) => check.requiredForPublish && (check.state === "missing-prepared-release" || check.state === "not-prepared"))
|
|
1351
|
+
.map((check) => check.name);
|
|
1352
|
+
const readyToPublish = publishBlockers.length === 0 && notPreparedPublishBlockers.length === 0;
|
|
1353
|
+
const readyForProductionDeploy = productionBlockers.length === 0;
|
|
1312
1354
|
return {
|
|
1313
1355
|
schemaVersion: "0.1.0",
|
|
1314
|
-
ok:
|
|
1356
|
+
ok: readyToPublish,
|
|
1315
1357
|
readyToPublish,
|
|
1358
|
+
readyForProductionDeploy,
|
|
1316
1359
|
summary: {
|
|
1317
1360
|
checks: checks.length,
|
|
1318
1361
|
failed,
|
|
1319
1362
|
notPrepared,
|
|
1363
|
+
publishBlockers,
|
|
1364
|
+
productionBlockers,
|
|
1320
1365
|
},
|
|
1321
1366
|
checks,
|
|
1322
1367
|
nextActions: uniqueNextActions(checks.flatMap((check) => check.result.nextActions ?? [])),
|
|
1323
|
-
exitCode:
|
|
1368
|
+
exitCode: readyToPublish ? 0 : 1,
|
|
1324
1369
|
};
|
|
1325
1370
|
}
|
|
1326
1371
|
|
|
@@ -1850,6 +1895,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1850
1895
|
process.stdout.write(command.json ? formatPgliteDoctorJson(result) : formatPgliteDoctorHuman(result));
|
|
1851
1896
|
return result.exitCode;
|
|
1852
1897
|
}
|
|
1898
|
+
if (command.target === "runtime") {
|
|
1899
|
+
const result = await runRuntimeDoctorCommand({ workspaceRoot: command.workspaceRoot });
|
|
1900
|
+
process.stdout.write(command.json ? formatRuntimeDoctorJson(result) : formatRuntimeDoctorHuman(result));
|
|
1901
|
+
return result.exitCode;
|
|
1902
|
+
}
|
|
1853
1903
|
const result = await runDoctorCommand({ workspaceRoot: command.workspaceRoot });
|
|
1854
1904
|
if (command.json) {
|
|
1855
1905
|
process.stdout.write(formatDoctorJson(result));
|
|
@@ -1906,6 +1956,16 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1906
1956
|
}
|
|
1907
1957
|
return result.exitCode;
|
|
1908
1958
|
}
|
|
1959
|
+
case "deploy": {
|
|
1960
|
+
const result = await runDeployCommand(command);
|
|
1961
|
+
process.stdout.write(command.json ? formatDeployJson(result) : formatDeployHuman(result));
|
|
1962
|
+
return result.exitCode;
|
|
1963
|
+
}
|
|
1964
|
+
case "field-test": {
|
|
1965
|
+
const result = await runFieldTestCommand(command);
|
|
1966
|
+
process.stdout.write(command.json ? formatFieldTestJson(result) : formatFieldTestHuman(result));
|
|
1967
|
+
return result.exitCode;
|
|
1968
|
+
}
|
|
1909
1969
|
case "rls": {
|
|
1910
1970
|
const result = await runRlsCommand(command);
|
|
1911
1971
|
if (command.json) {
|
|
@@ -1934,7 +1994,15 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1934
1994
|
[
|
|
1935
1995
|
`release doctor ${result.ok ? "ok" : "failed"}`,
|
|
1936
1996
|
`ready to publish: ${result.readyToPublish ? "yes" : "no"}`,
|
|
1937
|
-
|
|
1997
|
+
`ready for production deploy: ${result.readyForProductionDeploy ? "yes" : "no"}`,
|
|
1998
|
+
...result.checks.map((check) => {
|
|
1999
|
+
const scope = check.requiredForPublish
|
|
2000
|
+
? "publish"
|
|
2001
|
+
: check.requiredForProduction
|
|
2002
|
+
? "production"
|
|
2003
|
+
: "optional";
|
|
2004
|
+
return `${check.ok ? "ok" : "fail"} ${check.name} [${scope}]${check.state ? ` (${check.state})` : ""}`;
|
|
2005
|
+
}),
|
|
1938
2006
|
...(result.nextActions.length > 0 ? ["", "Next:", ...result.nextActions.map((action) => ` ${action}`)] : []),
|
|
1939
2007
|
].join("\n").concat("\n"),
|
|
1940
2008
|
);
|