refactor: split control-plane cli modules
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. publish module for scripts/src/ci.ts.
|
||||
|
||||
// Moved mechanically from scripts/src/ci.ts:2568-2959 for #903.
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, posix as posixPath } from "node:path";
|
||||
import { blockedCatalogArtifactIds, catalogSummary, findCiCatalogArtifact, loadCiCatalog, supportedSourceBuildArtifactIds, type CiCatalogArtifact, type CiSourceBuildCatalogArtifact, type CiUpstreamImageCatalogArtifact } from "../ci-catalog";
|
||||
import { runCommand } from "../command";
|
||||
import { type UniDeskConfig, repoRoot, rootPath } from "../config";
|
||||
import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "../deploy-ssh-identity";
|
||||
import { jobWithTail, listJobs, readJob, startJob } from "../jobs";
|
||||
import { coreInternalFetch } from "../microservices";
|
||||
import {
|
||||
artifactRegistryReadonlyResultFromCommand,
|
||||
buildArtifactRegistryReadonlyProbe,
|
||||
parseArtifactRegistryOptions,
|
||||
type ArtifactRegistryReadonlyProbe,
|
||||
} from "../artifact-registry";
|
||||
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard";
|
||||
import { runSshCommandCapture } from "../ssh";
|
||||
|
||||
import type { ArtifactSummaryContext, CiOptions, CiPublishBackendCoreOptions, CiPublishUserServiceArtifactOptions, PublishPreflightTransport } from "./types";
|
||||
import { artifactSummaryDefaults } from "./artifact-summary";
|
||||
import { publishUserServiceArtifactDirectDocker } from "./direct-docker";
|
||||
import { blockedArtifactResult, blockedReason, numberOption, publishTransportOption, repoConnectivityProbeUrl, repoNeedsGithubSshIdentity, repoSshUrl, requireFullCommit, requireRepoRelativePath, requireServiceId, resolveCatalogArtifact, stringOption, userServicePublishBoundaryBlock } from "./options";
|
||||
import { backendCoreArtifactPipelineRunManifest, backendCoreArtifactSourceHostPath, pipelineRunManifest, userServiceArtifactPipelineRunManifest, userServiceArtifactSourceHostPath } from "./pipelinerun";
|
||||
import { pipelineRunWaitSucceeded, readPipelineRunCondition, remoteCreatePipelineRun, waitForPipelineRun } from "./pipelinerun-runtime";
|
||||
import { publishPreflightControlChannelOrder, publishPreflightFailedScopes } from "./preflight";
|
||||
import { assertArtifactSummaryComplete, publishBackendCorePreflight, publishUserServicePreflight, readArtifactSummaryFromPipelineRun } from "./publish-preflight";
|
||||
import { localPublishPreflightTransport } from "./remote";
|
||||
import { backendCoreArtifactRequirements, backendCoreDevApplyPath } from "./runner-preflight";
|
||||
import { prepareBackendCoreArtifactSource, prepareClaudeqqArtifactSource, prepareUserServiceArtifactSource } from "./source-prepare";
|
||||
import { d601ProviderId, providerGatewayWsEgressProxyUrl } from "./types";
|
||||
|
||||
export async function run(options: CiOptions): Promise<Record<string, unknown>> {
|
||||
const name = await remoteCreatePipelineRun(pipelineRunManifest(options), options.target);
|
||||
const wait = await waitForPipelineRun(name, options.waitMs, options.target);
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name, options.target);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
providerId: options.target.providerId,
|
||||
pipelineRun: name,
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
revision: options.revision,
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
dispatchOk: wait.ok,
|
||||
dispatchStatus: wait.status,
|
||||
dispatchExitCode: wait.exitCode,
|
||||
stdoutTail: wait.stdout.slice(-6000),
|
||||
stderrTail: wait.stderr.slice(-6000),
|
||||
},
|
||||
condition,
|
||||
next: [
|
||||
`bun scripts/cli.ts ci logs ${name} --provider-id ${options.target.providerId}`,
|
||||
`bun scripts/cli.ts ci status --provider-id ${options.target.providerId}`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
|
||||
const summaryContext: ArtifactSummaryContext = {
|
||||
serviceId: "backend-core",
|
||||
commit: options.commit,
|
||||
repoUrl: options.repoUrl,
|
||||
dockerfile: options.dockerfile,
|
||||
imageRepository: options.imageRepository,
|
||||
};
|
||||
const plannedArtifact = artifactSummaryDefaults(summaryContext);
|
||||
if (options.dryRun) {
|
||||
return runCiPublishBackendCoreDryRunPreflight(config, [
|
||||
"publish-backend-core",
|
||||
"--commit",
|
||||
options.commit,
|
||||
"--dry-run",
|
||||
...(options.waitMs > 0 ? ["--wait-ms", String(options.waitMs)] : []),
|
||||
], localPublishPreflightTransport, options);
|
||||
}
|
||||
const source = await prepareBackendCoreArtifactSource(config, options);
|
||||
const name = await remoteCreatePipelineRun(backendCoreArtifactPipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
const artifact = waitSucceeded && wait !== null
|
||||
? await readArtifactSummaryFromPipelineRun(name, summaryContext)
|
||||
: plannedArtifact;
|
||||
if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
pipelineRun: name,
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
source,
|
||||
artifact: artifact.imageRef,
|
||||
artifactSummary: artifact,
|
||||
boundary: "CI publishes the image to D601 registry; CD must pull it and must not build backend-core",
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
dispatchOk: wait.ok,
|
||||
dispatchStatus: wait.status,
|
||||
dispatchExitCode: wait.exitCode,
|
||||
stdoutTail: wait.stdout.slice(-6000),
|
||||
stderrTail: wait.stderr.slice(-6000),
|
||||
},
|
||||
condition,
|
||||
next: [
|
||||
`bun scripts/cli.ts ci logs ${name}`,
|
||||
`bun scripts/cli.ts deploy apply --env prod --service backend-core --commit ${options.commit}`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function publishUserServiceArtifact(config: UniDeskConfig, options: CiPublishUserServiceArtifactOptions): Promise<Record<string, unknown>> {
|
||||
const summaryContext: ArtifactSummaryContext = {
|
||||
serviceId: options.serviceId,
|
||||
dockerfile: options.dockerfile,
|
||||
commit: options.commit,
|
||||
repoUrl: options.repoUrl,
|
||||
imageRepository: options.imageRepository,
|
||||
};
|
||||
const plannedArtifact = artifactSummaryDefaults(summaryContext);
|
||||
const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
|
||||
if (options.dryRun) {
|
||||
const preflight = await publishUserServicePreflight(config, options, plannedArtifact, localPublishPreflightTransport);
|
||||
return {
|
||||
ok: preflight.ok,
|
||||
mode: "dry-run-preflight",
|
||||
runnerDisposition: preflight.runnerDisposition,
|
||||
pipeline: "unidesk-user-service-artifact-publish",
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
supportedArtifactPublish: preflight.supportedArtifactPublish,
|
||||
missingChannels: preflight.missingChannels,
|
||||
missingControlChannels: preflight.missingControlChannels,
|
||||
controlChannels: preflight.controlChannels,
|
||||
channels: preflight.channels,
|
||||
failureClassification: preflight.failureClassification,
|
||||
failedScopes: publishPreflightFailedScopes(preflight),
|
||||
recommendedAction: preflight.recommendedAction,
|
||||
remoteCommandShape: preflight.remoteCommandShape,
|
||||
controlPlane: preflight.controlPlane,
|
||||
registry: preflight.registry,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
source: {
|
||||
ok: preflight.ok,
|
||||
mode: "planned-only",
|
||||
providerId: d601ProviderId,
|
||||
repoUrl: options.repoUrl,
|
||||
repoFetchUrl: plannedRepoFetchUrl,
|
||||
repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
dockerfile: options.dockerfile,
|
||||
imageRepository: options.imageRepository,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
...(options.serviceId === "claudeqq" ? { overlay: "UniDesk claudeqq Dockerfile and unidesk-adapter.cjs are injected before Tekton build" } : {}),
|
||||
},
|
||||
artifact: plannedArtifact.imageRef,
|
||||
artifactSummary: plannedArtifact,
|
||||
controlledPublish: {
|
||||
environment: "D601",
|
||||
namespace: "unidesk-ci",
|
||||
pipeline: "unidesk-user-service-artifact-publish",
|
||||
command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --wait-ms 1200000 --transport ${options.transport}`,
|
||||
requiresReadyControlChannels: publishPreflightControlChannelOrder,
|
||||
},
|
||||
boundary: preflight.boundary,
|
||||
next: preflight.next,
|
||||
};
|
||||
}
|
||||
if (options.transport === "direct-docker" || (options.transport === "auto" && options.serviceId === "code-queue" && options.repoUrl === "https://github.com/pikasTech/unidesk")) {
|
||||
return publishUserServiceArtifactDirectDocker(options, summaryContext);
|
||||
}
|
||||
const source = options.serviceId === "claudeqq"
|
||||
? await prepareClaudeqqArtifactSource(config, options)
|
||||
: await prepareUserServiceArtifactSource(config, options);
|
||||
const name = await remoteCreatePipelineRun(userServiceArtifactPipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
const artifact = waitSucceeded && wait !== null
|
||||
? await readArtifactSummaryFromPipelineRun(name, summaryContext)
|
||||
: plannedArtifact;
|
||||
if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
pipelineRun: name,
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
source,
|
||||
artifact: artifact.imageRef,
|
||||
artifactSummary: artifact,
|
||||
boundary: "CI publishes the user-service image to the D601 registry only; it must not deploy production or mutate the production namespace",
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
dispatchOk: wait.ok,
|
||||
dispatchStatus: wait.status,
|
||||
dispatchExitCode: wait.exitCode,
|
||||
stdoutTail: wait.stdout.slice(-6000),
|
||||
stderrTail: wait.stderr.slice(-6000),
|
||||
},
|
||||
condition,
|
||||
next: [
|
||||
`bun scripts/cli.ts ci logs ${name}`,
|
||||
"use artifactSummary.imageRef or artifactSummary.digestRef as later dev/prod deployment input",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCiPublishUserServiceDryRunPreflight(
|
||||
config: UniDeskConfig,
|
||||
args: string[],
|
||||
transport: PublishPreflightTransport,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const serviceId = requireServiceId(stringOption(args, "--service") ?? stringOption(args, "--service-id"));
|
||||
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
|
||||
if (!args.includes("--dry-run")) throw new Error("publish-user-service preflight requires --dry-run");
|
||||
if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
|
||||
throw new Error("ci publish-user-service reads source repo from CI.json; edit CI.json instead of using --repo");
|
||||
}
|
||||
const artifact = resolveCatalogArtifact(serviceId);
|
||||
if (artifact.kind === "source-build" && artifact.serviceId === "backend-core") {
|
||||
throw new Error("backend-core uses ci publish-backend-core; publish-user-service is for registered user services");
|
||||
}
|
||||
if (artifact.kind === "upstream-image") {
|
||||
return blockedArtifactResult(artifact, commit, artifact.blockedReason);
|
||||
}
|
||||
if (artifact.status === "blocked") {
|
||||
return blockedArtifactResult(artifact, commit, blockedReason(artifact));
|
||||
}
|
||||
const dockerfile = requireRepoRelativePath(artifact.source.dockerfile, `CI.json.artifacts.${serviceId}.source.dockerfile`);
|
||||
const boundaryBlock = userServicePublishBoundaryBlock(config, serviceId, commit, artifact);
|
||||
if (boundaryBlock !== null) return boundaryBlock;
|
||||
const summaryContext: ArtifactSummaryContext = {
|
||||
serviceId,
|
||||
dockerfile,
|
||||
commit,
|
||||
repoUrl: artifact.source.repo,
|
||||
imageRepository: artifact.image.repository,
|
||||
};
|
||||
const plannedArtifact = artifactSummaryDefaults(summaryContext);
|
||||
const options: CiPublishUserServiceArtifactOptions = {
|
||||
repoUrl: artifact.source.repo,
|
||||
commit,
|
||||
waitMs: numberOption(args, "--wait-ms", 0),
|
||||
serviceId,
|
||||
dockerfile,
|
||||
imageRepository: artifact.image.repository,
|
||||
sourceHostPath: userServiceArtifactSourceHostPath(serviceId, commit),
|
||||
dryRun: true,
|
||||
transport: publishTransportOption(stringOption(args, "--transport")),
|
||||
};
|
||||
const preflight = await publishUserServicePreflight(config, options, plannedArtifact, transport);
|
||||
const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
|
||||
return {
|
||||
ok: preflight.ok,
|
||||
mode: "dry-run-preflight",
|
||||
runnerDisposition: preflight.runnerDisposition,
|
||||
pipeline: "unidesk-user-service-artifact-publish",
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
supportedArtifactPublish: preflight.supportedArtifactPublish,
|
||||
missingChannels: preflight.missingChannels,
|
||||
missingControlChannels: preflight.missingControlChannels,
|
||||
controlChannels: preflight.controlChannels,
|
||||
channels: preflight.channels,
|
||||
failureClassification: preflight.failureClassification,
|
||||
failedScopes: publishPreflightFailedScopes(preflight),
|
||||
recommendedAction: preflight.recommendedAction,
|
||||
remoteCommandShape: preflight.remoteCommandShape,
|
||||
controlPlane: preflight.controlPlane,
|
||||
registry: preflight.registry,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
source: {
|
||||
ok: preflight.ok,
|
||||
mode: "planned-only",
|
||||
providerId: d601ProviderId,
|
||||
repoUrl: options.repoUrl,
|
||||
repoFetchUrl: plannedRepoFetchUrl,
|
||||
repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
dockerfile: options.dockerfile,
|
||||
imageRepository: options.imageRepository,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
...(options.serviceId === "claudeqq" ? { overlay: "UniDesk claudeqq Dockerfile and unidesk-adapter.cjs are injected before Tekton build" } : {}),
|
||||
},
|
||||
artifact: plannedArtifact.imageRef,
|
||||
artifactSummary: plannedArtifact,
|
||||
controlledPublish: {
|
||||
environment: "D601",
|
||||
namespace: "unidesk-ci",
|
||||
pipeline: "unidesk-user-service-artifact-publish",
|
||||
command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --wait-ms 1200000 --transport ${options.transport}`,
|
||||
requiresReadyControlChannels: publishPreflightControlChannelOrder,
|
||||
},
|
||||
boundary: preflight.boundary,
|
||||
next: preflight.next,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCiPublishBackendCoreDryRunPreflight(
|
||||
config: UniDeskConfig,
|
||||
args: string[],
|
||||
transport: PublishPreflightTransport,
|
||||
resolvedOptions?: CiPublishBackendCoreOptions,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
|
||||
if (!args.includes("--dry-run")) throw new Error("publish-backend-core preflight requires --dry-run");
|
||||
if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
|
||||
throw new Error("ci publish-backend-core reads source repo from CI.json; edit CI.json instead of using --repo");
|
||||
}
|
||||
const artifact = resolveCatalogArtifact("backend-core");
|
||||
if (artifact.kind !== "source-build") throw new Error("backend-core must be modeled as a source-build artifact in CI.json");
|
||||
if (artifact.status === "blocked") return blockedArtifactResult(artifact, commit, blockedReason(artifact));
|
||||
const dockerfile = requireRepoRelativePath(artifact.source.dockerfile, "CI.json.artifacts.backend-core.source.dockerfile");
|
||||
const options: CiPublishBackendCoreOptions = resolvedOptions ?? {
|
||||
repoUrl: artifact.source.repo,
|
||||
commit,
|
||||
waitMs: numberOption(args, "--wait-ms", 0),
|
||||
sourceHostPath: backendCoreArtifactSourceHostPath(commit),
|
||||
dockerfile,
|
||||
imageRepository: artifact.image.repository,
|
||||
dryRun: true,
|
||||
};
|
||||
const summaryContext: ArtifactSummaryContext = {
|
||||
serviceId: "backend-core",
|
||||
commit,
|
||||
repoUrl: options.repoUrl,
|
||||
dockerfile: options.dockerfile,
|
||||
imageRepository: options.imageRepository,
|
||||
};
|
||||
const plannedArtifact = artifactSummaryDefaults(summaryContext);
|
||||
const preflight = await publishBackendCorePreflight(config, options, plannedArtifact, transport);
|
||||
const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
|
||||
const blockedScopes = publishPreflightFailedScopes(preflight);
|
||||
return {
|
||||
ok: preflight.ok,
|
||||
mode: "dry-run-preflight",
|
||||
runnerDisposition: preflight.runnerDisposition,
|
||||
pipeline: "unidesk-backend-core-artifact-publish",
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
targetCommit: options.commit,
|
||||
sourceRepo: options.repoUrl,
|
||||
serviceId: "backend-core",
|
||||
providerId: d601ProviderId,
|
||||
ciRunner: {
|
||||
providerId: d601ProviderId,
|
||||
environment: "D601",
|
||||
namespace: "unidesk-ci",
|
||||
pipeline: "unidesk-backend-core-artifact-publish",
|
||||
wouldBuildOnD601: true,
|
||||
realBuildRequiresAuthorization: true,
|
||||
},
|
||||
registryTarget: plannedArtifact.repository,
|
||||
wouldBuildOnD601: true,
|
||||
dryRunBuildStarted: false,
|
||||
supportedArtifactPublish: preflight.supportedArtifactPublish,
|
||||
missingChannels: preflight.missingChannels,
|
||||
missingControlChannels: preflight.missingControlChannels,
|
||||
controlChannels: preflight.controlChannels,
|
||||
channels: preflight.channels,
|
||||
failureClassification: preflight.failureClassification,
|
||||
failedScopes: blockedScopes,
|
||||
blockedScopes,
|
||||
recommendedAction: preflight.recommendedAction,
|
||||
remoteCommandShape: preflight.remoteCommandShape,
|
||||
remoteControlPlaneCandidate: preflight.controlPlane,
|
||||
controlPlane: preflight.controlPlane,
|
||||
registry: preflight.registry,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
source: {
|
||||
ok: preflight.ok,
|
||||
mode: "planned-only",
|
||||
providerId: d601ProviderId,
|
||||
repoUrl: options.repoUrl,
|
||||
repoFetchUrl: plannedRepoFetchUrl,
|
||||
repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
|
||||
commit: options.commit,
|
||||
serviceId: "backend-core",
|
||||
dockerfile: options.dockerfile,
|
||||
imageRepository: options.imageRepository,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
},
|
||||
sourceAuth: {
|
||||
repoFetchUrl: plannedRepoFetchUrl,
|
||||
egressProxy: providerGatewayWsEgressProxyUrl,
|
||||
gitSshProxy: repoNeedsGithubSshIdentity(plannedRepoFetchUrl),
|
||||
identityRequired: repoNeedsGithubSshIdentity(plannedRepoFetchUrl),
|
||||
},
|
||||
d601Ci: {
|
||||
providerId: d601ProviderId,
|
||||
namespace: "unidesk-ci",
|
||||
pipeline: "unidesk-backend-core-artifact-publish",
|
||||
wouldBuildOnD601: true,
|
||||
dryRunWillStartTekton: false,
|
||||
dryRunWillCompileRust: false,
|
||||
dryRunWillPushRegistry: false,
|
||||
},
|
||||
artifact: plannedArtifact.imageRef,
|
||||
artifactSummary: plannedArtifact,
|
||||
artifactRequirements: backendCoreArtifactRequirements(options, plannedArtifact),
|
||||
controlledPublish: {
|
||||
environment: "D601",
|
||||
namespace: "unidesk-ci",
|
||||
pipeline: "unidesk-backend-core-artifact-publish",
|
||||
command: `bun scripts/cli.ts ci publish-backend-core --commit ${options.commit} --wait-ms 1200000`,
|
||||
requiresReadyControlChannels: publishPreflightControlChannelOrder,
|
||||
requiresHumanAuthorization: true,
|
||||
},
|
||||
devApplyPath: backendCoreDevApplyPath(options, plannedArtifact),
|
||||
boundary: preflight.boundary,
|
||||
next: preflight.next,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user