@treeseed/sdk 0.10.12 → 0.10.14
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/dist/api/auth/d1-store.js +20 -1
- package/dist/db/market-schema.d.ts +7515 -6263
- package/dist/db/market-schema.js +71 -15
- package/dist/index.d.ts +3 -1
- package/dist/index.js +18 -0
- package/dist/market-client.d.ts +107 -0
- package/dist/market-client.js +91 -1
- package/dist/operations/services/deploy.d.ts +8 -0
- package/dist/operations/services/deploy.js +12 -1
- package/dist/operations/services/github-api.d.ts +83 -0
- package/dist/operations/services/github-api.js +167 -0
- package/dist/operations/services/mailpit-runtime.d.ts +13 -2
- package/dist/operations/services/mailpit-runtime.js +19 -14
- package/dist/operations/services/project-web-monitor.d.ts +15 -0
- package/dist/operations/services/project-web-monitor.js +260 -0
- package/dist/operations/services/railway-api.js +2 -2
- package/dist/operations.d.ts +1 -0
- package/dist/operations.js +11 -1
- package/dist/platform-operation-store.d.ts +4 -0
- package/dist/platform-operation-store.js +29 -3
- package/dist/platform-operations.d.ts +8 -0
- package/dist/platform-operations.js +19 -0
- package/dist/remote.js +6 -6
- package/dist/scripts/test-cloudflare-local.js +1 -1
- package/dist/scripts/workspace-command-e2e.js +3 -1
- package/dist/sdk-types.d.ts +109 -1
- package/dist/sdk-types.js +5 -1
- package/drizzle/market/0000_market_control_plane.sql +80 -14
- package/drizzle/market/0002_user_email_addresses.sql +26 -0
- package/package.json +1 -1
- package/templates/github/deploy-web.workflow.yml +1 -0
|
@@ -58,6 +58,59 @@ export type GitHubWorkflowProgressEvent = {
|
|
|
58
58
|
completedJobs?: GitHubWorkflowJobSummary[];
|
|
59
59
|
failedJobs?: GitHubWorkflowJobSummary[];
|
|
60
60
|
};
|
|
61
|
+
export interface GitHubWorkflowDispatchResult {
|
|
62
|
+
repository: string;
|
|
63
|
+
workflow: string;
|
|
64
|
+
branch: string;
|
|
65
|
+
inputs: Record<string, string> | undefined;
|
|
66
|
+
status: number | null;
|
|
67
|
+
dispatchedAt: string;
|
|
68
|
+
}
|
|
69
|
+
export interface GitHubWorkflowCancellationResult {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
supported: boolean;
|
|
72
|
+
repository: string | null;
|
|
73
|
+
runId: number | null;
|
|
74
|
+
url?: string | null;
|
|
75
|
+
message: string;
|
|
76
|
+
cancelledAt?: string | null;
|
|
77
|
+
}
|
|
78
|
+
export interface GitHubWorkflowFileStatus {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
exists: boolean | null;
|
|
81
|
+
repository: string;
|
|
82
|
+
workflow: string;
|
|
83
|
+
url: string | null;
|
|
84
|
+
message: string;
|
|
85
|
+
}
|
|
86
|
+
export interface GitHubWorkflowFailureSummaryInput {
|
|
87
|
+
repository?: string | null;
|
|
88
|
+
workflow?: string | null;
|
|
89
|
+
runId?: number | string | null;
|
|
90
|
+
runUrl?: string | null;
|
|
91
|
+
conclusion?: string | null;
|
|
92
|
+
failedJobName?: string | null;
|
|
93
|
+
lastActiveStep?: string | null;
|
|
94
|
+
message?: string | null;
|
|
95
|
+
blockerCode?: string | null;
|
|
96
|
+
retrySafe?: boolean;
|
|
97
|
+
resumeSafe?: boolean;
|
|
98
|
+
}
|
|
99
|
+
export interface GitHubWorkflowFailureSummary {
|
|
100
|
+
summary: string;
|
|
101
|
+
provider: 'github';
|
|
102
|
+
repository: string | null;
|
|
103
|
+
workflow: string | null;
|
|
104
|
+
runId: number | null;
|
|
105
|
+
runUrl: string | null;
|
|
106
|
+
inspectCommand: string | null;
|
|
107
|
+
failedJobName: string | null;
|
|
108
|
+
lastActiveStep: string | null;
|
|
109
|
+
conclusion: string | null;
|
|
110
|
+
retrySafe: boolean;
|
|
111
|
+
resumeSafe: boolean;
|
|
112
|
+
blockerCode: string;
|
|
113
|
+
}
|
|
61
114
|
export declare function resolveGitHubApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
|
|
62
115
|
export declare function parseGitHubRepositorySlug(value: string): {
|
|
63
116
|
owner: string;
|
|
@@ -147,6 +200,36 @@ export declare function upsertGitHubRepositoryVariableWithGhCli(repository: stri
|
|
|
147
200
|
}, name: string, value: string, { env, }?: {
|
|
148
201
|
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
149
202
|
}): void;
|
|
203
|
+
export declare function formatGitHubWorkflowFailure(input?: GitHubWorkflowFailureSummaryInput): GitHubWorkflowFailureSummary;
|
|
204
|
+
export declare function dispatchGitHubWorkflowRun(repository: string | {
|
|
205
|
+
owner: string;
|
|
206
|
+
name: string;
|
|
207
|
+
}, { client, workflow, branch, inputs, }: {
|
|
208
|
+
client?: GitHubApiClient;
|
|
209
|
+
workflow?: string;
|
|
210
|
+
branch: string;
|
|
211
|
+
inputs?: Record<string, string>;
|
|
212
|
+
}): Promise<GitHubWorkflowDispatchResult>;
|
|
213
|
+
export declare function cancelGitHubWorkflowRun(repository: string | {
|
|
214
|
+
owner: string;
|
|
215
|
+
name: string;
|
|
216
|
+
} | null | undefined, runId: number | string | null | undefined, { client }?: {
|
|
217
|
+
client?: GitHubApiClient;
|
|
218
|
+
}): Promise<GitHubWorkflowCancellationResult>;
|
|
219
|
+
export declare function getGitHubWorkflowFileStatus(repository: string | {
|
|
220
|
+
owner: string;
|
|
221
|
+
name: string;
|
|
222
|
+
}, workflow?: string, { client }?: {
|
|
223
|
+
client?: GitHubApiClient;
|
|
224
|
+
}): Promise<GitHubWorkflowFileStatus>;
|
|
225
|
+
export declare function getLatestGitHubWorkflowRun(repository: string | {
|
|
226
|
+
owner: string;
|
|
227
|
+
name: string;
|
|
228
|
+
}, { client, workflow, branch, }?: {
|
|
229
|
+
client?: GitHubApiClient;
|
|
230
|
+
workflow?: string;
|
|
231
|
+
branch?: string | null;
|
|
232
|
+
}): Promise<GitHubWorkflowRunSummary | null>;
|
|
150
233
|
export declare function waitForGitHubWorkflowRunCompletion(repository: string | {
|
|
151
234
|
owner: string;
|
|
152
235
|
name: string;
|
|
@@ -548,6 +548,168 @@ function normalizeWorkflowJob(job) {
|
|
|
548
548
|
})) : []
|
|
549
549
|
};
|
|
550
550
|
}
|
|
551
|
+
function workflowInspectCommand(repository, runId) {
|
|
552
|
+
return repository && runId ? `gh run view ${runId} --repo ${repository} --log-failed` : null;
|
|
553
|
+
}
|
|
554
|
+
function formatGitHubWorkflowFailure(input = {}) {
|
|
555
|
+
const repository = typeof input.repository === "string" && input.repository.trim() ? input.repository.trim() : null;
|
|
556
|
+
const workflow = typeof input.workflow === "string" && input.workflow.trim() ? input.workflow.trim() : null;
|
|
557
|
+
const numericRunId = Number(input.runId);
|
|
558
|
+
const runId = Number.isFinite(numericRunId) && numericRunId > 0 ? numericRunId : null;
|
|
559
|
+
const runUrl = typeof input.runUrl === "string" && input.runUrl.trim() ? input.runUrl.trim() : null;
|
|
560
|
+
const conclusion = typeof input.conclusion === "string" && input.conclusion.trim() ? input.conclusion.trim() : null;
|
|
561
|
+
const failedJobName = typeof input.failedJobName === "string" && input.failedJobName.trim() ? input.failedJobName.trim() : null;
|
|
562
|
+
const lastActiveStep = typeof input.lastActiveStep === "string" && input.lastActiveStep.trim() ? input.lastActiveStep.trim() : null;
|
|
563
|
+
const blockerCode = typeof input.blockerCode === "string" && input.blockerCode.trim() ? input.blockerCode.trim() : conclusion === "cancelled" ? "github_workflow_cancelled" : conclusion === "timed_out" ? "github_workflow_timed_out" : "github_workflow_failed";
|
|
564
|
+
const detail = failedJobName ? ` Failed job: ${failedJobName}.` : lastActiveStep ? ` Last active step: ${lastActiveStep}.` : "";
|
|
565
|
+
const summary = typeof input.message === "string" && input.message.trim() ? input.message.trim() : `${workflow ?? "GitHub workflow"} ${conclusion ? `completed with conclusion ${conclusion}` : "failed"}.${detail}`;
|
|
566
|
+
return {
|
|
567
|
+
summary,
|
|
568
|
+
provider: "github",
|
|
569
|
+
repository,
|
|
570
|
+
workflow,
|
|
571
|
+
runId,
|
|
572
|
+
runUrl,
|
|
573
|
+
inspectCommand: workflowInspectCommand(repository, runId),
|
|
574
|
+
failedJobName,
|
|
575
|
+
lastActiveStep,
|
|
576
|
+
conclusion,
|
|
577
|
+
retrySafe: input.retrySafe ?? true,
|
|
578
|
+
resumeSafe: input.resumeSafe ?? false,
|
|
579
|
+
blockerCode
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
async function dispatchGitHubWorkflowRun(repository, {
|
|
583
|
+
client = createGitHubApiClient(),
|
|
584
|
+
workflow = "deploy-web.yml",
|
|
585
|
+
branch,
|
|
586
|
+
inputs
|
|
587
|
+
}) {
|
|
588
|
+
const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
589
|
+
try {
|
|
590
|
+
const result = await client.rest.actions.createWorkflowDispatch({
|
|
591
|
+
owner,
|
|
592
|
+
repo: name,
|
|
593
|
+
workflow_id: workflow,
|
|
594
|
+
ref: branch,
|
|
595
|
+
inputs
|
|
596
|
+
});
|
|
597
|
+
return {
|
|
598
|
+
repository: `${owner}/${name}`,
|
|
599
|
+
workflow,
|
|
600
|
+
branch,
|
|
601
|
+
inputs,
|
|
602
|
+
status: typeof result.status === "number" ? result.status : null,
|
|
603
|
+
dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
throw normalizeGitHubApiError(error, `Unable to dispatch GitHub workflow ${workflow} in ${owner}/${name}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function cancelGitHubWorkflowRun(repository, runId, { client = createGitHubApiClient() } = {}) {
|
|
610
|
+
if (!repository || !runId) {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
supported: false,
|
|
614
|
+
repository: typeof repository === "string" ? repository : null,
|
|
615
|
+
runId: null,
|
|
616
|
+
message: "GitHub workflow cancellation requires a repository and run id."
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
620
|
+
const numericRunId = Number(runId);
|
|
621
|
+
if (!Number.isFinite(numericRunId) || numericRunId <= 0) {
|
|
622
|
+
return {
|
|
623
|
+
ok: false,
|
|
624
|
+
supported: false,
|
|
625
|
+
repository: `${owner}/${name}`,
|
|
626
|
+
runId: null,
|
|
627
|
+
message: "GitHub workflow cancellation requires a numeric run id."
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
await client.rest.actions.cancelWorkflowRun({
|
|
632
|
+
owner,
|
|
633
|
+
repo: name,
|
|
634
|
+
run_id: numericRunId
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
ok: true,
|
|
638
|
+
supported: true,
|
|
639
|
+
repository: `${owner}/${name}`,
|
|
640
|
+
runId: numericRunId,
|
|
641
|
+
url: `https://github.com/${owner}/${name}/actions/runs/${numericRunId}`,
|
|
642
|
+
message: "GitHub workflow cancellation requested.",
|
|
643
|
+
cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
644
|
+
};
|
|
645
|
+
} catch (error) {
|
|
646
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
647
|
+
if (/not supported|not found|404/iu.test(message)) {
|
|
648
|
+
return {
|
|
649
|
+
ok: false,
|
|
650
|
+
supported: false,
|
|
651
|
+
repository: `${owner}/${name}`,
|
|
652
|
+
runId: numericRunId,
|
|
653
|
+
message: "GitHub workflow cancellation is not supported for this run."
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
throw normalizeGitHubApiError(error, `Unable to cancel GitHub workflow run ${numericRunId} in ${owner}/${name}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function getGitHubWorkflowFileStatus(repository, workflow = "deploy-web.yml", { client = createGitHubApiClient() } = {}) {
|
|
660
|
+
const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
661
|
+
const normalizedWorkflow = workflow.replace(/^\.github\/workflows\//u, "");
|
|
662
|
+
const path = `.github/workflows/${normalizedWorkflow}`;
|
|
663
|
+
try {
|
|
664
|
+
const result = await client.rest.repos.getContent({
|
|
665
|
+
owner,
|
|
666
|
+
repo: name,
|
|
667
|
+
path
|
|
668
|
+
});
|
|
669
|
+
const data = result.data;
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
exists: true,
|
|
673
|
+
repository: `${owner}/${name}`,
|
|
674
|
+
workflow: normalizedWorkflow,
|
|
675
|
+
url: typeof data.html_url === "string" ? data.html_url : `https://github.com/${owner}/${name}/blob/HEAD/${path}`,
|
|
676
|
+
message: `${normalizedWorkflow} is present.`
|
|
677
|
+
};
|
|
678
|
+
} catch (error) {
|
|
679
|
+
const status = typeof error?.status === "number" ? error.status : null;
|
|
680
|
+
if (status === 404) {
|
|
681
|
+
return {
|
|
682
|
+
ok: true,
|
|
683
|
+
exists: false,
|
|
684
|
+
repository: `${owner}/${name}`,
|
|
685
|
+
workflow: normalizedWorkflow,
|
|
686
|
+
url: null,
|
|
687
|
+
message: `${normalizedWorkflow} is missing from ${owner}/${name}.`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
throw normalizeGitHubApiError(error, `Unable to inspect GitHub workflow file ${path} in ${owner}/${name}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function getLatestGitHubWorkflowRun(repository, {
|
|
694
|
+
client = createGitHubApiClient(),
|
|
695
|
+
workflow = "deploy-web.yml",
|
|
696
|
+
branch
|
|
697
|
+
} = {}) {
|
|
698
|
+
const { owner, name } = typeof repository === "string" ? parseGitHubRepositorySlug(repository) : repository;
|
|
699
|
+
try {
|
|
700
|
+
const listed = await client.rest.actions.listWorkflowRuns({
|
|
701
|
+
owner,
|
|
702
|
+
repo: name,
|
|
703
|
+
workflow_id: workflow,
|
|
704
|
+
...branch ? { branch } : {},
|
|
705
|
+
per_page: 1
|
|
706
|
+
});
|
|
707
|
+
const run = listed.data.workflow_runs[0] ?? null;
|
|
708
|
+
return run ? normalizeWorkflowRun(run) : null;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
throw normalizeGitHubApiError(error, `Unable to inspect latest GitHub workflow run ${workflow} in ${owner}/${name}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
551
713
|
async function listWorkflowJobsForProgress(client, owner, repo, runId) {
|
|
552
714
|
try {
|
|
553
715
|
const jobs = await client.rest.actions.listJobsForWorkflowRun({
|
|
@@ -717,11 +879,16 @@ async function ensureGitHubBranchFromBase(repository, branch, {
|
|
|
717
879
|
}
|
|
718
880
|
}
|
|
719
881
|
export {
|
|
882
|
+
cancelGitHubWorkflowRun,
|
|
720
883
|
createGitHubApiClient,
|
|
884
|
+
dispatchGitHubWorkflowRun,
|
|
721
885
|
ensureGitHubActionsEnvironment,
|
|
722
886
|
ensureGitHubBranchFromBase,
|
|
723
887
|
ensureGitHubRepository,
|
|
888
|
+
formatGitHubWorkflowFailure,
|
|
724
889
|
getGitHubRepository,
|
|
890
|
+
getGitHubWorkflowFileStatus,
|
|
891
|
+
getLatestGitHubWorkflowRun,
|
|
725
892
|
listGitHubEnvironmentSecretNames,
|
|
726
893
|
listGitHubEnvironmentVariableNames,
|
|
727
894
|
listGitHubRepositorySecretNames,
|
|
@@ -3,7 +3,18 @@ export type TreeseedMailpitContainer = {
|
|
|
3
3
|
image: string;
|
|
4
4
|
ports: string;
|
|
5
5
|
};
|
|
6
|
+
type RunDocker = typeof runDocker;
|
|
7
|
+
declare function runDocker(args: string[], options?: {}): import("child_process").SpawnSyncReturns<string>;
|
|
6
8
|
export declare function dockerIsAvailable(): boolean;
|
|
7
|
-
export declare function findRunningMailpitContainer(
|
|
8
|
-
|
|
9
|
+
export declare function findRunningMailpitContainer(options?: {
|
|
10
|
+
run?: RunDocker;
|
|
11
|
+
}): {
|
|
12
|
+
name: string;
|
|
13
|
+
image: string;
|
|
14
|
+
ports: string;
|
|
15
|
+
} | null;
|
|
16
|
+
export declare function stopKnownMailpitContainers(options?: {
|
|
17
|
+
run?: RunDocker;
|
|
18
|
+
}): boolean;
|
|
9
19
|
export declare function streamKnownMailpitLogs(): void;
|
|
20
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
const EXPECTED_PORTS = ["1025->1025/tcp", "8025->8025/tcp"];
|
|
3
|
-
const KNOWN_MAILPIT_NAMES = ["treeseed_mailpit", "
|
|
3
|
+
const KNOWN_MAILPIT_NAMES = ["treeseed_mailpit", "docs_mailpit"];
|
|
4
4
|
function runDocker(args, options = {}) {
|
|
5
5
|
return spawnSync("docker", args, {
|
|
6
6
|
encoding: "utf8",
|
|
@@ -23,24 +23,29 @@ function dockerIsAvailable() {
|
|
|
23
23
|
const result = runDocker(["ps", "--format", "{{.Names}} {{.Image}} {{.Ports}}"]);
|
|
24
24
|
return result.status === 0;
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
26
|
+
function findKnownMailpitContainers({ all = false, run = runDocker } = {}) {
|
|
27
|
+
const args = [
|
|
28
|
+
"ps",
|
|
29
|
+
...all ? ["-a"] : [],
|
|
30
|
+
"--format",
|
|
31
|
+
"{{.Names}} {{.Image}} {{.Ports}}"
|
|
32
|
+
];
|
|
33
|
+
const result = run(args);
|
|
28
34
|
if (result.status !== 0) {
|
|
29
|
-
return
|
|
35
|
+
return [];
|
|
30
36
|
}
|
|
31
|
-
return parseDockerPsOutput(result.stdout).
|
|
37
|
+
return parseDockerPsOutput(result.stdout).filter(isCompatibleMailpitContainer);
|
|
32
38
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
function findRunningMailpitContainer(options = {}) {
|
|
40
|
+
return findKnownMailpitContainers(options).at(0) ?? null;
|
|
41
|
+
}
|
|
42
|
+
function stopKnownMailpitContainers(options = {}) {
|
|
43
|
+
const run = options.run ?? runDocker;
|
|
44
|
+
const containers = findKnownMailpitContainers({ all: true, run });
|
|
45
|
+
if (containers.length === 0) {
|
|
36
46
|
return true;
|
|
37
47
|
}
|
|
38
|
-
|
|
39
|
-
if (stopResult.status !== 0) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
const removeResult = runDocker(["rm", "-f", container.name]);
|
|
43
|
-
return removeResult.status === 0;
|
|
48
|
+
return containers.every((container) => run(["rm", "-f", container.name]).status === 0);
|
|
44
49
|
}
|
|
45
50
|
function streamKnownMailpitLogs() {
|
|
46
51
|
const container = findRunningMailpitContainer();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type GitHubApiClient } from './github-api.ts';
|
|
2
|
+
import type { ProjectDeploymentEnvironment, ProjectWebMonitorResult } from '../../sdk-types.ts';
|
|
3
|
+
export declare function buildProjectWebMonitorResult(input: {
|
|
4
|
+
environment: ProjectDeploymentEnvironment;
|
|
5
|
+
action?: string | null;
|
|
6
|
+
repository?: Record<string, unknown> | null;
|
|
7
|
+
workflowFile?: string | null;
|
|
8
|
+
target?: Record<string, unknown> | null;
|
|
9
|
+
externalWorkflow?: Record<string, unknown> | null;
|
|
10
|
+
workflowResult?: Record<string, any> | null;
|
|
11
|
+
githubClient?: GitHubApiClient | null;
|
|
12
|
+
fetchImpl?: typeof fetch | null;
|
|
13
|
+
mockExternal?: boolean;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
}): Promise<ProjectWebMonitorResult>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getGitHubWorkflowFileStatus,
|
|
3
|
+
getLatestGitHubWorkflowRun
|
|
4
|
+
} from "./github-api.js";
|
|
5
|
+
const FORBIDDEN_MONITOR_FIELDS = /* @__PURE__ */ new Set([
|
|
6
|
+
"capacityProviderId",
|
|
7
|
+
"laneId",
|
|
8
|
+
"grantId",
|
|
9
|
+
"workerPoolId",
|
|
10
|
+
"runtimeHostId",
|
|
11
|
+
"railwayServiceId",
|
|
12
|
+
"runnerToken"
|
|
13
|
+
]);
|
|
14
|
+
function text(value, fallback = "") {
|
|
15
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
16
|
+
}
|
|
17
|
+
function objectValue(value) {
|
|
18
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
19
|
+
}
|
|
20
|
+
function repositorySlug(repository) {
|
|
21
|
+
const record = objectValue(repository);
|
|
22
|
+
const owner = text(record.owner);
|
|
23
|
+
const name = text(record.name);
|
|
24
|
+
return owner && name ? `${owner}/${name}` : null;
|
|
25
|
+
}
|
|
26
|
+
function externalUrl(...values) {
|
|
27
|
+
for (const value of values) {
|
|
28
|
+
const raw = text(value);
|
|
29
|
+
if (!raw) continue;
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(raw);
|
|
32
|
+
if (url.protocol === "http:" || url.protocol === "https:") return url.href;
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function redact(value) {
|
|
40
|
+
if (Array.isArray(value)) return value.map((entry) => redact(entry));
|
|
41
|
+
if (!value || typeof value !== "object") return value;
|
|
42
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !FORBIDDEN_MONITOR_FIELDS.has(key)).filter(([key]) => !/(?:secret|token|password|apiKey|privateKey)/iu.test(key)).map(([key, entry]) => [key, redact(entry)]));
|
|
43
|
+
}
|
|
44
|
+
function check(input) {
|
|
45
|
+
return redact(input);
|
|
46
|
+
}
|
|
47
|
+
function workflowStatusFromConclusion(conclusion) {
|
|
48
|
+
if (conclusion === "success") return "passed";
|
|
49
|
+
if (conclusion === "cancelled" || conclusion === "failure" || conclusion === "timed_out") return "failed";
|
|
50
|
+
if (conclusion) return "warning";
|
|
51
|
+
return "warning";
|
|
52
|
+
}
|
|
53
|
+
function statusFromChecks(checks) {
|
|
54
|
+
if (checks.some((entry) => entry.status === "failed")) return "failed";
|
|
55
|
+
if (checks.some((entry) => entry.status === "warning")) return "degraded";
|
|
56
|
+
if (checks.some((entry) => entry.status === "passed")) return "healthy";
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
function workflowRunCheck(input) {
|
|
60
|
+
const run = input.workflowResult?.runId ? input.workflowResult : input.latestWorkflowRun;
|
|
61
|
+
if (!run) {
|
|
62
|
+
return check({
|
|
63
|
+
key: "latest_workflow",
|
|
64
|
+
label: "Latest workflow",
|
|
65
|
+
status: "skipped",
|
|
66
|
+
source: "github",
|
|
67
|
+
summary: input.repository ? "No workflow run was recorded for this deployment yet." : "Repository is unavailable, so workflow state was skipped."
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const conclusion = text(run.conclusion, "");
|
|
71
|
+
const runStatus = text(run.status, "");
|
|
72
|
+
const status = runStatus === "completed" ? workflowStatusFromConclusion(conclusion) : "warning";
|
|
73
|
+
const runId = Number(run.runId ?? run.id);
|
|
74
|
+
const inspectCommand = input.repository && Number.isFinite(runId) && runId > 0 ? `gh run view ${runId} --repo ${input.repository} --log-failed` : void 0;
|
|
75
|
+
return check({
|
|
76
|
+
key: "latest_workflow",
|
|
77
|
+
label: "Latest workflow",
|
|
78
|
+
status,
|
|
79
|
+
source: "github",
|
|
80
|
+
summary: status === "passed" ? `${input.workflowFile} completed successfully.` : runStatus === "completed" ? `${input.workflowFile} completed with conclusion ${conclusion || "unknown"}.` : `${input.workflowFile} is ${runStatus || "active"}.`,
|
|
81
|
+
url: externalUrl(run.url, run.runUrl),
|
|
82
|
+
...inspectCommand ? { inspectCommand } : {}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function workflowFileCheck(input) {
|
|
86
|
+
if (!input.repository) {
|
|
87
|
+
return check({
|
|
88
|
+
key: "workflow_file",
|
|
89
|
+
label: "Workflow file",
|
|
90
|
+
status: "failed",
|
|
91
|
+
source: "github",
|
|
92
|
+
summary: "GitHub repository is not configured."
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (input.mockExternal || input.dryRun) {
|
|
96
|
+
return check({
|
|
97
|
+
key: "workflow_file",
|
|
98
|
+
label: "Workflow file",
|
|
99
|
+
status: "passed",
|
|
100
|
+
source: "github",
|
|
101
|
+
summary: `${input.workflowFile} is assumed present in mock mode.`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (!input.githubClient) {
|
|
105
|
+
return check({
|
|
106
|
+
key: "workflow_file",
|
|
107
|
+
label: "Workflow file",
|
|
108
|
+
status: "warning",
|
|
109
|
+
source: "github",
|
|
110
|
+
summary: "GitHub credentials are unavailable, so workflow file presence could not be verified."
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const status = await getGitHubWorkflowFileStatus(input.repository, input.workflowFile, { client: input.githubClient }).catch((error) => ({
|
|
114
|
+
ok: false,
|
|
115
|
+
exists: null,
|
|
116
|
+
repository: input.repository,
|
|
117
|
+
workflow: input.workflowFile,
|
|
118
|
+
url: null,
|
|
119
|
+
message: error instanceof Error ? error.message : String(error)
|
|
120
|
+
}));
|
|
121
|
+
return check({
|
|
122
|
+
key: "workflow_file",
|
|
123
|
+
label: "Workflow file",
|
|
124
|
+
status: status.exists === true ? "passed" : status.exists === false ? "failed" : "warning",
|
|
125
|
+
source: "github",
|
|
126
|
+
summary: status.message,
|
|
127
|
+
...status.url ? { url: status.url } : {}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function fetchWithTimeout(fetchImpl, url, timeoutMs) {
|
|
131
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
132
|
+
return await fetchImpl(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
|
|
133
|
+
}
|
|
134
|
+
return await fetchImpl(url, { method: "GET" });
|
|
135
|
+
}
|
|
136
|
+
async function httpCheck(input) {
|
|
137
|
+
if (!input.url) {
|
|
138
|
+
return check({
|
|
139
|
+
key: "http_response",
|
|
140
|
+
label: "HTTP response",
|
|
141
|
+
status: "skipped",
|
|
142
|
+
source: "http",
|
|
143
|
+
summary: "No public URL is available for HTTP probing."
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (input.mockExternal || input.dryRun || !input.fetchImpl) {
|
|
147
|
+
return check({
|
|
148
|
+
key: "http_response",
|
|
149
|
+
label: "HTTP response",
|
|
150
|
+
status: input.mockExternal ? "passed" : "skipped",
|
|
151
|
+
source: "http",
|
|
152
|
+
url: input.url,
|
|
153
|
+
summary: input.mockExternal ? "Mock HTTP probe passed." : "HTTP probe skipped for this monitor run."
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetchWithTimeout(input.fetchImpl, input.url, input.timeoutMs ?? 1e4);
|
|
158
|
+
const status = response.status >= 200 && response.status < 400 ? "passed" : response.status === 401 || response.status === 403 || response.status >= 500 ? "warning" : "failed";
|
|
159
|
+
return check({
|
|
160
|
+
key: "http_response",
|
|
161
|
+
label: "HTTP response",
|
|
162
|
+
status,
|
|
163
|
+
source: "http",
|
|
164
|
+
url: input.url,
|
|
165
|
+
summary: `HTTP probe returned ${response.status}.`
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return check({
|
|
169
|
+
key: "http_response",
|
|
170
|
+
label: "HTTP response",
|
|
171
|
+
status: "failed",
|
|
172
|
+
source: "http",
|
|
173
|
+
url: input.url,
|
|
174
|
+
summary: error instanceof Error ? error.message : "HTTP probe failed."
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function buildProjectWebMonitorResult(input) {
|
|
179
|
+
const repository = repositorySlug(input.repository);
|
|
180
|
+
const workflowFile = text(input.workflowFile ?? input.repository?.workflowFile, "deploy-web.yml");
|
|
181
|
+
const target = objectValue(input.target);
|
|
182
|
+
const targetUrl = externalUrl(target.url, target.baseUrl, target.previewUrl, target.lastDeploymentUrl);
|
|
183
|
+
const latestWorkflowRun = !input.workflowResult && repository && input.githubClient ? await getLatestGitHubWorkflowRun(repository, {
|
|
184
|
+
client: input.githubClient,
|
|
185
|
+
workflow: workflowFile,
|
|
186
|
+
branch: text(input.repository?.branch, "") || null
|
|
187
|
+
}).catch(() => null) : null;
|
|
188
|
+
const checks = [
|
|
189
|
+
workflowRunCheck({
|
|
190
|
+
workflowResult: input.workflowResult ?? input.externalWorkflow ?? null,
|
|
191
|
+
latestWorkflowRun,
|
|
192
|
+
repository,
|
|
193
|
+
workflowFile
|
|
194
|
+
}),
|
|
195
|
+
await workflowFileCheck({
|
|
196
|
+
repository,
|
|
197
|
+
workflowFile,
|
|
198
|
+
githubClient: input.githubClient,
|
|
199
|
+
mockExternal: input.mockExternal,
|
|
200
|
+
dryRun: input.dryRun
|
|
201
|
+
}),
|
|
202
|
+
check({
|
|
203
|
+
key: "web_host",
|
|
204
|
+
label: "Web host",
|
|
205
|
+
status: Object.keys(target).length > 0 ? "passed" : "failed",
|
|
206
|
+
source: "market",
|
|
207
|
+
summary: Object.keys(target).length > 0 ? "Web host target is configured." : "Web host target is missing."
|
|
208
|
+
}),
|
|
209
|
+
check({
|
|
210
|
+
key: "target_url",
|
|
211
|
+
label: "Public URL",
|
|
212
|
+
status: targetUrl ? "passed" : "failed",
|
|
213
|
+
source: "market",
|
|
214
|
+
...targetUrl ? { url: targetUrl } : {},
|
|
215
|
+
summary: targetUrl ? "Deployment target URL is recorded." : "Deployment target URL is missing."
|
|
216
|
+
}),
|
|
217
|
+
await httpCheck({
|
|
218
|
+
url: targetUrl,
|
|
219
|
+
fetchImpl: input.fetchImpl,
|
|
220
|
+
mockExternal: input.mockExternal,
|
|
221
|
+
dryRun: input.dryRun
|
|
222
|
+
}),
|
|
223
|
+
check({
|
|
224
|
+
key: "content_publish",
|
|
225
|
+
label: "Content publish",
|
|
226
|
+
status: input.action === "publish_content" ? "passed" : "skipped",
|
|
227
|
+
source: "sdk",
|
|
228
|
+
summary: input.action === "publish_content" ? "Content publish workflow completed." : "Content publish was not part of this action."
|
|
229
|
+
}),
|
|
230
|
+
check({
|
|
231
|
+
key: "d1_migration",
|
|
232
|
+
label: "D1 migration",
|
|
233
|
+
status: "skipped",
|
|
234
|
+
source: "sdk",
|
|
235
|
+
summary: "No D1 migration result was reported for this deployment."
|
|
236
|
+
}),
|
|
237
|
+
check({
|
|
238
|
+
key: "form_api_route",
|
|
239
|
+
label: "Form/API route",
|
|
240
|
+
status: "skipped",
|
|
241
|
+
source: "http",
|
|
242
|
+
summary: "No form/API route probe was configured for this project."
|
|
243
|
+
})
|
|
244
|
+
];
|
|
245
|
+
const status = statusFromChecks(checks);
|
|
246
|
+
const warnings = checks.filter((entry) => entry.status === "warning").map((entry) => entry.summary);
|
|
247
|
+
const urls = [...new Set(checks.map((entry) => entry.url).filter((url) => Boolean(url)))];
|
|
248
|
+
const result = {
|
|
249
|
+
environment: input.environment,
|
|
250
|
+
status,
|
|
251
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
252
|
+
checks,
|
|
253
|
+
urls,
|
|
254
|
+
warnings
|
|
255
|
+
};
|
|
256
|
+
return redact(result);
|
|
257
|
+
}
|
|
258
|
+
export {
|
|
259
|
+
buildProjectWebMonitorResult
|
|
260
|
+
};
|
|
@@ -283,8 +283,8 @@ async function railwayGraphqlRequest({
|
|
|
283
283
|
apiToken,
|
|
284
284
|
apiUrl,
|
|
285
285
|
fetchImpl = fetch,
|
|
286
|
-
timeoutMs =
|
|
287
|
-
retries =
|
|
286
|
+
timeoutMs = 3e4,
|
|
287
|
+
retries = 8
|
|
288
288
|
}) {
|
|
289
289
|
const token = apiToken || resolveRailwayApiToken(env);
|
|
290
290
|
if (!token) {
|
package/dist/operations.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { TRESEED_OPERATION_SPECS, findTreeseedOperation, listTreeseedOperationNa
|
|
|
2
2
|
export { collectTreeseedConfigSeedValues } from './operations/services/config-runtime.ts';
|
|
3
3
|
export { createKnowledgeHubRepositories, defaultHubContentResolutionPolicy, executeKnowledgeHubLaunch, normalizeKnowledgeHubLaunchIntent, planKnowledgeHubLaunch, planKnowledgeHubRepositories, validateRepositoryHost, } from './operations/services/hub-launch.ts';
|
|
4
4
|
export { TreeseedOperationsSdk } from './operations/runtime.ts';
|
|
5
|
+
export { cancelGitHubWorkflowRun, dispatchGitHubWorkflowRun, formatGitHubWorkflowFailure, waitForGitHubWorkflowRunCompletion, type GitHubWorkflowCancellationResult, type GitHubWorkflowDispatchResult, type GitHubWorkflowFailureSummary, type GitHubWorkflowFailureSummaryInput, type GitHubWorkflowProgressEvent, } from './operations/services/github-api.ts';
|
|
5
6
|
export type { HubContentResolutionPolicy, KnowledgeHubLaunchIntent, KnowledgeHubLaunchPhase, KnowledgeHubLaunchPlan, KnowledgeHubLaunchResult, KnowledgeHubRepositoryPlan, RepositoryHost, } from './operations/services/hub-launch.ts';
|
|
6
7
|
export type { TreeseedOperationContext, TreeseedOperationImplementation, TreeseedOperationId, TreeseedOperationMetadata, TreeseedOperationProvider, TreeseedOperationProviderId, TreeseedOperationRequest, TreeseedOperationResult, TreeseedOperationGroup, } from './operations-types.ts';
|
|
7
8
|
export { TreeseedOperationError } from './operations-types.ts';
|