ras-stack 0.41.1 β†’ 0.42.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/README.md CHANGED
@@ -103,7 +103,7 @@ Three reusable workflows provide the standard pull-request preview lifecycle:
103
103
  - `deploy-dokploy-preview.yml` publishes, resolves, deploys, reports, and removes previews.
104
104
  - `prune-dokploy-previews.yml` cleans up applications and images left behind by interrupted runs.
105
105
 
106
- Applications supply only their package, application prefix, domain, port, environment template, and optional product hook. The shared `DOKPLOY_URL`, `DOKPLOY_API_KEY`, and staging-only `DOKPLOY_ENVIRONMENT_ID` secrets can be configured once at organization level. See [Repository tooling](docs/repository-tooling.md) for the caller contract, private-registry options, and lifecycle hooks.
106
+ Applications supply only their package, application prefix, port, environment template, and optional product hook. They can provide a custom HTTPS domain or let Dokploy generate an HTTP `sslip.io` address. The shared `DOKPLOY_URL`, `DOKPLOY_API_KEY`, and staging-only `DOKPLOY_ENVIRONMENT_ID` secrets can be configured once at organization level. See [Repository tooling](docs/repository-tooling.md) for the caller contract, private-registry options, and lifecycle hooks.
107
107
 
108
108
  ## Guides πŸ“š
109
109
 
@@ -1,2 +1,2 @@
1
1
  export declare function runDokployPreviewCli(arguments_: string[], environment?: NodeJS.ProcessEnv): Promise<void>;
2
- export declare function renderPreviewEnvironment(template: string, prNumber: string): string;
2
+ export declare function renderPreviewEnvironment(template: string, prNumber: string, previewUrl?: string): string;
@@ -1,19 +1,16 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { appendFileSync } from 'node:fs';
3
2
  import { dokployPreviewFromEnvironment, pullRequestNumber } from './dokploy.js';
4
3
  export async function runDokployPreviewCli(arguments_, environment = process.env) {
5
4
  const command = arguments_[0];
6
5
  const { config, manager } = dokployPreviewFromEnvironment(environment);
7
6
  if (command === 'deploy') {
8
7
  const prNumber = pullRequestNumber(required(environment, 'PR_NUMBER'));
9
- const preview = await manager.deploy({
8
+ await manager.deploy({
10
9
  prNumber,
11
10
  image: required(environment, 'PREVIEW_IMAGE'),
12
- environment: renderPreviewEnvironment(required(environment, 'PREVIEW_ENVIRONMENT'), prNumber),
11
+ environment: ({ url }) => renderPreviewEnvironment(required(environment, 'PREVIEW_ENVIRONMENT'), prNumber, url),
13
12
  ...(config.registry ? { registry: config.registry } : {}),
14
13
  });
15
- if (environment.GITHUB_OUTPUT)
16
- appendFileSync(environment.GITHUB_OUTPUT, `preview-url=${preview.url}\n`);
17
14
  return;
18
15
  }
19
16
  if (command === 'delete') {
@@ -29,9 +26,12 @@ export async function runDokployPreviewCli(arguments_, environment = process.env
29
26
  }
30
27
  throw new Error('usage: ras preview dokploy <deploy|delete|prune>');
31
28
  }
32
- export function renderPreviewEnvironment(template, prNumber) {
29
+ export function renderPreviewEnvironment(template, prNumber, previewUrl) {
30
+ if (template.includes('{{PREVIEW_URL}}') && !previewUrl)
31
+ throw new Error('preview URL is required to render PREVIEW_ENVIRONMENT');
33
32
  return template
34
33
  .replaceAll('{{PR_NUMBER}}', pullRequestNumber(prNumber))
34
+ .replaceAll('{{PREVIEW_URL}}', previewUrl ?? '')
35
35
  .replaceAll('{{RANDOM_HEX_32}}', () => randomBytes(32).toString('hex'));
36
36
  }
37
37
  function required(environment, name) {
@@ -1 +1 @@
1
- {"version":3,"file":"dokploy-cli.js","sourceRoot":"","sources":["../../src/preview/dokploy-cli.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { appendFileSync } from 'node:fs'\nimport { dokployPreviewFromEnvironment, pullRequestNumber } from './dokploy.js'\n\nexport async function runDokployPreviewCli(arguments_: string[], environment: NodeJS.ProcessEnv = process.env) {\n const command = arguments_[0]\n const { config, manager } = dokployPreviewFromEnvironment(environment)\n\n if (command === 'deploy') {\n const prNumber = pullRequestNumber(required(environment, 'PR_NUMBER'))\n const preview = await manager.deploy({\n prNumber,\n image: required(environment, 'PREVIEW_IMAGE'),\n environment: renderPreviewEnvironment(required(environment, 'PREVIEW_ENVIRONMENT'), prNumber),\n ...(config.registry ? { registry: config.registry } : {}),\n })\n if (environment.GITHUB_OUTPUT) appendFileSync(environment.GITHUB_OUTPUT, `preview-url=${preview.url}\\n`)\n return\n }\n if (command === 'delete') {\n const prNumber = pullRequestNumber(required(environment, 'PR_NUMBER'))\n console.log((await manager.delete(prNumber)) ? `Deleted ${config.applicationPrefix}-pr-${prNumber}` : `No preview for pr-${prNumber}`)\n return\n }\n if (command === 'prune') {\n const open = new Set((environment.OPEN_PR_NUMBERS ?? '').split(/\\s+/).filter(Boolean).map(pullRequestNumber))\n for (const prNumber of await manager.prune(open)) console.log(`Deleted ${config.applicationPrefix}-pr-${prNumber}`)\n return\n }\n throw new Error('usage: ras preview dokploy <deploy|delete|prune>')\n}\n\nexport function renderPreviewEnvironment(template: string, prNumber: string) {\n return template\n .replaceAll('{{PR_NUMBER}}', pullRequestNumber(prNumber))\n .replaceAll('{{RANDOM_HEX_32}}', () => randomBytes(32).toString('hex'))\n}\n\nfunction required(environment: NodeJS.ProcessEnv, name: string) {\n const value = optional(environment, name)\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction optional(environment: NodeJS.ProcessEnv, name: string) {\n return environment[name]?.trim() || undefined\n}\n"]}
1
+ {"version":3,"file":"dokploy-cli.js","sourceRoot":"","sources":["../../src/preview/dokploy-cli.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { dokployPreviewFromEnvironment, pullRequestNumber } from './dokploy.js'\n\nexport async function runDokployPreviewCli(arguments_: string[], environment: NodeJS.ProcessEnv = process.env) {\n const command = arguments_[0]\n const { config, manager } = dokployPreviewFromEnvironment(environment)\n\n if (command === 'deploy') {\n const prNumber = pullRequestNumber(required(environment, 'PR_NUMBER'))\n await manager.deploy({\n prNumber,\n image: required(environment, 'PREVIEW_IMAGE'),\n environment: ({ url }) => renderPreviewEnvironment(required(environment, 'PREVIEW_ENVIRONMENT'), prNumber, url),\n ...(config.registry ? { registry: config.registry } : {}),\n })\n return\n }\n if (command === 'delete') {\n const prNumber = pullRequestNumber(required(environment, 'PR_NUMBER'))\n console.log((await manager.delete(prNumber)) ? `Deleted ${config.applicationPrefix}-pr-${prNumber}` : `No preview for pr-${prNumber}`)\n return\n }\n if (command === 'prune') {\n const open = new Set((environment.OPEN_PR_NUMBERS ?? '').split(/\\s+/).filter(Boolean).map(pullRequestNumber))\n for (const prNumber of await manager.prune(open)) console.log(`Deleted ${config.applicationPrefix}-pr-${prNumber}`)\n return\n }\n throw new Error('usage: ras preview dokploy <deploy|delete|prune>')\n}\n\nexport function renderPreviewEnvironment(template: string, prNumber: string, previewUrl?: string) {\n if (template.includes('{{PREVIEW_URL}}') && !previewUrl) throw new Error('preview URL is required to render PREVIEW_ENVIRONMENT')\n return template\n .replaceAll('{{PR_NUMBER}}', pullRequestNumber(prNumber))\n .replaceAll('{{PREVIEW_URL}}', previewUrl ?? '')\n .replaceAll('{{RANDOM_HEX_32}}', () => randomBytes(32).toString('hex'))\n}\n\nfunction required(environment: NodeJS.ProcessEnv, name: string) {\n const value = optional(environment, name)\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction optional(environment: NodeJS.ProcessEnv, name: string) {\n return environment[name]?.trim() || undefined\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  export type DokployApplication = {
2
2
  applicationId: string;
3
3
  name: string;
4
+ serverId?: string | null;
4
5
  };
5
6
  export declare function dokployPreviewFromEnvironment(environment?: NodeJS.ProcessEnv): {
6
7
  config: {
@@ -8,7 +9,7 @@ export declare function dokployPreviewFromEnvironment(environment?: NodeJS.Proce
8
9
  apiKey: string;
9
10
  environmentId: string;
10
11
  applicationPrefix: string;
11
- domain: string;
12
+ domain: string | undefined;
12
13
  port: number;
13
14
  healthPath: string | undefined;
14
15
  registry?: {
@@ -38,11 +39,12 @@ export declare class DokployClient {
38
39
  }): Promise<T>;
39
40
  applications(): Promise<DokployApplication[]>;
40
41
  application(name: string): Promise<DokployApplication | undefined>;
42
+ generatedDomain(appName: string, serverId?: string, existing?: string[]): Promise<string>;
41
43
  }
42
44
  export type DokployPreviewOptions = {
43
45
  client: DokployClient;
44
46
  applicationName: (prNumber: string) => string;
45
- hostname: (prNumber: string) => string;
47
+ hostname?: (prNumber: string) => string;
46
48
  port: number;
47
49
  healthPath?: string;
48
50
  deploymentTimeoutMs?: number;
@@ -52,11 +54,18 @@ export type DokployPreviewOptions = {
52
54
  sleep?: (milliseconds: number) => Promise<void>;
53
55
  now?: () => number;
54
56
  log?: (message: string) => void;
57
+ onResolved?: (context: {
58
+ host: string;
59
+ url: string;
60
+ }) => void | Promise<void>;
55
61
  };
56
62
  export type DeployPreviewOptions = {
57
63
  prNumber: string;
58
64
  image: string;
59
- environment: string;
65
+ environment: string | ((context: {
66
+ host: string;
67
+ url: string;
68
+ }) => string);
60
69
  registry?: {
61
70
  username: string;
62
71
  password: string;
@@ -65,6 +74,7 @@ export type DeployPreviewOptions = {
65
74
  applicationId: string;
66
75
  client: DokployClient;
67
76
  host: string;
77
+ url: string;
68
78
  }) => void | Promise<void>;
69
79
  };
70
80
  export declare class DokployPreviewManager {
@@ -1,4 +1,7 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { BlockList, isIP } from 'node:net';
1
3
  export function dokployPreviewFromEnvironment(environment = process.env) {
4
+ const githubOutput = optionalEnvironment(environment, 'GITHUB_OUTPUT');
2
5
  const username = optionalEnvironment(environment, 'PREVIEW_REGISTRY_USERNAME');
3
6
  const password = optionalEnvironment(environment, 'PREVIEW_REGISTRY_PASSWORD');
4
7
  if (Boolean(username) !== Boolean(password))
@@ -9,8 +12,9 @@ export function dokployPreviewFromEnvironment(environment = process.env) {
9
12
  const applicationPrefix = requiredEnvironment(environment, 'PREVIEW_APPLICATION_PREFIX');
10
13
  if (!/^[a-z\d](?:[a-z\d-]*[a-z\d])?$/.test(applicationPrefix))
11
14
  throw new Error('PREVIEW_APPLICATION_PREFIX must be a slug');
12
- const domain = requiredEnvironment(environment, 'PREVIEW_DOMAIN');
13
- previewHostname(`pr-1.${domain}`);
15
+ const domain = optionalEnvironment(environment, 'PREVIEW_DOMAIN');
16
+ if (domain)
17
+ previewHostname(`pr-1.${domain}`);
14
18
  const config = {
15
19
  url: requiredEnvironment(environment, 'DOKPLOY_URL'),
16
20
  apiKey: requiredEnvironment(environment, 'DOKPLOY_API_KEY'),
@@ -27,9 +31,10 @@ export function dokployPreviewFromEnvironment(environment = process.env) {
27
31
  manager: new DokployPreviewManager({
28
32
  client,
29
33
  applicationName: (prNumber) => `${applicationPrefix}-pr-${prNumber}`,
30
- hostname: (prNumber) => `pr-${prNumber}.${domain}`,
34
+ ...(domain ? { hostname: (prNumber) => `pr-${prNumber}.${domain}` } : {}),
31
35
  port,
32
36
  ...(config.healthPath ? { healthPath: config.healthPath } : {}),
37
+ ...(githubOutput ? { onResolved: ({ url }) => appendFileSync(githubOutput, `preview-url=${url}\n`) } : {}),
33
38
  }),
34
39
  };
35
40
  }
@@ -82,6 +87,21 @@ export class DokployClient {
82
87
  async application(name) {
83
88
  return (await this.applications()).find((application) => application.name === name);
84
89
  }
90
+ async generatedDomain(appName, serverId, existing = []) {
91
+ const serverIp = serverId
92
+ ? await this.api('domain.canGenerateTraefikMeDomains', { query: { serverId } })
93
+ : await this.api('settings.getIp');
94
+ const publicIp = previewServerIp(serverIp);
95
+ const current = existing.find((host) => isCurrentGeneratedPreviewHostname(host, appName, publicIp));
96
+ if (current)
97
+ return current;
98
+ const generated = await this.api('domain.generateDomain', {
99
+ body: { appName, ...(serverId ? { serverId } : {}) },
100
+ });
101
+ if (typeof generated !== 'string')
102
+ throw new Error('domain.generateDomain returned an invalid hostname');
103
+ return generatedPreviewHostname(generated, publicIp, appName);
104
+ }
85
105
  }
86
106
  export class DokployPreviewManager {
87
107
  options;
@@ -99,7 +119,6 @@ export class DokployPreviewManager {
99
119
  async deploy(options) {
100
120
  const prNumber = pullRequestNumber(options.prNumber);
101
121
  const name = this.options.applicationName(prNumber);
102
- const host = previewHostname(this.options.hostname(prNumber));
103
122
  let application = await this.options.client.application(name);
104
123
  if (!application) {
105
124
  await this.options.client.api('application.create', {
@@ -110,9 +129,13 @@ export class DokployPreviewManager {
110
129
  throw new Error(`Dokploy did not report ${name} after creating it`);
111
130
  }
112
131
  const applicationId = application.applicationId;
113
- const details = await this.options.client.api('application.one', {
114
- query: { applicationId },
115
- });
132
+ const details = await this.options.client.api('application.one', { query: { applicationId } });
133
+ const generated = !this.options.hostname;
134
+ const managedDomains = details?.domains?.filter((domain) => isGeneratedPreviewHostname(domain.host, generatedPreviewName)) ?? [];
135
+ const host = previewHostname(this.options.hostname
136
+ ? this.options.hostname(prNumber)
137
+ : await this.options.client.generatedDomain(generatedPreviewName, details?.serverId ?? application.serverId ?? undefined, managedDomains.map((domain) => domain.host)));
138
+ const url = `${generated ? 'http' : 'https'}://${host}`;
116
139
  if (!details?.domains?.some((domain) => domain.host === host)) {
117
140
  await this.options.client.api('domain.create', {
118
141
  body: {
@@ -120,13 +143,14 @@ export class DokployPreviewManager {
120
143
  host,
121
144
  path: '/',
122
145
  port: this.options.port,
123
- https: true,
124
- certificateType: 'letsencrypt',
146
+ https: !generated,
147
+ certificateType: generated ? 'none' : 'letsencrypt',
125
148
  domainType: 'application',
126
149
  },
127
150
  });
128
151
  }
129
- await options.configure?.({ applicationId, client: this.options.client, host });
152
+ await this.options.onResolved?.({ host, url });
153
+ await options.configure?.({ applicationId, client: this.options.client, host, url });
130
154
  await this.options.client.api('application.saveDockerProvider', {
131
155
  body: {
132
156
  applicationId,
@@ -137,12 +161,23 @@ export class DokployPreviewManager {
137
161
  },
138
162
  });
139
163
  await this.options.client.api('application.saveEnvironment', {
140
- body: { applicationId, env: options.environment, buildArgs: null, buildSecrets: null, createEnvFile: false },
164
+ body: {
165
+ applicationId,
166
+ env: typeof options.environment === 'function' ? options.environment({ host, url }) : options.environment,
167
+ buildArgs: null,
168
+ buildSecrets: null,
169
+ createEnvFile: false,
170
+ },
141
171
  });
142
172
  await this.options.client.api('application.deploy', { body: { applicationId } });
143
173
  await this.waitForDeployment(applicationId);
144
- const url = `https://${host}`;
145
174
  await this.waitForHealth(new URL(this.options.healthPath ?? '/api/health', url).toString());
175
+ for (const domain of managedDomains.filter((candidate) => candidate.host !== host)) {
176
+ if (!domain.domainId)
177
+ throw new Error(`Dokploy did not report an ID for generated domain ${domain.host}`);
178
+ // oxlint-disable-next-line no-await-in-loop
179
+ await this.options.client.api('domain.delete', { body: { domainId: domain.domainId } });
180
+ }
146
181
  this.log(`Preview ready at ${url}`);
147
182
  return { applicationId, host, url };
148
183
  }
@@ -231,6 +266,69 @@ export function previewHostname(value) {
231
266
  }
232
267
  return value;
233
268
  }
269
+ function generatedPreviewHostname(value, serverIp, appName) {
270
+ const host = previewHostname(value);
271
+ if (!isCurrentGeneratedPreviewHostname(host, appName, serverIp))
272
+ throw new Error('domain.generateDomain did not return an sslip.io hostname for the Dokploy server');
273
+ return host;
274
+ }
275
+ function isGeneratedPreviewHostname(host, appName) {
276
+ return new RegExp(`^${regularExpressionLiteral(appName.slice(0, 40))}-[a-f\\d]{6}-[a-f\\d-]+\\.sslip\\.io$`).test(host);
277
+ }
278
+ function isCurrentGeneratedPreviewHostname(host, appName, serverIp) {
279
+ return new RegExp(`^${regularExpressionLiteral(appName.slice(0, 40))}-[a-f\\d]{6}-${regularExpressionLiteral(encodedIp(serverIp))}\\.sslip\\.io$`).test(host);
280
+ }
281
+ function regularExpressionLiteral(value) {
282
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
283
+ }
284
+ function encodedIp(address) {
285
+ return address.replaceAll('.', '-').replaceAll(':', '-');
286
+ }
287
+ const generatedPreviewName = 'ras-preview';
288
+ const blockedPreviewAddresses = new BlockList();
289
+ for (const [address, prefix, family] of [
290
+ ['0.0.0.0', 8, 'ipv4'],
291
+ ['10.0.0.0', 8, 'ipv4'],
292
+ ['100.64.0.0', 10, 'ipv4'],
293
+ ['127.0.0.0', 8, 'ipv4'],
294
+ ['169.254.0.0', 16, 'ipv4'],
295
+ ['172.16.0.0', 12, 'ipv4'],
296
+ ['192.0.0.0', 24, 'ipv4'],
297
+ ['192.0.2.0', 24, 'ipv4'],
298
+ ['192.88.99.0', 24, 'ipv4'],
299
+ ['192.168.0.0', 16, 'ipv4'],
300
+ ['198.18.0.0', 15, 'ipv4'],
301
+ ['198.51.100.0', 24, 'ipv4'],
302
+ ['203.0.113.0', 24, 'ipv4'],
303
+ ['224.0.0.0', 4, 'ipv4'],
304
+ ['240.0.0.0', 4, 'ipv4'],
305
+ ['::', 128, 'ipv6'],
306
+ ['::1', 128, 'ipv6'],
307
+ ['64:ff9b:1::', 48, 'ipv6'],
308
+ ['100::', 64, 'ipv6'],
309
+ ['100:0:0:1::', 64, 'ipv6'],
310
+ ['2001::', 23, 'ipv6'],
311
+ ['2001:db8::', 32, 'ipv6'],
312
+ ['2002::', 16, 'ipv6'],
313
+ ['3fff::', 20, 'ipv6'],
314
+ ['5f00::', 16, 'ipv6'],
315
+ ['fc00::', 7, 'ipv6'],
316
+ ['fe80::', 10, 'ipv6'],
317
+ ['ff00::', 8, 'ipv6'],
318
+ ]) {
319
+ blockedPreviewAddresses.addSubnet(address, prefix, family);
320
+ }
321
+ function previewServerIp(value) {
322
+ if (typeof value !== 'string')
323
+ throw new Error('Dokploy requires a public server IP before it can generate preview domains');
324
+ const address = value.trim().toLowerCase();
325
+ const version = isIP(address);
326
+ const family = version === 4 ? 'ipv4' : version === 6 ? 'ipv6' : undefined;
327
+ if (!family || address.startsWith('::ffff:') || blockedPreviewAddresses.check(address, family)) {
328
+ throw new Error('Dokploy requires a public server IP before it can generate preview domains');
329
+ }
330
+ return address;
331
+ }
234
332
  function previewApplicationPrNumber(name, applicationName) {
235
333
  const match = /^(\d+)$/.exec(name.match(/(\d+)$/)?.[1] ?? '');
236
334
  const prNumber = match?.[1];
@@ -1 +1 @@
1
- {"version":3,"file":"dokploy.js","sourceRoot":"","sources":["../../src/preview/dokploy.ts"],"names":[],"mappings":"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","sourcesContent":["export type DokployApplication = { applicationId: string; name: string }\n\nexport function dokployPreviewFromEnvironment(environment: NodeJS.ProcessEnv = process.env) {\n const username = optionalEnvironment(environment, 'PREVIEW_REGISTRY_USERNAME')\n const password = optionalEnvironment(environment, 'PREVIEW_REGISTRY_PASSWORD')\n if (Boolean(username) !== Boolean(password)) throw new Error('preview registry username and password must be configured together')\n const port = Number(requiredEnvironment(environment, 'PREVIEW_PORT'))\n if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error('PREVIEW_PORT must be a valid port')\n const applicationPrefix = requiredEnvironment(environment, 'PREVIEW_APPLICATION_PREFIX')\n if (!/^[a-z\\d](?:[a-z\\d-]*[a-z\\d])?$/.test(applicationPrefix)) throw new Error('PREVIEW_APPLICATION_PREFIX must be a slug')\n const domain = requiredEnvironment(environment, 'PREVIEW_DOMAIN')\n previewHostname(`pr-1.${domain}`)\n const config = {\n url: requiredEnvironment(environment, 'DOKPLOY_URL'),\n apiKey: requiredEnvironment(environment, 'DOKPLOY_API_KEY'),\n environmentId: requiredEnvironment(environment, 'DOKPLOY_ENVIRONMENT_ID'),\n applicationPrefix,\n domain,\n port,\n healthPath: optionalEnvironment(environment, 'PREVIEW_HEALTH_PATH'),\n ...(username && password ? { registry: { username, password } } : {}),\n }\n const client = new DokployClient(config)\n return {\n config,\n manager: new DokployPreviewManager({\n client,\n applicationName: (prNumber) => `${applicationPrefix}-pr-${prNumber}`,\n hostname: (prNumber) => `pr-${prNumber}.${domain}`,\n port,\n ...(config.healthPath ? { healthPath: config.healthPath } : {}),\n }),\n }\n}\n\nexport type DokployClientOptions = {\n url: string\n apiKey: string\n environmentId: string\n fetch?: typeof fetch\n log?: (message: string) => void\n}\n\nexport class DokployClient {\n private readonly request: typeof fetch\n private readonly log: (message: string) => void\n\n constructor(private readonly options: DokployClientOptions) {\n this.request = options.fetch ?? fetch\n this.log = options.log ?? console.log\n }\n\n get environmentId() {\n return this.options.environmentId\n }\n\n async api<T = unknown>(\n procedure: string,\n options: { query?: Record<string, string>; body?: unknown; signal?: AbortSignal } = {},\n ): Promise<T> {\n const url = new URL(`${this.options.url.replace(/\\/$/, '')}/api/${procedure}`)\n for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value)\n this.log(`β†’ ${procedure}`)\n const response = await this.request(url, {\n method: options.body === undefined ? 'GET' : 'POST',\n headers: {\n 'x-api-key': this.options.apiKey,\n ...(options.body === undefined ? {} : { 'content-type': 'application/json' }),\n },\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),\n })\n const text = await response.text()\n if (!response.ok) throw new Error(`${procedure} failed with ${response.status}: ${text.slice(0, 500)}`)\n if (!text) return undefined as T\n try {\n return JSON.parse(text) as T\n } catch {\n throw new Error(`${procedure} returned ${response.status} with a non-JSON body: ${text.slice(0, 200)}`)\n }\n }\n\n async applications() {\n const environment = await this.api<{ applications?: DokployApplication[] } | undefined>('environment.one', {\n query: { environmentId: this.options.environmentId },\n })\n if (!environment) throw new Error('environment.one returned an empty response')\n return environment.applications ?? []\n }\n\n async application(name: string) {\n return (await this.applications()).find((application) => application.name === name)\n }\n}\n\nexport type DokployPreviewOptions = {\n client: DokployClient\n applicationName: (prNumber: string) => string\n hostname: (prNumber: string) => string\n port: number\n healthPath?: string\n deploymentTimeoutMs?: number\n healthTimeoutMs?: number\n pollIntervalMs?: number\n fetch?: typeof fetch\n sleep?: (milliseconds: number) => Promise<void>\n now?: () => number\n log?: (message: string) => void\n}\n\nexport type DeployPreviewOptions = {\n prNumber: string\n image: string\n environment: string\n registry?: { username: string; password: string }\n configure?: (context: { applicationId: string; client: DokployClient; host: string }) => void | Promise<void>\n}\n\nexport class DokployPreviewManager {\n private readonly request: typeof fetch\n private readonly pause: (milliseconds: number) => Promise<void>\n private readonly log: (message: string) => void\n private readonly now: () => number\n\n constructor(private readonly options: DokployPreviewOptions) {\n this.request = options.fetch ?? fetch\n this.pause = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)))\n this.log = options.log ?? console.log\n this.now = options.now ?? Date.now\n }\n\n async deploy(options: DeployPreviewOptions) {\n const prNumber = pullRequestNumber(options.prNumber)\n const name = this.options.applicationName(prNumber)\n const host = previewHostname(this.options.hostname(prNumber))\n let application = await this.options.client.application(name)\n if (!application) {\n await this.options.client.api('application.create', {\n body: { name, appName: name, environmentId: this.options.client.environmentId },\n })\n application = await this.options.client.application(name)\n if (!application) throw new Error(`Dokploy did not report ${name} after creating it`)\n }\n const applicationId = application.applicationId\n const details = await this.options.client.api<{ domains?: { host: string }[] } | undefined>('application.one', {\n query: { applicationId },\n })\n if (!details?.domains?.some((domain) => domain.host === host)) {\n await this.options.client.api('domain.create', {\n body: {\n applicationId,\n host,\n path: '/',\n port: this.options.port,\n https: true,\n certificateType: 'letsencrypt',\n domainType: 'application',\n },\n })\n }\n await options.configure?.({ applicationId, client: this.options.client, host })\n await this.options.client.api('application.saveDockerProvider', {\n body: {\n applicationId,\n dockerImage: options.image,\n username: options.registry?.username ?? null,\n password: options.registry?.password ?? null,\n registryUrl: options.registry ? options.image.split('/')[0] : null,\n },\n })\n await this.options.client.api('application.saveEnvironment', {\n body: { applicationId, env: options.environment, buildArgs: null, buildSecrets: null, createEnvFile: false },\n })\n await this.options.client.api('application.deploy', { body: { applicationId } })\n await this.waitForDeployment(applicationId)\n const url = `https://${host}`\n await this.waitForHealth(new URL(this.options.healthPath ?? '/api/health', url).toString())\n this.log(`Preview ready at ${url}`)\n return { applicationId, host, url }\n }\n\n async delete(prNumber: string, beforeDelete?: (application: DokployApplication | undefined) => void | Promise<void>) {\n const name = this.options.applicationName(pullRequestNumber(prNumber))\n const application = await this.options.client.application(name)\n await beforeDelete?.(application)\n if (!application) return false\n await this.options.client.api('application.delete', { body: { applicationId: application.applicationId } })\n return true\n }\n\n async prune(\n openPullRequests: ReadonlySet<string>,\n beforeDelete?: (prNumber: string, application: DokployApplication) => void | Promise<void>,\n ) {\n const deleted: string[] = []\n for (const application of await this.options.client.applications()) {\n const prNumber = previewApplicationPrNumber(application.name, this.options.applicationName)\n if (!prNumber || openPullRequests.has(prNumber)) continue\n // Dokploy mutations are intentionally ordered to avoid overwhelming one environment.\n // oxlint-disable-next-line no-await-in-loop\n await beforeDelete?.(prNumber, application)\n // oxlint-disable-next-line no-await-in-loop\n await this.options.client.api('application.delete', { body: { applicationId: application.applicationId } })\n deleted.push(prNumber)\n }\n return deleted\n }\n\n private async waitForDeployment(applicationId: string) {\n const deadline = this.now() + (this.options.deploymentTimeoutMs ?? 600_000)\n while (this.now() < deadline) {\n // Polling is intentionally sequential; each response determines whether another request is needed.\n // oxlint-disable-next-line no-await-in-loop\n await this.pause(this.options.pollIntervalMs ?? 5_000)\n let applicationStatus: string\n try {\n // Polling is intentionally sequential; each response determines whether another request is needed.\n // oxlint-disable-next-line no-await-in-loop\n const details = await this.options.client.api<{ applicationStatus: string }>('application.one', {\n query: { applicationId },\n signal: AbortSignal.timeout(Math.max(1, deadline - this.now())),\n })\n applicationStatus = details.applicationStatus\n } catch (error) {\n if (error instanceof DOMException && error.name === 'TimeoutError') break\n throw error\n }\n if (applicationStatus === 'done') return\n if (applicationStatus === 'error') throw new Error('Dokploy reported a failed deployment')\n }\n throw new Error('Timed out waiting for the Dokploy deployment to finish')\n }\n\n private async waitForHealth(url: string) {\n const deadline = this.now() + (this.options.healthTimeoutMs ?? 300_000)\n let lastFailure = 'no response'\n while (this.now() < deadline) {\n try {\n // Polling is intentionally sequential; each response determines whether another request is needed.\n // oxlint-disable-next-line no-await-in-loop\n const response = await this.request(url, { signal: AbortSignal.timeout(Math.max(1, deadline - this.now())) })\n if (response.status === 200) return\n lastFailure = `status ${response.status}`\n } catch (error) {\n lastFailure = error instanceof Error ? error.message : String(error)\n }\n // oxlint-disable-next-line no-await-in-loop\n await this.pause(this.options.pollIntervalMs ?? 5_000)\n }\n throw new Error(`Timed out waiting for ${url} (${lastFailure})`)\n }\n}\n\nexport function pullRequestNumber(value: string) {\n if (!/^\\d+$/.test(value)) throw new Error('pull request number must contain only digits')\n return value\n}\n\nexport function previewHostname(value: string) {\n const parsed = new URL(`https://${value}`)\n if (parsed.hostname !== value || parsed.port || parsed.username || parsed.password || parsed.pathname !== '/') {\n throw new Error('preview hostname must be a bare hostname')\n }\n return value\n}\n\nfunction previewApplicationPrNumber(name: string, applicationName: (prNumber: string) => string) {\n const match = /^(\\d+)$/.exec(name.match(/(\\d+)$/)?.[1] ?? '')\n const prNumber = match?.[1]\n return prNumber && applicationName(prNumber) === name ? prNumber : undefined\n}\n\nfunction requiredEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n const value = optionalEnvironment(environment, name)\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction optionalEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n return environment[name]?.trim() || undefined\n}\n"]}
1
+ {"version":3,"file":"dokploy.js","sourceRoot":"","sources":["../../src/preview/dokploy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAI1C,MAAM,UAAU,6BAA6B,CAAC,WAAW,GAAsB,OAAO,CAAC,GAAG;IACxF,MAAM,YAAY,GAAG,mBAAmB,CAAC,WAAW,EAAE,eAAe,CAAC,CAAA;IACtE,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,EAAE,2BAA2B,CAAC,CAAA;IAC9E,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,EAAE,2BAA2B,CAAC,CAAA;IAC9E,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IAClI,MAAM,IAAI,GAAG,MAAM,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAA;IACrE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAA;IAC9G,MAAM,iBAAiB,GAAG,mBAAmB,CAAC,WAAW,EAAE,4BAA4B,CAAC,CAAA;IACxF,IAAI,CAAC,gCAAgC,CAAC,IAAI,CAAC,iBAAiB,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;IAC3H,MAAM,MAAM,GAAG,mBAAmB,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;IACjE,IAAI,MAAM;QAAE,eAAe,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAA;IAC7C,MAAM,MAAM,GAAG;QACb,GAAG,EAAE,mBAAmB,CAAC,WAAW,EAAE,aAAa,CAAC;QACpD,MAAM,EAAE,mBAAmB,CAAC,WAAW,EAAE,iBAAiB,CAAC;QAC3D,aAAa,EAAE,mBAAmB,CAAC,WAAW,EAAE,wBAAwB,CAAC;QACzE,iBAAiB;QACjB,MAAM;QACN,IAAI;QACJ,UAAU,EAAE,mBAAmB,CAAC,WAAW,EAAE,qBAAqB,CAAC;QACnE,GAAG,CAAC,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtE,CAAA;IACD,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,OAAO;QACL,MAAM;QACN,OAAO,EAAE,IAAI,qBAAqB,CAAC;YACjC,MAAM;YACN,eAAe,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,iBAAiB,OAAO,QAAQ,EAAE;YACpE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,QAAQ,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjF,IAAI;YACJ,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/D,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,GAAG,EAAmB,EAAE,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5H,CAAC;KACH,CAAA;AACH,CAAC;AAUD,MAAM,OAAO,aAAa;IAIK,OAAO;IAHnB,OAAO,CAAc;IACrB,GAAG,CAA2B;IAE/C,YAA6B,OAA6B;uBAA7B,OAAO;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;QACrC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAA;IACvC,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAA;IACnC,CAAC;IAED,KAAK,CAAC,GAAG,CACP,SAAiB,EACjB,OAAO,GAA6E,EAAE;QAEtF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,SAAS,EAAE,CAAC,CAAA;QAC9E,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAChG,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC,CAAA;QAC1B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YACvC,MAAM,EAAE,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM;YACnD,OAAO,EAAE;gBACP,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM;gBAChC,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;aAC9E;YACD,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;SAC9E,CAAC,CAAA;QACF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAClC,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,gBAAgB,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;QACvG,IAAI,CAAC,IAAI;YAAE,OAAO,SAAc,CAAA;QAChC,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAA;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,GAAG,SAAS,aAAa,QAAQ,CAAC,MAAM,0BAA0B,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;QACzG,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,GAAG,CAAsD,iBAAiB,EAAE;YACzG,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE;SACrD,CAAC,CAAA;QACF,IAAI,CAAC,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;QAC/E,OAAO,WAAW,CAAC,YAAY,IAAI,EAAE,CAAA;IACvC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,OAAO,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;IACrF,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,OAAe,EAAE,QAAiB,EAAE,QAAQ,GAAa,EAAE;QAC/E,MAAM,QAAQ,GAAG,QAAQ;YACvB,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,oCAAoC,EAAE,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC;YAC/E,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QACpC,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;QAC1C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iCAAiC,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;QACnG,IAAI,OAAO;YAAE,OAAO,OAAO,CAAA;QAC3B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE;YACxD,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;SACrD,CAAC,CAAA;QACF,IAAI,OAAO,SAAS,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;QACxG,OAAO,wBAAwB,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC/D,CAAC;CACF;AA0BD,MAAM,OAAO,qBAAqB;IAMH,OAAO;IALnB,OAAO,CAAc;IACrB,KAAK,CAAyC;IAC9C,GAAG,CAA2B;IAC9B,GAAG,CAAc;IAElC,YAA6B,OAA8B;uBAA9B,OAAO;QAClC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAA;QAC7G,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAA;QACrC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACpC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAA6B;QACxC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;QACnD,IAAI,WAAW,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QAC7D,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE;gBAClD,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE;aAChF,CAAC,CAAA;YACF,WAAW,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;YACzD,IAAI,CAAC,WAAW;gBAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,oBAAoB,CAAC,CAAA;QACvF,CAAC;QACD,MAAM,aAAa,GAAG,WAAW,CAAC,aAAa,CAAA;QAC/C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAE3C,iBAAiB,EAAE,EAAE,KAAK,EAAE,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;QAClD,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAA;QACxC,MAAM,cAAc,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,0BAA0B,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC,IAAI,EAAE,CAAA;QAChI,MAAM,IAAI,GAAG,eAAe,CAC1B,IAAI,CAAC,OAAO,CAAC,QAAQ;YACnB,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,CAAC,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CACvC,oBAAoB,EACpB,OAAO,EAAE,QAAQ,IAAI,WAAW,CAAC,QAAQ,IAAI,SAAS,EACtD,cAAc,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAC5C,CACN,CAAA;QACD,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,MAAM,IAAI,EAAE,CAAA;QACvD,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE;gBAC7C,IAAI,EAAE;oBACJ,aAAa;oBACb,IAAI;oBACJ,IAAI,EAAE,GAAG;oBACT,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI;oBACvB,KAAK,EAAE,CAAC,SAAS;oBACjB,eAAe,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa;oBACnD,UAAU,EAAE,aAAa;iBAC1B;aACF,CAAC,CAAA;QACJ,CAAC;QACD,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;QAC9C,MAAM,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAA;QACpF,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,gCAAgC,EAAE;YAC9D,IAAI,EAAE;gBACJ,aAAa;gBACb,WAAW,EAAE,OAAO,CAAC,KAAK;gBAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,IAAI,IAAI;gBAC5C,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,IAAI,IAAI;gBAC5C,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;aACnE;SACF,CAAC,CAAA;QACF,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,6BAA6B,EAAE;YAC3D,IAAI,EAAE;gBACJ,aAAa;gBACb,GAAG,EAAE,OAAO,OAAO,CAAC,WAAW,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW;gBACzG,SAAS,EAAE,IAAI;gBACf,YAAY,EAAE,IAAI;gBAClB,aAAa,EAAE,KAAK;aACrB;SACF,CAAC,CAAA;QACF,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;QAChF,MAAM,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC,CAAA;QAC3C,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,aAAa,EAAE,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC3F,KAAK,MAAM,MAAM,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACnF,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;YACzG,4CAA4C;YAC5C,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;QACzF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,oBAAoB,GAAG,EAAE,CAAC,CAAA;QACnC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;IACrC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,QAAgB,EAAE,YAAoF;QACjH,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAA;QACtE,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QAC/D,MAAM,YAAY,EAAE,CAAC,WAAW,CAAC,CAAA;QACjC,IAAI,CAAC,WAAW;YAAE,OAAO,KAAK,CAAA;QAC9B,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,EAAE,aAAa,EAAE,WAAW,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;QAC3G,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,KAAK,CACT,gBAAqC,EACrC,YAA0F;QAE1F,MAAM,OAAO,GAAa,EAAE,CAAA;QAC5B,KAAK,MAAM,WAAW,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,CAAC;YACnE,MAAM,QAAQ,GAAG,0BAA0B,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;YAC3F,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAQ;YACzD,qFAAqF;YACrF,4CAA4C;YAC5C,MAAM,YAAY,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;YAC3C,4CAA4C;YAC5C,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,EAAE,aAAa,EAAE,WAAW,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;YAC3G,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACxB,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,aAAqB;QACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,IAAI,OAAO,CAAC,CAAA;QAC3E,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC7B,mGAAmG;YACnG,4CAA4C;YAC5C,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,KAAK,CAAC,CAAA;YACtD,IAAI,iBAAyB,CAAA;YAC7B,IAAI,CAAC;gBACH,mGAAmG;gBACnG,4CAA4C;gBAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAgC,iBAAiB,EAAE;oBAC9F,KAAK,EAAE,EAAE,aAAa,EAAE;oBACxB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;iBAChE,CAAC,CAAA;gBACF,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAA;YAC/C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,YAAY,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;oBAAE,MAAK;gBACzE,MAAM,KAAK,CAAA;YACb,CAAC;YACD,IAAI,iBAAiB,KAAK,MAAM;gBAAE,OAAM;YACxC,IAAI,iBAAiB,KAAK,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;QAC5F,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,GAAW;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,CAAA;QACvE,IAAI,WAAW,GAAG,aAAa,CAAA;QAC/B,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACH,mGAAmG;gBACnG,4CAA4C;gBAC5C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;gBAC7G,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;oBAAE,OAAM;gBACnC,WAAW,GAAG,UAAU,QAAQ,CAAC,MAAM,EAAE,CAAA;YAC3C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,WAAW,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACtE,CAAC;YACD,4CAA4C;YAC5C,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,KAAK,CAAC,CAAA;QACxD,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,KAAK,WAAW,GAAG,CAAC,CAAA;IAClE,CAAC;CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAA;IACzF,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,WAAW,KAAK,EAAE,CAAC,CAAA;IAC1C,IAAI,MAAM,CAAC,QAAQ,KAAK,KAAK,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,GAAG,EAAE,CAAC;QAC9G,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IAC7D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,wBAAwB,CAAC,KAAa,EAAE,QAAgB,EAAE,OAAe;IAChF,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;IACnC,IAAI,CAAC,iCAAiC,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAAA;IACrG,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,0BAA0B,CAAC,IAAY,EAAE,OAAe;IAC/D,OAAO,IAAI,MAAM,CAAC,IAAI,wBAAwB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,uCAAuC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzH,CAAC;AAED,SAAS,iCAAiC,CAAC,IAAY,EAAE,OAAe,EAAE,QAAgB;IACxF,OAAO,IAAI,MAAM,CACf,IAAI,wBAAwB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,gBAAgB,wBAAwB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,gBAAgB,CAChI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC;AAED,SAAS,wBAAwB,CAAC,KAAa;IAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACrD,CAAC;AAED,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,oBAAoB,GAAG,aAAa,CAAA;AAE1C,MAAM,uBAAuB,GAAG,IAAI,SAAS,EAAE,CAAA;AAC/C,KAAK,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI;IACtC,CAAC,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,UAAU,EAAE,CAAC,EAAE,MAAM,CAAC;IACvB,CAAC,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC;IAC1B,CAAC,WAAW,EAAE,CAAC,EAAE,MAAM,CAAC;IACxB,CAAC,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC;IAC3B,CAAC,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC;IAC1B,CAAC,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC;IACzB,CAAC,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC;IACzB,CAAC,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC;IAC3B,CAAC,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC;IAC3B,CAAC,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC;IAC1B,CAAC,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC;IAC5B,CAAC,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC;IAC3B,CAAC,WAAW,EAAE,CAAC,EAAE,MAAM,CAAC;IACxB,CAAC,WAAW,EAAE,CAAC,EAAE,MAAM,CAAC;IACxB,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC;IACnB,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC;IACpB,CAAC,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC;IAC3B,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC;IACrB,CAAC,aAAa,EAAE,EAAE,EAAE,MAAM,CAAC;IAC3B,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC;IACtB,CAAC,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC;IAC1B,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC;IACtB,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC;IACtB,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC;IACtB,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC;IACtB,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,EAAE,CAAC;IACX,uBAAuB,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAC5D,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAA;IAC5H,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAA;IAC7B,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAC1E,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,uBAAuB,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;QAC/F,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAA;IAC/F,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,0BAA0B,CAAC,IAAY,EAAE,eAA6C;IAC7F,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IAC7D,MAAM,QAAQ,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;IAC3B,OAAO,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9E,CAAC;AAED,SAAS,mBAAmB,CAAC,WAA8B,EAAE,IAAY;IACvE,MAAM,KAAK,GAAG,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IACpD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAA;IAClD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,mBAAmB,CAAC,WAA8B,EAAE,IAAY;IACvE,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAA;AAC/C,CAAC","sourcesContent":["import { appendFileSync } from 'node:fs'\nimport { BlockList, isIP } from 'node:net'\n\nexport type DokployApplication = { applicationId: string; name: string; serverId?: string | null }\n\nexport function dokployPreviewFromEnvironment(environment: NodeJS.ProcessEnv = process.env) {\n const githubOutput = optionalEnvironment(environment, 'GITHUB_OUTPUT')\n const username = optionalEnvironment(environment, 'PREVIEW_REGISTRY_USERNAME')\n const password = optionalEnvironment(environment, 'PREVIEW_REGISTRY_PASSWORD')\n if (Boolean(username) !== Boolean(password)) throw new Error('preview registry username and password must be configured together')\n const port = Number(requiredEnvironment(environment, 'PREVIEW_PORT'))\n if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error('PREVIEW_PORT must be a valid port')\n const applicationPrefix = requiredEnvironment(environment, 'PREVIEW_APPLICATION_PREFIX')\n if (!/^[a-z\\d](?:[a-z\\d-]*[a-z\\d])?$/.test(applicationPrefix)) throw new Error('PREVIEW_APPLICATION_PREFIX must be a slug')\n const domain = optionalEnvironment(environment, 'PREVIEW_DOMAIN')\n if (domain) previewHostname(`pr-1.${domain}`)\n const config = {\n url: requiredEnvironment(environment, 'DOKPLOY_URL'),\n apiKey: requiredEnvironment(environment, 'DOKPLOY_API_KEY'),\n environmentId: requiredEnvironment(environment, 'DOKPLOY_ENVIRONMENT_ID'),\n applicationPrefix,\n domain,\n port,\n healthPath: optionalEnvironment(environment, 'PREVIEW_HEALTH_PATH'),\n ...(username && password ? { registry: { username, password } } : {}),\n }\n const client = new DokployClient(config)\n return {\n config,\n manager: new DokployPreviewManager({\n client,\n applicationName: (prNumber) => `${applicationPrefix}-pr-${prNumber}`,\n ...(domain ? { hostname: (prNumber: string) => `pr-${prNumber}.${domain}` } : {}),\n port,\n ...(config.healthPath ? { healthPath: config.healthPath } : {}),\n ...(githubOutput ? { onResolved: ({ url }: { url: string }) => appendFileSync(githubOutput, `preview-url=${url}\\n`) } : {}),\n }),\n }\n}\n\nexport type DokployClientOptions = {\n url: string\n apiKey: string\n environmentId: string\n fetch?: typeof fetch\n log?: (message: string) => void\n}\n\nexport class DokployClient {\n private readonly request: typeof fetch\n private readonly log: (message: string) => void\n\n constructor(private readonly options: DokployClientOptions) {\n this.request = options.fetch ?? fetch\n this.log = options.log ?? console.log\n }\n\n get environmentId() {\n return this.options.environmentId\n }\n\n async api<T = unknown>(\n procedure: string,\n options: { query?: Record<string, string>; body?: unknown; signal?: AbortSignal } = {},\n ): Promise<T> {\n const url = new URL(`${this.options.url.replace(/\\/$/, '')}/api/${procedure}`)\n for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value)\n this.log(`β†’ ${procedure}`)\n const response = await this.request(url, {\n method: options.body === undefined ? 'GET' : 'POST',\n headers: {\n 'x-api-key': this.options.apiKey,\n ...(options.body === undefined ? {} : { 'content-type': 'application/json' }),\n },\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),\n })\n const text = await response.text()\n if (!response.ok) throw new Error(`${procedure} failed with ${response.status}: ${text.slice(0, 500)}`)\n if (!text) return undefined as T\n try {\n return JSON.parse(text) as T\n } catch {\n throw new Error(`${procedure} returned ${response.status} with a non-JSON body: ${text.slice(0, 200)}`)\n }\n }\n\n async applications() {\n const environment = await this.api<{ applications?: DokployApplication[] } | undefined>('environment.one', {\n query: { environmentId: this.options.environmentId },\n })\n if (!environment) throw new Error('environment.one returned an empty response')\n return environment.applications ?? []\n }\n\n async application(name: string) {\n return (await this.applications()).find((application) => application.name === name)\n }\n\n async generatedDomain(appName: string, serverId?: string, existing: string[] = []) {\n const serverIp = serverId\n ? await this.api('domain.canGenerateTraefikMeDomains', { query: { serverId } })\n : await this.api('settings.getIp')\n const publicIp = previewServerIp(serverIp)\n const current = existing.find((host) => isCurrentGeneratedPreviewHostname(host, appName, publicIp))\n if (current) return current\n const generated = await this.api('domain.generateDomain', {\n body: { appName, ...(serverId ? { serverId } : {}) },\n })\n if (typeof generated !== 'string') throw new Error('domain.generateDomain returned an invalid hostname')\n return generatedPreviewHostname(generated, publicIp, appName)\n }\n}\n\nexport type DokployPreviewOptions = {\n client: DokployClient\n applicationName: (prNumber: string) => string\n hostname?: (prNumber: string) => string\n port: number\n healthPath?: string\n deploymentTimeoutMs?: number\n healthTimeoutMs?: number\n pollIntervalMs?: number\n fetch?: typeof fetch\n sleep?: (milliseconds: number) => Promise<void>\n now?: () => number\n log?: (message: string) => void\n onResolved?: (context: { host: string; url: string }) => void | Promise<void>\n}\n\nexport type DeployPreviewOptions = {\n prNumber: string\n image: string\n environment: string | ((context: { host: string; url: string }) => string)\n registry?: { username: string; password: string }\n configure?: (context: { applicationId: string; client: DokployClient; host: string; url: string }) => void | Promise<void>\n}\n\nexport class DokployPreviewManager {\n private readonly request: typeof fetch\n private readonly pause: (milliseconds: number) => Promise<void>\n private readonly log: (message: string) => void\n private readonly now: () => number\n\n constructor(private readonly options: DokployPreviewOptions) {\n this.request = options.fetch ?? fetch\n this.pause = options.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)))\n this.log = options.log ?? console.log\n this.now = options.now ?? Date.now\n }\n\n async deploy(options: DeployPreviewOptions) {\n const prNumber = pullRequestNumber(options.prNumber)\n const name = this.options.applicationName(prNumber)\n let application = await this.options.client.application(name)\n if (!application) {\n await this.options.client.api('application.create', {\n body: { name, appName: name, environmentId: this.options.client.environmentId },\n })\n application = await this.options.client.application(name)\n if (!application) throw new Error(`Dokploy did not report ${name} after creating it`)\n }\n const applicationId = application.applicationId\n const details = await this.options.client.api<\n { domains?: { domainId?: string; host: string }[]; serverId?: string | null } | undefined\n >('application.one', { query: { applicationId } })\n const generated = !this.options.hostname\n const managedDomains = details?.domains?.filter((domain) => isGeneratedPreviewHostname(domain.host, generatedPreviewName)) ?? []\n const host = previewHostname(\n this.options.hostname\n ? this.options.hostname(prNumber)\n : await this.options.client.generatedDomain(\n generatedPreviewName,\n details?.serverId ?? application.serverId ?? undefined,\n managedDomains.map((domain) => domain.host),\n ),\n )\n const url = `${generated ? 'http' : 'https'}://${host}`\n if (!details?.domains?.some((domain) => domain.host === host)) {\n await this.options.client.api('domain.create', {\n body: {\n applicationId,\n host,\n path: '/',\n port: this.options.port,\n https: !generated,\n certificateType: generated ? 'none' : 'letsencrypt',\n domainType: 'application',\n },\n })\n }\n await this.options.onResolved?.({ host, url })\n await options.configure?.({ applicationId, client: this.options.client, host, url })\n await this.options.client.api('application.saveDockerProvider', {\n body: {\n applicationId,\n dockerImage: options.image,\n username: options.registry?.username ?? null,\n password: options.registry?.password ?? null,\n registryUrl: options.registry ? options.image.split('/')[0] : null,\n },\n })\n await this.options.client.api('application.saveEnvironment', {\n body: {\n applicationId,\n env: typeof options.environment === 'function' ? options.environment({ host, url }) : options.environment,\n buildArgs: null,\n buildSecrets: null,\n createEnvFile: false,\n },\n })\n await this.options.client.api('application.deploy', { body: { applicationId } })\n await this.waitForDeployment(applicationId)\n await this.waitForHealth(new URL(this.options.healthPath ?? '/api/health', url).toString())\n for (const domain of managedDomains.filter((candidate) => candidate.host !== host)) {\n if (!domain.domainId) throw new Error(`Dokploy did not report an ID for generated domain ${domain.host}`)\n // oxlint-disable-next-line no-await-in-loop\n await this.options.client.api('domain.delete', { body: { domainId: domain.domainId } })\n }\n this.log(`Preview ready at ${url}`)\n return { applicationId, host, url }\n }\n\n async delete(prNumber: string, beforeDelete?: (application: DokployApplication | undefined) => void | Promise<void>) {\n const name = this.options.applicationName(pullRequestNumber(prNumber))\n const application = await this.options.client.application(name)\n await beforeDelete?.(application)\n if (!application) return false\n await this.options.client.api('application.delete', { body: { applicationId: application.applicationId } })\n return true\n }\n\n async prune(\n openPullRequests: ReadonlySet<string>,\n beforeDelete?: (prNumber: string, application: DokployApplication) => void | Promise<void>,\n ) {\n const deleted: string[] = []\n for (const application of await this.options.client.applications()) {\n const prNumber = previewApplicationPrNumber(application.name, this.options.applicationName)\n if (!prNumber || openPullRequests.has(prNumber)) continue\n // Dokploy mutations are intentionally ordered to avoid overwhelming one environment.\n // oxlint-disable-next-line no-await-in-loop\n await beforeDelete?.(prNumber, application)\n // oxlint-disable-next-line no-await-in-loop\n await this.options.client.api('application.delete', { body: { applicationId: application.applicationId } })\n deleted.push(prNumber)\n }\n return deleted\n }\n\n private async waitForDeployment(applicationId: string) {\n const deadline = this.now() + (this.options.deploymentTimeoutMs ?? 600_000)\n while (this.now() < deadline) {\n // Polling is intentionally sequential; each response determines whether another request is needed.\n // oxlint-disable-next-line no-await-in-loop\n await this.pause(this.options.pollIntervalMs ?? 5_000)\n let applicationStatus: string\n try {\n // Polling is intentionally sequential; each response determines whether another request is needed.\n // oxlint-disable-next-line no-await-in-loop\n const details = await this.options.client.api<{ applicationStatus: string }>('application.one', {\n query: { applicationId },\n signal: AbortSignal.timeout(Math.max(1, deadline - this.now())),\n })\n applicationStatus = details.applicationStatus\n } catch (error) {\n if (error instanceof DOMException && error.name === 'TimeoutError') break\n throw error\n }\n if (applicationStatus === 'done') return\n if (applicationStatus === 'error') throw new Error('Dokploy reported a failed deployment')\n }\n throw new Error('Timed out waiting for the Dokploy deployment to finish')\n }\n\n private async waitForHealth(url: string) {\n const deadline = this.now() + (this.options.healthTimeoutMs ?? 300_000)\n let lastFailure = 'no response'\n while (this.now() < deadline) {\n try {\n // Polling is intentionally sequential; each response determines whether another request is needed.\n // oxlint-disable-next-line no-await-in-loop\n const response = await this.request(url, { signal: AbortSignal.timeout(Math.max(1, deadline - this.now())) })\n if (response.status === 200) return\n lastFailure = `status ${response.status}`\n } catch (error) {\n lastFailure = error instanceof Error ? error.message : String(error)\n }\n // oxlint-disable-next-line no-await-in-loop\n await this.pause(this.options.pollIntervalMs ?? 5_000)\n }\n throw new Error(`Timed out waiting for ${url} (${lastFailure})`)\n }\n}\n\nexport function pullRequestNumber(value: string) {\n if (!/^\\d+$/.test(value)) throw new Error('pull request number must contain only digits')\n return value\n}\n\nexport function previewHostname(value: string) {\n const parsed = new URL(`https://${value}`)\n if (parsed.hostname !== value || parsed.port || parsed.username || parsed.password || parsed.pathname !== '/') {\n throw new Error('preview hostname must be a bare hostname')\n }\n return value\n}\n\nfunction generatedPreviewHostname(value: string, serverIp: string, appName: string) {\n const host = previewHostname(value)\n if (!isCurrentGeneratedPreviewHostname(host, appName, serverIp))\n throw new Error('domain.generateDomain did not return an sslip.io hostname for the Dokploy server')\n return host\n}\n\nfunction isGeneratedPreviewHostname(host: string, appName: string) {\n return new RegExp(`^${regularExpressionLiteral(appName.slice(0, 40))}-[a-f\\\\d]{6}-[a-f\\\\d-]+\\\\.sslip\\\\.io$`).test(host)\n}\n\nfunction isCurrentGeneratedPreviewHostname(host: string, appName: string, serverIp: string) {\n return new RegExp(\n `^${regularExpressionLiteral(appName.slice(0, 40))}-[a-f\\\\d]{6}-${regularExpressionLiteral(encodedIp(serverIp))}\\\\.sslip\\\\.io$`,\n ).test(host)\n}\n\nfunction regularExpressionLiteral(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction encodedIp(address: string) {\n return address.replaceAll('.', '-').replaceAll(':', '-')\n}\n\nconst generatedPreviewName = 'ras-preview'\n\nconst blockedPreviewAddresses = new BlockList()\nfor (const [address, prefix, family] of [\n ['0.0.0.0', 8, 'ipv4'],\n ['10.0.0.0', 8, 'ipv4'],\n ['100.64.0.0', 10, 'ipv4'],\n ['127.0.0.0', 8, 'ipv4'],\n ['169.254.0.0', 16, 'ipv4'],\n ['172.16.0.0', 12, 'ipv4'],\n ['192.0.0.0', 24, 'ipv4'],\n ['192.0.2.0', 24, 'ipv4'],\n ['192.88.99.0', 24, 'ipv4'],\n ['192.168.0.0', 16, 'ipv4'],\n ['198.18.0.0', 15, 'ipv4'],\n ['198.51.100.0', 24, 'ipv4'],\n ['203.0.113.0', 24, 'ipv4'],\n ['224.0.0.0', 4, 'ipv4'],\n ['240.0.0.0', 4, 'ipv4'],\n ['::', 128, 'ipv6'],\n ['::1', 128, 'ipv6'],\n ['64:ff9b:1::', 48, 'ipv6'],\n ['100::', 64, 'ipv6'],\n ['100:0:0:1::', 64, 'ipv6'],\n ['2001::', 23, 'ipv6'],\n ['2001:db8::', 32, 'ipv6'],\n ['2002::', 16, 'ipv6'],\n ['3fff::', 20, 'ipv6'],\n ['5f00::', 16, 'ipv6'],\n ['fc00::', 7, 'ipv6'],\n ['fe80::', 10, 'ipv6'],\n ['ff00::', 8, 'ipv6'],\n] as const) {\n blockedPreviewAddresses.addSubnet(address, prefix, family)\n}\n\nfunction previewServerIp(value: unknown) {\n if (typeof value !== 'string') throw new Error('Dokploy requires a public server IP before it can generate preview domains')\n const address = value.trim().toLowerCase()\n const version = isIP(address)\n const family = version === 4 ? 'ipv4' : version === 6 ? 'ipv6' : undefined\n if (!family || address.startsWith('::ffff:') || blockedPreviewAddresses.check(address, family)) {\n throw new Error('Dokploy requires a public server IP before it can generate preview domains')\n }\n return address\n}\n\nfunction previewApplicationPrNumber(name: string, applicationName: (prNumber: string) => string) {\n const match = /^(\\d+)$/.exec(name.match(/(\\d+)$/)?.[1] ?? '')\n const prNumber = match?.[1]\n return prNumber && applicationName(prNumber) === name ? prNumber : undefined\n}\n\nfunction requiredEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n const value = optionalEnvironment(environment, name)\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction optionalEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n return environment[name]?.trim() || undefined\n}\n"]}
@@ -1,5 +1,17 @@
1
- import type { GitHubPreviewOptions, PreviewStatus } from './github.js';
1
+ import type { GitHubPreviewOptions, PreviewStatusState } from './github.js';
2
2
  export declare function previewStatusFromEnvironment(stateValue: string | undefined, environment?: NodeJS.ProcessEnv): {
3
3
  options: GitHubPreviewOptions;
4
- status: PreviewStatus;
4
+ status: {
5
+ state: "deleted";
6
+ prNumber: string;
7
+ };
8
+ } | {
9
+ options: GitHubPreviewOptions;
10
+ status: {
11
+ state: Exclude<PreviewStatusState, 'deleted'>;
12
+ prNumber: string;
13
+ sha: string;
14
+ previewUrl?: string;
15
+ runUrl?: string;
16
+ };
5
17
  };
@@ -14,16 +14,20 @@ export function previewStatusFromEnvironment(stateValue, environment = process.e
14
14
  ...(checkName ? { checkName } : {}),
15
15
  };
16
16
  const prNumber = requiredEnvironment(environment, 'PR_NUMBER');
17
+ if (state === 'deleted')
18
+ return { options, status: { state, prNumber } };
17
19
  const runUrl = optionalEnvironment(environment, 'RUN_URL');
18
- const status = state === 'deleted'
19
- ? { state, prNumber }
20
- : {
21
- state,
22
- prNumber,
23
- sha: requiredEnvironment(environment, 'COMMIT_SHA'),
24
- previewUrl: requiredEnvironment(environment, 'PREVIEW_URL'),
25
- ...(runUrl ? { runUrl } : {}),
26
- };
20
+ const sha = requiredEnvironment(environment, 'COMMIT_SHA');
21
+ const previewUrl = optionalEnvironment(environment, 'PREVIEW_URL');
22
+ if (state === 'ready' && !previewUrl)
23
+ throw new Error('PREVIEW_URL is required');
24
+ const status = {
25
+ state,
26
+ prNumber,
27
+ sha,
28
+ ...(previewUrl ? { previewUrl } : {}),
29
+ ...(runUrl ? { runUrl } : {}),
30
+ };
27
31
  return { options, status };
28
32
  }
29
33
  function requiredEnvironment(environment, name) {
@@ -1 +1 @@
1
- {"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/preview/environment.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,IAAI,GAAG,CAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAA;AAElG,MAAM,UAAU,4BAA4B,CAAC,UAA8B,EAAE,WAAW,GAAsB,OAAO,CAAC,GAAG;IACvH,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAgC,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAA;IACxF,CAAC;IACD,MAAM,KAAK,GAAG,UAAgC,CAAA;IAC9C,MAAM,IAAI,GAAG,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;IAC7D,MAAM,SAAS,GAAG,mBAAmB,CAAC,WAAW,EAAE,oBAAoB,CAAC,CAAA;IACxE,MAAM,OAAO,GAAyB;QACpC,UAAU,EAAE,mBAAmB,CAAC,WAAW,EAAE,mBAAmB,CAAC;QACjE,KAAK,EAAE,mBAAmB,CAAC,WAAW,EAAE,UAAU,CAAC;QACnD,MAAM,EAAE,mBAAmB,CAAC,WAAW,EAAE,gBAAgB,CAAC;QAC1D,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAA;IACD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,mBAAmB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAC1D,MAAM,MAAM,GACV,KAAK,KAAK,SAAS;QACjB,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;QACrB,CAAC,CAAC;YACE,KAAK;YACL,QAAQ;YACR,GAAG,EAAE,mBAAmB,CAAC,WAAW,EAAE,YAAY,CAAC;YACnD,UAAU,EAAE,mBAAmB,CAAC,WAAW,EAAE,aAAa,CAAC;YAC3D,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAA;IACP,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC;AAED,SAAS,mBAAmB,CAAC,WAA8B,EAAE,IAAY;IACvE,MAAM,KAAK,GAAG,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IACpD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAA;IAClD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,mBAAmB,CAAC,WAA8B,EAAE,IAAY;IACvE,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAA;AAC/C,CAAC","sourcesContent":["import type { GitHubPreviewOptions, PreviewStatus, PreviewStatusState } from './github.js'\n\nconst states = new Set<PreviewStatusState>(['awaiting', 'building', 'ready', 'failed', 'deleted'])\n\nexport function previewStatusFromEnvironment(stateValue: string | undefined, environment: NodeJS.ProcessEnv = process.env) {\n if (!stateValue || !states.has(stateValue as PreviewStatusState)) {\n throw new Error('preview state must be awaiting, building, ready, failed, or deleted')\n }\n const state = stateValue as PreviewStatusState\n const note = optionalEnvironment(environment, 'PREVIEW_NOTE')\n const checkName = optionalEnvironment(environment, 'PREVIEW_CHECK_NAME')\n const options: GitHubPreviewOptions = {\n repository: requiredEnvironment(environment, 'GITHUB_REPOSITORY'),\n token: requiredEnvironment(environment, 'GH_TOKEN'),\n marker: requiredEnvironment(environment, 'PREVIEW_MARKER'),\n ...(note ? { note } : {}),\n ...(checkName ? { checkName } : {}),\n }\n const prNumber = requiredEnvironment(environment, 'PR_NUMBER')\n const runUrl = optionalEnvironment(environment, 'RUN_URL')\n const status: PreviewStatus =\n state === 'deleted'\n ? { state, prNumber }\n : {\n state,\n prNumber,\n sha: requiredEnvironment(environment, 'COMMIT_SHA'),\n previewUrl: requiredEnvironment(environment, 'PREVIEW_URL'),\n ...(runUrl ? { runUrl } : {}),\n }\n return { options, status }\n}\n\nfunction requiredEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n const value = optionalEnvironment(environment, name)\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction optionalEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n return environment[name]?.trim() || undefined\n}\n"]}
1
+ {"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/preview/environment.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,IAAI,GAAG,CAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAA;AAElG,MAAM,UAAU,4BAA4B,CAAC,UAA8B,EAAE,WAAW,GAAsB,OAAO,CAAC,GAAG;IACvH,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,UAAgC,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAA;IACxF,CAAC;IACD,MAAM,KAAK,GAAG,UAAgC,CAAA;IAC9C,MAAM,IAAI,GAAG,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;IAC7D,MAAM,SAAS,GAAG,mBAAmB,CAAC,WAAW,EAAE,oBAAoB,CAAC,CAAA;IACxE,MAAM,OAAO,GAAyB;QACpC,UAAU,EAAE,mBAAmB,CAAC,WAAW,EAAE,mBAAmB,CAAC;QACjE,KAAK,EAAE,mBAAmB,CAAC,WAAW,EAAE,UAAU,CAAC;QACnD,MAAM,EAAE,mBAAmB,CAAC,WAAW,EAAE,gBAAgB,CAAC;QAC1D,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAA;IACD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAA;IAC9D,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAA;IACxE,MAAM,MAAM,GAAG,mBAAmB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAC1D,MAAM,GAAG,GAAG,mBAAmB,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC1D,MAAM,UAAU,GAAG,mBAAmB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;IAClE,IAAI,KAAK,KAAK,OAAO,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAA;IAChF,MAAM,MAAM,GAAkB;QAC5B,KAAK;QACL,QAAQ;QACR,GAAG;QACH,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9B,CAAA;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC;AAED,SAAS,mBAAmB,CAAC,WAA8B,EAAE,IAAY;IACvE,MAAM,KAAK,GAAG,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IACpD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,cAAc,CAAC,CAAA;IAClD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,mBAAmB,CAAC,WAA8B,EAAE,IAAY;IACvE,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAA;AAC/C,CAAC","sourcesContent":["import type { GitHubPreviewOptions, PreviewStatus, PreviewStatusState } from './github.js'\n\nconst states = new Set<PreviewStatusState>(['awaiting', 'building', 'ready', 'failed', 'deleted'])\n\nexport function previewStatusFromEnvironment(stateValue: string | undefined, environment: NodeJS.ProcessEnv = process.env) {\n if (!stateValue || !states.has(stateValue as PreviewStatusState)) {\n throw new Error('preview state must be awaiting, building, ready, failed, or deleted')\n }\n const state = stateValue as PreviewStatusState\n const note = optionalEnvironment(environment, 'PREVIEW_NOTE')\n const checkName = optionalEnvironment(environment, 'PREVIEW_CHECK_NAME')\n const options: GitHubPreviewOptions = {\n repository: requiredEnvironment(environment, 'GITHUB_REPOSITORY'),\n token: requiredEnvironment(environment, 'GH_TOKEN'),\n marker: requiredEnvironment(environment, 'PREVIEW_MARKER'),\n ...(note ? { note } : {}),\n ...(checkName ? { checkName } : {}),\n }\n const prNumber = requiredEnvironment(environment, 'PR_NUMBER')\n if (state === 'deleted') return { options, status: { state, prNumber } }\n const runUrl = optionalEnvironment(environment, 'RUN_URL')\n const sha = requiredEnvironment(environment, 'COMMIT_SHA')\n const previewUrl = optionalEnvironment(environment, 'PREVIEW_URL')\n if (state === 'ready' && !previewUrl) throw new Error('PREVIEW_URL is required')\n const status: PreviewStatus = {\n state,\n prNumber,\n sha,\n ...(previewUrl ? { previewUrl } : {}),\n ...(runUrl ? { runUrl } : {}),\n }\n return { options, status }\n}\n\nfunction requiredEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n const value = optionalEnvironment(environment, name)\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nfunction optionalEnvironment(environment: NodeJS.ProcessEnv, name: string) {\n return environment[name]?.trim() || undefined\n}\n"]}
@@ -8,7 +8,14 @@ export async function reportPreviewStatus(options, status) {
8
8
  const request = options.fetch ?? fetch;
9
9
  const api = (path, init) => github(request, options.token, `/repos/${repository}${path}`, init);
10
10
  if (status.state !== 'deleted') {
11
- await updateCheck(api, options.checkName ?? 'PR preview deploy', commitSha(status.sha), status);
11
+ const sha = commitSha(status.sha);
12
+ if (status.state === 'ready' && !status.previewUrl)
13
+ throw new Error('preview URL is required for ready preview status');
14
+ if (status.previewUrl)
15
+ webUrl(status.previewUrl, 'preview URL');
16
+ if (status.runUrl)
17
+ webUrl(status.runUrl, 'workflow run URL');
18
+ await updateCheck(api, options.checkName ?? 'PR preview deploy', sha, status);
12
19
  }
13
20
  const comments = await issueComments(api, prNumber);
14
21
  const existing = comments.find((comment) => comment.body?.includes(marker));
@@ -51,15 +58,12 @@ function previewComment(options, status, previous) {
51
58
  awaiting: `⏸️ The preview of \`${sha}\` is waiting for a maintainer to approve its build.${standing}`,
52
59
  building: `πŸ”„ Deploying \`${sha}\`.${standing}`,
53
60
  ready: `βœ… Preview is up to date with commit \`${sha}\`.`,
54
- failed: `❌ Deploying commit \`${sha}\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview below may be stale or unavailable.`,
61
+ failed: `❌ Deploying commit \`${sha}\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview may be stale or unavailable.`,
55
62
  }[status.state];
56
- if (!status.previewUrl)
57
- throw new Error('preview URL is required for active preview status');
58
63
  return [
59
64
  options.marker,
60
65
  heading,
61
- '',
62
- `Preview: ${webUrl(status.previewUrl, 'preview URL')}`,
66
+ ...(status.previewUrl ? ['', `Preview: ${webUrl(status.previewUrl, 'preview URL')}`] : []),
63
67
  ...(options.note ? ['', options.note] : []),
64
68
  ].join('\n');
65
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/preview/github.ts"],"names":[],"mappings":"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","sourcesContent":["import { pullRequestNumber } from './dokploy.js'\n\nexport type PreviewStatusState = 'awaiting' | 'building' | 'ready' | 'failed' | 'deleted'\n\nexport type GitHubPreviewOptions = {\n repository: string\n token: string\n marker: string\n note?: string\n checkName?: string\n fetch?: typeof fetch\n}\n\nexport type PreviewStatus =\n | { state: 'deleted'; prNumber: string }\n | {\n state: Exclude<PreviewStatusState, 'deleted'>\n prNumber: string\n sha: string\n previewUrl?: string\n runUrl?: string\n }\n\ntype Check = { id: number }\ntype Comment = { id: number; body?: string }\n\nexport async function reportPreviewStatus(options: GitHubPreviewOptions, status: PreviewStatus) {\n const repository = githubRepository(options.repository)\n const marker = commentMarker(options.marker)\n if (!options.token) throw new Error('GitHub token is required')\n const prNumber = pullRequestNumber(status.prNumber)\n const request = options.fetch ?? fetch\n const api = <T>(path: string, init?: RequestInit) => github<T>(request, options.token, `/repos/${repository}${path}`, init)\n\n if (status.state !== 'deleted') {\n await updateCheck(api, options.checkName ?? 'PR preview deploy', commitSha(status.sha), status)\n }\n const comments = await issueComments(api, prNumber)\n const existing = comments.find((comment) => comment.body?.includes(marker))\n const body = previewComment({ ...options, marker }, status, existing?.body)\n if (existing) await api(`/issues/comments/${existing.id}`, jsonRequest('PATCH', { body }))\n else await api(`/issues/${prNumber}/comments`, jsonRequest('POST', { body }))\n}\n\nasync function updateCheck(\n api: <T>(path: string, init?: RequestInit) => Promise<T>,\n name: string,\n sha: string,\n status: Extract<PreviewStatus, { state: Exclude<PreviewStatusState, 'deleted'> }>,\n) {\n const checks = await api<{ check_runs: Check[] }>(`/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=latest`)\n const state = checkState(status.state)\n const body = {\n name,\n head_sha: sha,\n status: state.status,\n ...(state.conclusion ? { conclusion: state.conclusion } : {}),\n ...(status.runUrl ? { details_url: webUrl(status.runUrl, 'workflow run URL') } : {}),\n output: { title: name, summary: state.summary },\n }\n const existing = checks.check_runs[0]\n if (existing) await api(`/check-runs/${existing.id}`, jsonRequest('PATCH', body))\n else await api('/check-runs', jsonRequest('POST', body))\n}\n\nasync function issueComments(api: <T>(path: string, init?: RequestInit) => Promise<T>, prNumber: string, page = 1): Promise<Comment[]> {\n if (page > 10) throw new Error('preview comment lookup exceeded 1,000 comments')\n const comments = await api<Comment[]>(`/issues/${prNumber}/comments?per_page=100&page=${page}`)\n return comments.length < 100 ? comments : [...comments, ...(await issueComments(api, prNumber, page + 1))]\n}\n\nfunction previewComment(options: GitHubPreviewOptions, status: PreviewStatus, previous?: string) {\n if (status.state === 'deleted') return `${options.marker}\\nπŸ—‘οΈ Preview deleted because this pull request was closed.`\n const sha = commitSha(status.sha).slice(0, 7)\n const standingSha = previous?.match(/up to date with commit `([0-9a-f]{7})`/)?.[1]\n const standing = standingSha ? ` The preview of \\`${standingSha}\\` stays up until it does.` : ''\n const heading = {\n awaiting: `⏸️ The preview of \\`${sha}\\` is waiting for a maintainer to approve its build.${standing}`,\n building: `πŸ”„ Deploying \\`${sha}\\`.${standing}`,\n ready: `βœ… Preview is up to date with commit \\`${sha}\\`.`,\n failed: `❌ Deploying commit \\`${sha}\\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview below may be stale or unavailable.`,\n }[status.state]\n if (!status.previewUrl) throw new Error('preview URL is required for active preview status')\n return [\n options.marker,\n heading,\n '',\n `Preview: ${webUrl(status.previewUrl, 'preview URL')}`,\n ...(options.note ? ['', options.note] : []),\n ].join('\\n')\n}\n\nfunction checkState(state: Exclude<PreviewStatusState, 'deleted'>) {\n return {\n awaiting: { status: 'queued', summary: 'The preview build is waiting for workflow approval.' },\n building: { status: 'in_progress', summary: 'A new preview version is deploying.' },\n ready: { status: 'completed', conclusion: 'success', summary: 'The preview is up to date.' },\n failed: { status: 'completed', conclusion: 'failure', summary: 'The preview deployment failed.' },\n }[state]\n}\n\nasync function github<T>(request: typeof fetch, token: string, path: string, init?: RequestInit): Promise<T> {\n const headers = new Headers(init?.headers)\n headers.set('accept', 'application/vnd.github+json')\n headers.set('authorization', `Bearer ${token}`)\n headers.set('x-github-api-version', '2022-11-28')\n const response = await request(`https://api.github.com${path}`, {\n ...init,\n headers,\n })\n const text = await response.text()\n if (!response.ok) throw new Error(`GitHub ${path.split('?')[0]} failed with ${response.status}: ${text.slice(0, 500)}`)\n return text ? (JSON.parse(text) as T) : (undefined as T)\n}\n\nfunction jsonRequest(method: 'POST' | 'PATCH', body: unknown): RequestInit {\n return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }\n}\n\nfunction githubRepository(value: string) {\n if (!/^[a-z\\d][\\w.-]*\\/[a-z\\d][\\w.-]*$/i.test(value)) throw new Error('repository must use an owner/name identifier')\n return value\n}\n\nfunction commentMarker(value: string) {\n if (!/^<!-- [a-z\\d-]+ -->$/i.test(value)) throw new Error('preview marker must be a named HTML comment')\n return value\n}\n\nfunction webUrl(value: string, name: string) {\n const url = new URL(value)\n if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error(`${name} must use HTTP or HTTPS`)\n return url.toString().replace(/\\/$/, '')\n}\n\nfunction commitSha(value: string | undefined) {\n if (!value || !/^[0-9a-f]{40}$/i.test(value)) throw new Error('commit SHA must contain 40 hexadecimal characters')\n return value\n}\n"]}
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/preview/github.ts"],"names":[],"mappings":"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","sourcesContent":["import { pullRequestNumber } from './dokploy.js'\n\nexport type PreviewStatusState = 'awaiting' | 'building' | 'ready' | 'failed' | 'deleted'\n\nexport type GitHubPreviewOptions = {\n repository: string\n token: string\n marker: string\n note?: string\n checkName?: string\n fetch?: typeof fetch\n}\n\nexport type PreviewStatus =\n | { state: 'deleted'; prNumber: string }\n | {\n state: Exclude<PreviewStatusState, 'deleted'>\n prNumber: string\n sha: string\n previewUrl?: string\n runUrl?: string\n }\n\ntype Check = { id: number }\ntype Comment = { id: number; body?: string }\n\nexport async function reportPreviewStatus(options: GitHubPreviewOptions, status: PreviewStatus) {\n const repository = githubRepository(options.repository)\n const marker = commentMarker(options.marker)\n if (!options.token) throw new Error('GitHub token is required')\n const prNumber = pullRequestNumber(status.prNumber)\n const request = options.fetch ?? fetch\n const api = <T>(path: string, init?: RequestInit) => github<T>(request, options.token, `/repos/${repository}${path}`, init)\n\n if (status.state !== 'deleted') {\n const sha = commitSha(status.sha)\n if (status.state === 'ready' && !status.previewUrl) throw new Error('preview URL is required for ready preview status')\n if (status.previewUrl) webUrl(status.previewUrl, 'preview URL')\n if (status.runUrl) webUrl(status.runUrl, 'workflow run URL')\n await updateCheck(api, options.checkName ?? 'PR preview deploy', sha, status)\n }\n const comments = await issueComments(api, prNumber)\n const existing = comments.find((comment) => comment.body?.includes(marker))\n const body = previewComment({ ...options, marker }, status, existing?.body)\n if (existing) await api(`/issues/comments/${existing.id}`, jsonRequest('PATCH', { body }))\n else await api(`/issues/${prNumber}/comments`, jsonRequest('POST', { body }))\n}\n\nasync function updateCheck(\n api: <T>(path: string, init?: RequestInit) => Promise<T>,\n name: string,\n sha: string,\n status: Extract<PreviewStatus, { state: Exclude<PreviewStatusState, 'deleted'> }>,\n) {\n const checks = await api<{ check_runs: Check[] }>(`/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=latest`)\n const state = checkState(status.state)\n const body = {\n name,\n head_sha: sha,\n status: state.status,\n ...(state.conclusion ? { conclusion: state.conclusion } : {}),\n ...(status.runUrl ? { details_url: webUrl(status.runUrl, 'workflow run URL') } : {}),\n output: { title: name, summary: state.summary },\n }\n const existing = checks.check_runs[0]\n if (existing) await api(`/check-runs/${existing.id}`, jsonRequest('PATCH', body))\n else await api('/check-runs', jsonRequest('POST', body))\n}\n\nasync function issueComments(api: <T>(path: string, init?: RequestInit) => Promise<T>, prNumber: string, page = 1): Promise<Comment[]> {\n if (page > 10) throw new Error('preview comment lookup exceeded 1,000 comments')\n const comments = await api<Comment[]>(`/issues/${prNumber}/comments?per_page=100&page=${page}`)\n return comments.length < 100 ? comments : [...comments, ...(await issueComments(api, prNumber, page + 1))]\n}\n\nfunction previewComment(options: GitHubPreviewOptions, status: PreviewStatus, previous?: string) {\n if (status.state === 'deleted') return `${options.marker}\\nπŸ—‘οΈ Preview deleted because this pull request was closed.`\n const sha = commitSha(status.sha).slice(0, 7)\n const standingSha = previous?.match(/up to date with commit `([0-9a-f]{7})`/)?.[1]\n const standing = standingSha ? ` The preview of \\`${standingSha}\\` stays up until it does.` : ''\n const heading = {\n awaiting: `⏸️ The preview of \\`${sha}\\` is waiting for a maintainer to approve its build.${standing}`,\n building: `πŸ”„ Deploying \\`${sha}\\`.${standing}`,\n ready: `βœ… Preview is up to date with commit \\`${sha}\\`.`,\n failed: `❌ Deploying commit \\`${sha}\\` failed${status.runUrl ? ` ([workflow run](${webUrl(status.runUrl, 'workflow run URL')}))` : ''}. The preview may be stale or unavailable.`,\n }[status.state]\n return [\n options.marker,\n heading,\n ...(status.previewUrl ? ['', `Preview: ${webUrl(status.previewUrl, 'preview URL')}`] : []),\n ...(options.note ? ['', options.note] : []),\n ].join('\\n')\n}\n\nfunction checkState(state: Exclude<PreviewStatusState, 'deleted'>) {\n return {\n awaiting: { status: 'queued', summary: 'The preview build is waiting for workflow approval.' },\n building: { status: 'in_progress', summary: 'A new preview version is deploying.' },\n ready: { status: 'completed', conclusion: 'success', summary: 'The preview is up to date.' },\n failed: { status: 'completed', conclusion: 'failure', summary: 'The preview deployment failed.' },\n }[state]\n}\n\nasync function github<T>(request: typeof fetch, token: string, path: string, init?: RequestInit): Promise<T> {\n const headers = new Headers(init?.headers)\n headers.set('accept', 'application/vnd.github+json')\n headers.set('authorization', `Bearer ${token}`)\n headers.set('x-github-api-version', '2022-11-28')\n const response = await request(`https://api.github.com${path}`, {\n ...init,\n headers,\n })\n const text = await response.text()\n if (!response.ok) throw new Error(`GitHub ${path.split('?')[0]} failed with ${response.status}: ${text.slice(0, 500)}`)\n return text ? (JSON.parse(text) as T) : (undefined as T)\n}\n\nfunction jsonRequest(method: 'POST' | 'PATCH', body: unknown): RequestInit {\n return { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }\n}\n\nfunction githubRepository(value: string) {\n if (!/^[a-z\\d][\\w.-]*\\/[a-z\\d][\\w.-]*$/i.test(value)) throw new Error('repository must use an owner/name identifier')\n return value\n}\n\nfunction commentMarker(value: string) {\n if (!/^<!-- [a-z\\d-]+ -->$/i.test(value)) throw new Error('preview marker must be a named HTML comment')\n return value\n}\n\nfunction webUrl(value: string, name: string) {\n const url = new URL(value)\n if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error(`${name} must use HTTP or HTTPS`)\n return url.toString().replace(/\\/$/, '')\n}\n\nfunction commitSha(value: string | undefined) {\n if (!value || !/^[0-9a-f]{40}$/i.test(value)) throw new Error('commit SHA must contain 40 hexadecimal characters')\n return value\n}\n"]}
@@ -1,5 +1,14 @@
1
1
  import { expect, test } from '@playwright/test'
2
2
 
3
+ test('keeps authentication submission disabled before hydration', async ({ browser }) => {
4
+ const context = await browser.newContext({ javaScriptEnabled: false })
5
+ const page = await context.newPage()
6
+ await page.goto('/')
7
+
8
+ await expect(page.getByRole('button', { name: 'Create account', exact: true })).toBeDisabled()
9
+ await context.close()
10
+ })
11
+
3
12
  test('signs up, authorizes resources, persists an upload, and receives an outbox publication', async ({ browser, request }, testInfo) => {
4
13
  const run = Date.now()
5
14
  const firstMessage = `first message ${run}`
@@ -1,7 +1,7 @@
1
1
  import { useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
2
2
  import { createFileRoute } from '@tanstack/react-router'
3
3
  import { useAuthAction } from 'ras-stack/auth/react'
4
- import { useState } from 'react'
4
+ import { useEffect, useState } from 'react'
5
5
  import { authClient } from '../client/auth'
6
6
  import { snapshotQuery } from '../client/queries'
7
7
  import { queryErrorMessage } from '../client/queryClient'
@@ -38,10 +38,12 @@ function Home() {
38
38
  }
39
39
 
40
40
  function Authentication({ emailConfigured }: { emailConfigured: boolean }) {
41
+ const [hydrated, setHydrated] = useState(false)
41
42
  const [mode, setMode] = useState<'sign-up' | 'sign-in'>('sign-up')
42
43
  const [notice, setNotice] = useState('')
43
44
  const queryClient = useQueryClient()
44
45
  const action = useAuthAction()
46
+ useEffect(() => setHydrated(true), [])
45
47
  return (
46
48
  <form
47
49
  onSubmit={async (event) => {
@@ -84,7 +86,7 @@ function Authentication({ emailConfigured }: { emailConfigured: boolean }) {
84
86
  type="password"
85
87
  />
86
88
  </label>
87
- <button disabled={action.busy}>{mode === 'sign-up' ? 'Create account' : 'Sign in'}</button>
89
+ <button disabled={!hydrated || action.busy}>{mode === 'sign-up' ? 'Create account' : 'Sign in'}</button>
88
90
  <button type="button" onClick={() => setMode(mode === 'sign-up' ? 'sign-in' : 'sign-up')}>
89
91
  {mode === 'sign-up' ? 'Use an existing account' : 'Create a new account'}
90
92
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.41.1",
3
+ "version": "0.42.1",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",