@ebay-dt/bonfire-node 0.2.0-alpha.0 → 0.2.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deployment-provider.d.ts +1 -0
- package/dist/deployment-provider.d.ts.map +1 -1
- package/dist/deployment-provider.js +1 -0
- package/dist/deployment-provider.js.map +1 -1
- package/dist/git-repository.d.ts +3 -1
- package/dist/git-repository.d.ts.map +1 -1
- package/dist/git-repository.js +30 -0
- package/dist/git-repository.js.map +1 -1
- package/dist/github-cli.d.ts.map +1 -1
- package/dist/github-cli.js +3 -1
- package/dist/github-cli.js.map +1 -1
- package/dist/lifecycle/deployment-worktree.d.ts +35 -0
- package/dist/lifecycle/deployment-worktree.d.ts.map +1 -0
- package/dist/lifecycle/deployment-worktree.js +302 -0
- package/dist/lifecycle/deployment-worktree.js.map +1 -0
- package/dist/lifecycle/local-prototype-lifecycle.d.ts +2 -0
- package/dist/lifecycle/local-prototype-lifecycle.d.ts.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.js +170 -54
- package/dist/lifecycle/local-prototype-lifecycle.js.map +1 -1
- package/dist/lifecycle/repository-summary.d.ts +2 -0
- package/dist/lifecycle/repository-summary.d.ts.map +1 -1
- package/dist/lifecycle/repository-summary.js +14 -4
- package/dist/lifecycle/repository-summary.js.map +1 -1
- package/dist/lifecycle/repository-synchronization.d.ts +1 -0
- package/dist/lifecycle/repository-synchronization.d.ts.map +1 -1
- package/dist/lifecycle/repository-synchronization.js +54 -10
- package/dist/lifecycle/repository-synchronization.js.map +1 -1
- package/dist/lifecycle/runtime-context.d.ts +2 -0
- package/dist/lifecycle/runtime-context.d.ts.map +1 -1
- package/dist/lifecycle/runtime-context.js +5 -1
- package/dist/lifecycle/runtime-context.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-prototype-lifecycle.d.ts","sourceRoot":"","sources":["../../src/lifecycle/local-prototype-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["import { constants } from \"node:fs\";\nimport { lstat, open, rename, rm, writeFile } from \"node:fs/promises\";\nimport { randomUUID } from \"node:crypto\";\nimport { join, relative, sep } from \"node:path\";\nimport {\n definePrototypeRef,\n getPrototypeUrl,\n LifecycleInputError,\n lifecycleFailure,\n lifecycleSuccess,\n parseCreatePrototypeInput,\n parseDuplicatePrototypeInput,\n parseEditPrototypeInput,\n parseBonfireIdentity,\n parsePrototypeCommandInput,\n parsePrototypeMetadata,\n slugifyPrototypeTitle,\n PrototypeRefError,\n type BranchPreparationValue,\n type CreatePrototypeInput,\n type CreatePrototypeValue,\n type DeletePrototypeValue,\n type DeploymentValue,\n type DeploymentWarningCode,\n type DirtyObservationValue,\n type DiscardPrototypeValue,\n type DuplicatePrototypeInput,\n type DuplicatePrototypeValue,\n type EditPrototypeInput,\n type EditPrototypeValue,\n parseGitModeSetting,\n type GitMode,\n type LifecycleErrorCode,\n type LifecycleResult,\n type BonfireIdentity,\n type PrototypeCommandInput,\n type SavePrototypeValue,\n type RepositorySummary,\n type ThemeRegistry,\n} from \"@ebay-dt/bonfire-core\";\nimport {\n commitPath,\n discardPath,\n GitRepositoryError,\n pushBranch,\n type GitRepository,\n} from \"../git-repository.js\";\nimport { duplicatePrototype, PrototypeDuplicateError } from \"../duplicate.js\";\nimport {\n PrototypeCatalog,\n type PrototypeCatalogLike,\n} from \"../prototype-catalog.js\";\nimport { PrototypeScaffoldError, scaffoldPrototype } from \"../scaffold.js\";\nimport {\n createPrototypeLifecycleRuntime,\n type PrototypeLifecycleRuntimeContext,\n} from \"./runtime-context.js\";\nimport type {\n PreparePrototypeFiles,\n PreparePrototypeFilesRequest,\n} from \"../prepare-prototype-files.js\";\nimport {\n cleanupOwnedDestination,\n validateOwnedDestination,\n type OwnedDestinationReceipt,\n} from \"../internal/owned-destination.js\";\nimport {\n RepositoryBusyError,\n withRepositoryOperation,\n} from \"./operation-gate.js\";\nimport {\n DeploymentProviderError,\n type DeploymentProvider,\n} from \"../deployment-provider.js\";\nimport {\n assertIdentityBranchReady,\n synchronizeIdentityBranch,\n} from \"./repository-synchronization.js\";\nimport { getRepositorySummary } from \"./repository-summary.js\";\n\ntype LifecycleMethod<Input, Value> = (\n identity: BonfireIdentity,\n input: Input,\n) => Promise<LifecycleResult<Value>>;\n\nexport type PrototypeLifecycle = {\n /** Declares whether this lifecycle enforces the selected Git mode. */\n readonly gitMode?: GitMode;\n prepareBranch(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<BranchPreparationValue>>;\n create: LifecycleMethod<CreatePrototypeInput, CreatePrototypeValue>;\n duplicate: LifecycleMethod<DuplicatePrototypeInput, DuplicatePrototypeValue>;\n edit: LifecycleMethod<EditPrototypeInput, EditPrototypeValue>;\n delete: LifecycleMethod<PrototypeCommandInput, DeletePrototypeValue>;\n getDirtyState: LifecycleMethod<PrototypeCommandInput, DirtyObservationValue>;\n getRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>>;\n checkRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>>;\n save: LifecycleMethod<PrototypeCommandInput, SavePrototypeValue>;\n discard: LifecycleMethod<PrototypeCommandInput, DiscardPrototypeValue>;\n deploy: LifecycleMethod<PrototypeCommandInput, DeploymentValue>;\n};\n\nexport type LocalPrototypeLifecycleOptions = {\n repositoryRoot: string;\n prototypeRoot?: string;\n remote?: string;\n primaryBranch?: string;\n automaticBranchSwitch?: \"enabled\" | \"disabled\";\n gitMode?: GitMode;\n commitIdentity?: {\n name: string;\n email: string;\n };\n catalog?: PrototypeCatalogLike;\n preparePrototypeFiles?: PreparePrototypeFiles;\n deploymentProvider?: DeploymentProvider;\n applicationId?: string;\n themeRegistry?: ThemeRegistry;\n};\n\ntype InternalLocalPrototypeLifecycleOptions = LocalPrototypeLifecycleOptions & {\n runtime?: PrototypeLifecycleRuntimeContext;\n};\n\nfunction repositoryRelativePath(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n const absolutePath = join(runtime.prototypeRoot, ref.login, ref.slug);\n return relative(runtime.repositoryRoot, absolutePath).split(sep).join(\"/\");\n}\n\nfunction destinationRef(identity: BonfireIdentity, title: string) {\n const slug = slugifyPrototypeTitle(title);\n if (slug === null)\n throw new PrototypeRefError(\"title\", \"must produce a slug\");\n return definePrototypeRef({ login: identity.login, slug });\n}\n\nclass PrototypeNotFoundError extends Error {}\nclass PrototypeOwnershipError extends Error {}\nclass PrototypeMetadataFileError extends Error {}\nclass DeploymentUnavailableError extends Error {}\n\nfunction metadataPath(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n return join(runtime.prototypeRoot, ref.login, ref.slug, \"metadata.json\");\n}\n\nasync function assertPrototypeDirectory(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n const paths = [\n runtime.prototypeRoot,\n join(runtime.prototypeRoot, ref.login),\n join(runtime.prototypeRoot, ref.login, ref.slug),\n ];\n\n try {\n for (const path of paths) {\n const stats = await lstat(path);\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n throw new PrototypeMetadataFileError();\n }\n }\n } catch (error) {\n if (error instanceof PrototypeMetadataFileError) throw error;\n throw new PrototypeMetadataFileError();\n }\n}\n\nasync function readMetadataDocument(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n themeRegistry?: ThemeRegistry,\n) {\n const path = metadataPath(runtime, ref);\n try {\n const file = await open(\n path,\n constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),\n );\n try {\n const source = await file.readFile(\"utf8\");\n const document: unknown = JSON.parse(source);\n parsePrototypeMetadata(document, { themeRegistry });\n if (\n typeof document !== \"object\" ||\n document === null ||\n Array.isArray(document)\n ) {\n throw new PrototypeMetadataFileError();\n }\n return { ...(document as Record<string, unknown>) };\n } finally {\n await file.close();\n }\n } catch (error) {\n if (error instanceof PrototypeMetadataFileError) throw error;\n throw new PrototypeMetadataFileError();\n }\n}\n\nasync function removePrototypeDirectory(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n await assertPrototypeDirectory(runtime, ref);\n try {\n await rm(join(runtime.prototypeRoot, ref.login, ref.slug), {\n force: false,\n recursive: true,\n });\n } catch {\n throw new PrototypeMetadataFileError();\n }\n}\n\nasync function writeMetadataDocument(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n document: Record<string, unknown>,\n) {\n const path = metadataPath(runtime, ref);\n const temporaryPath = `${path}.${randomUUID()}.tmp`;\n\n try {\n const stats = await lstat(path);\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new PrototypeMetadataFileError();\n }\n await writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}\\n`, {\n encoding: \"utf8\",\n flag: \"wx\",\n });\n await rename(temporaryPath, path);\n } catch (error) {\n if (error instanceof PrototypeMetadataFileError) throw error;\n throw new PrototypeMetadataFileError();\n } finally {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n }\n}\n\nasync function mergeIdentityBranchToPrimary(\n repository: GitRepository,\n options: {\n branch: string;\n primaryBranch: string;\n remote: string;\n },\n) {\n if (options.branch === options.primaryBranch) return;\n\n const hasRemote = (await repository.remotes()).some(\n (remote) => remote.name === options.remote,\n );\n if (!hasRemote) return;\n\n await repository.checkout([options.primaryBranch]);\n try {\n await repository.merge([\n options.branch,\n \"--no-ff\",\n \"-m\",\n `merge: ${options.branch}`,\n ]);\n await repository.push(options.remote, options.primaryBranch);\n } finally {\n await repository.checkout([options.branch]);\n }\n}\n\nfunction mapError(error: unknown): LifecycleErrorCode {\n if (error instanceof PrototypeNotFoundError) return \"not-found\";\n if (error instanceof PrototypeOwnershipError) return \"forbidden\";\n if (error instanceof PrototypeMetadataFileError) return \"filesystem-failure\";\n if (error instanceof RepositoryBusyError) return \"repository-busy\";\n if (\n error instanceof PrototypeRefError ||\n error instanceof LifecycleInputError\n ) {\n return \"invalid-input\";\n }\n if (error instanceof GitRepositoryError) {\n if (error.kind === \"timeout\") return \"timeout\";\n if (error.kind === \"conflict\") return \"working-tree-conflict\";\n if (error.kind === \"invalid-configuration\") return \"unexpected-failure\";\n return \"git-failure\";\n }\n if (error instanceof PrototypeScaffoldError) {\n if (error.kind === \"collision\") return \"already-exists\";\n if (error.kind === \"invalid-input\") return \"invalid-input\";\n return \"filesystem-failure\";\n }\n if (error instanceof PrototypeDuplicateError) {\n if (error.kind === \"source-not-found\") return \"not-found\";\n if (error.kind === \"collision\") return \"already-exists\";\n if (error.kind === \"invalid-source\") return \"invalid-input\";\n return \"filesystem-failure\";\n }\n if (error instanceof DeploymentUnavailableError) {\n return \"deployment-unavailable\";\n }\n if (error instanceof DeploymentProviderError) {\n return error.kind === \"timeout\" ? \"timeout\" : \"deployment-failure\";\n }\n return \"unexpected-failure\";\n}\n\nfunction resultFrom<Value>(\n operation: () => Promise<Value>,\n): Promise<LifecycleResult<Value>> {\n return operation()\n .then(lifecycleSuccess)\n .catch((error: unknown) => lifecycleFailure(mapError(error)));\n}\n\nexport class LocalPrototypeLifecycle implements PrototypeLifecycle {\n readonly #runtime: PrototypeLifecycleRuntimeContext;\n readonly gitMode: GitMode;\n readonly #catalog: PrototypeCatalogLike;\n readonly #repository: GitRepository;\n readonly #preparePrototypeFiles: PreparePrototypeFiles;\n readonly #deploymentProvider?: DeploymentProvider;\n readonly #applicationId?: string;\n readonly #themeRegistry?: ThemeRegistry;\n\n constructor(options: LocalPrototypeLifecycleOptions) {\n const internalOptions = options as InternalLocalPrototypeLifecycleOptions;\n const runtime =\n internalOptions.runtime ?? createPrototypeLifecycleRuntime(internalOptions);\n this.#runtime = {\n ...runtime,\n gitMode: parseGitModeSetting(runtime.gitMode),\n };\n this.gitMode = this.#runtime.gitMode;\n this.#themeRegistry = options.themeRegistry;\n this.#catalog =\n internalOptions.catalog ??\n new PrototypeCatalog({\n ...this.#runtime,\n themeRegistry: this.#themeRegistry,\n });\n this.#repository = this.#runtime.gitRepository;\n this.#preparePrototypeFiles =\n internalOptions.preparePrototypeFiles ?? (() => undefined);\n this.#deploymentProvider = internalOptions.deploymentProvider;\n this.#applicationId = internalOptions.applicationId;\n if (\n this.#deploymentProvider &&\n (typeof this.#applicationId !== \"string\" ||\n this.#applicationId.length === 0)\n ) {\n throw new TypeError(\n \"applicationId must be provided when deploymentProvider is configured\",\n );\n }\n }\n\n async #verifiedIdentity(identity: BonfireIdentity) {\n return parseBonfireIdentity(identity);\n }\n\n async #currentBranch() {\n const branch = await this.#repository.currentBranch();\n if (branch === \"HEAD\" || branch.length === 0) {\n throw new GitRepositoryError(\"conflict\", \"current-branch\");\n }\n return branch;\n }\n\n async #ownedPrototype(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ) {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n if (normalizedInput.ref.login !== verifiedIdentity.login) {\n throw new PrototypeOwnershipError();\n }\n const summary = await this.#catalog.getSummary(normalizedInput.ref);\n if (!summary) {\n throw new PrototypeNotFoundError();\n }\n\n return {\n ref: normalizedInput.ref,\n path: repositoryRelativePath(this.#runtime, normalizedInput.ref),\n summary,\n };\n }\n\n async #prepareMutationBranch(login: string) {\n await assertIdentityBranchReady(this.#repository, login);\n return login;\n }\n\n async #prepareDestination(\n receipt: OwnedDestinationReceipt,\n expectedFiles: readonly string[],\n request: PreparePrototypeFilesRequest,\n ) {\n try {\n const frozenRequest = Object.freeze({\n ...request,\n ref: Object.freeze({ ...request.ref }),\n identity: Object.freeze({ ...request.identity }),\n ...(request.operation === \"duplicate\"\n ? { source: Object.freeze({ ...request.source }) }\n : {}),\n }) as PreparePrototypeFilesRequest;\n await this.#preparePrototypeFiles(frozenRequest);\n await validateOwnedDestination(\n receipt,\n expectedFiles,\n request.ref,\n this.#themeRegistry,\n );\n } catch {\n try {\n await cleanupOwnedDestination(receipt);\n } catch {\n // The failure remains a finite filesystem failure without retrying cleanup.\n }\n throw new PrototypeScaffoldError(\"filesystem\");\n }\n }\n\n async prepareBranch(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<BranchPreparationValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n if (\n this.#runtime.gitMode === \"manual\" ||\n this.#runtime.automaticBranchSwitch === \"disabled\"\n ) {\n await this.#currentBranch();\n return { status: \"disabled\" } satisfies BranchPreparationValue;\n }\n\n const synchronization = await synchronizeIdentityBranch(\n this.#repository,\n {\n identityBranch: verifiedIdentity.login,\n primaryBranch: this.#runtime.primaryBranch,\n remote: this.#runtime.remote,\n },\n );\n if (synchronization.status !== \"synchronized\") {\n return {\n status: synchronization.status,\n reason: synchronization.reason,\n ...(synchronization.affectedPaths.length > 0\n ? { affectedPaths: synchronization.affectedPaths }\n : {}),\n } satisfies BranchPreparationValue;\n }\n return { status: \"ready\" } satisfies BranchPreparationValue;\n },\n );\n });\n }\n\n async create(\n identity: BonfireIdentity,\n input: CreatePrototypeInput,\n ): Promise<LifecycleResult<CreatePrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parseCreatePrototypeInput(\n input,\n this.#themeRegistry,\n );\n const ref = destinationRef(verifiedIdentity, normalizedInput.title);\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n if (await this.#catalog.getSummary(ref)) {\n throw new PrototypeScaffoldError(\"collision\");\n }\n\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n const prepared = await scaffoldPrototype({\n paths: this.#runtime,\n ref,\n title: normalizedInput.title,\n description: normalizedInput.description,\n authorName: verifiedIdentity.name,\n theme: normalizedInput.theme,\n themeRegistry: this.#themeRegistry,\n });\n await this.#prepareDestination(\n prepared.receipt,\n prepared.expectedFiles,\n {\n operation: \"create\",\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n prototypePath: prepared.destination,\n ref,\n identity: verifiedIdentity,\n title: normalizedInput.title,\n description: normalizedInput.description,\n },\n );\n if (branch === undefined) {\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: \"manual\",\n } satisfies CreatePrototypeValue;\n }\n\n await commitPath(\n this.#repository,\n repositoryRelativePath(this.#runtime, ref),\n `init: ${ref.slug}`,\n );\n const push = await pushBranch(\n this.#repository,\n this.#runtime.remote,\n branch,\n );\n\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: push.remoteStatus,\n } satisfies CreatePrototypeValue;\n },\n );\n });\n }\n\n async edit(\n identity: BonfireIdentity,\n input: EditPrototypeInput,\n ): Promise<LifecycleResult<EditPrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parseEditPrototypeInput(\n input,\n this.#themeRegistry,\n );\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n const { ref, path } = await this.#ownedPrototype(verifiedIdentity, {\n ref: normalizedInput.ref,\n });\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n await assertPrototypeDirectory(this.#runtime, ref);\n const document = await readMetadataDocument(\n this.#runtime,\n ref,\n this.#themeRegistry,\n );\n const metadata = parsePrototypeMetadata(document, {\n themeRegistry: this.#themeRegistry,\n });\n if (metadata.authorLogin !== ref.login) {\n throw new PrototypeNotFoundError();\n }\n\n await writeMetadataDocument(this.#runtime, ref, {\n ...document,\n title: normalizedInput.title,\n description: normalizedInput.description,\n updatedAt: new Date().toISOString(),\n ...(normalizedInput.theme === undefined\n ? {}\n : { theme: normalizedInput.theme }),\n });\n if (branch !== undefined) {\n await commitPath(this.#repository, path, `edit: ${ref.slug}`);\n await pushBranch(this.#repository, this.#runtime.remote, branch);\n }\n\n return { edited: true, ref } satisfies EditPrototypeValue;\n },\n );\n });\n }\n\n async delete(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DeletePrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n const { ref, path } = await this.#ownedPrototype(\n verifiedIdentity,\n normalizedInput,\n );\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n await removePrototypeDirectory(this.#runtime, ref);\n if (branch !== undefined) {\n await commitPath(this.#repository, path, `delete: ${ref.slug}`);\n await pushBranch(this.#repository, this.#runtime.remote, branch);\n }\n\n return { deleted: true, ref } satisfies DeletePrototypeValue;\n },\n );\n });\n }\n\n async getDirtyState(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DirtyObservationValue>> {\n return resultFrom(async () => {\n const { path, summary } = await this.#ownedPrototype(identity, input);\n const status = await this.#repository.status([path]);\n return {\n status: status.files.length > 0 ? \"dirty\" : \"clean\",\n ...(summary.previewUrl !== undefined\n ? { previewUrl: summary.previewUrl }\n : {}),\n } satisfies DirtyObservationValue;\n });\n }\n\n async getRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n return getRepositorySummary({\n repository: this.#repository,\n catalog: this.#catalog,\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n identity: verifiedIdentity,\n mode: this.#runtime.gitMode,\n remote: this.#runtime.remote,\n primaryBranch: this.#runtime.primaryBranch,\n });\n });\n }\n\n async checkRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n return getRepositorySummary({\n repository: this.#repository,\n catalog: this.#catalog,\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n identity: verifiedIdentity,\n mode: this.#runtime.gitMode,\n remote: this.#runtime.remote,\n primaryBranch: this.#runtime.primaryBranch,\n refresh: true,\n });\n });\n }\n\n async deploy(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DeploymentValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n if (!this.#deploymentProvider) {\n throw new DeploymentUnavailableError();\n }\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n const { ref, path } = await this.#ownedPrototype(\n verifiedIdentity,\n normalizedInput,\n );\n const branch = await this.#prepareMutationBranch(\n verifiedIdentity.login,\n );\n await assertPrototypeDirectory(this.#runtime, ref);\n const document = await readMetadataDocument(\n this.#runtime,\n ref,\n this.#themeRegistry,\n );\n const metadata = parsePrototypeMetadata(document, {\n themeRegistry: this.#themeRegistry,\n });\n if (metadata.authorLogin !== ref.login) {\n throw new PrototypeNotFoundError();\n }\n await commitPath(this.#repository, path, `deploy: ${ref.slug}`);\n await pushBranch(this.#repository, this.#runtime.remote, branch);\n\n const deployment = await this.#deploymentProvider!.deploy({\n applicationId: this.#applicationId!,\n login: verifiedIdentity.login,\n });\n const previewUrl = new URL(\n getPrototypeUrl(ref).slice(1),\n `${deployment.url.replace(/\\/+$/, \"\")}/`,\n ).toString();\n\n let warning: DeploymentWarningCode | undefined;\n try {\n await writeMetadataDocument(this.#runtime, ref, {\n ...document,\n previewUrl,\n updatedAt: new Date().toISOString(),\n });\n await commitPath(\n this.#repository,\n path,\n `deploy: ${ref.slug} (metadata)`,\n );\n await pushBranch(this.#repository, this.#runtime.remote, branch);\n } catch {\n warning = \"metadata-persistence-failed\";\n }\n\n if (warning === undefined) {\n try {\n await mergeIdentityBranchToPrimary(this.#repository, {\n branch,\n primaryBranch: this.#runtime.primaryBranch,\n remote: this.#runtime.remote,\n });\n } catch {\n warning = \"primary-merge-failed\";\n }\n }\n\n return {\n deployed: true,\n ref,\n previewUrl,\n deploymentId: deployment.deploymentId,\n readyState: deployment.readyState,\n warning,\n } satisfies DeploymentValue;\n },\n );\n });\n }\n\n async save(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<SavePrototypeValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const { ref, path } = await this.#ownedPrototype(\n verifiedIdentity,\n input,\n );\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n await this.#prepareMutationBranch(verifiedIdentity.login);\n await commitPath(this.#repository, path, `save: ${ref.slug}`);\n await pushBranch(\n this.#repository,\n this.#runtime.remote,\n verifiedIdentity.login,\n );\n return { saved: true } satisfies SavePrototypeValue;\n },\n );\n });\n }\n\n async discard(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DiscardPrototypeValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const { path } = await this.#ownedPrototype(\n verifiedIdentity,\n input,\n );\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n await this.#prepareMutationBranch(verifiedIdentity.login);\n await discardPath(this.#repository, path);\n return { discarded: true } satisfies DiscardPrototypeValue;\n },\n );\n });\n }\n\n async duplicate(\n identity: BonfireIdentity,\n input: DuplicatePrototypeInput,\n ): Promise<LifecycleResult<DuplicatePrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parseDuplicatePrototypeInput(input);\n if (!(await this.#catalog.getSummary(normalizedInput.source))) {\n throw new PrototypeDuplicateError(\"source-not-found\");\n }\n const ref = destinationRef(verifiedIdentity, normalizedInput.title);\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n if (await this.#catalog.getSummary(ref)) {\n throw new PrototypeDuplicateError(\"collision\");\n }\n\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n const prepared = await duplicatePrototype({\n paths: this.#runtime,\n source: normalizedInput.source,\n destination: ref,\n title: normalizedInput.title,\n description: normalizedInput.description,\n authorName: verifiedIdentity.name,\n themeRegistry: this.#themeRegistry,\n });\n await this.#prepareDestination(\n prepared.receipt,\n prepared.expectedFiles,\n {\n operation: \"duplicate\",\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n prototypePath: prepared.destination,\n ref,\n source: normalizedInput.source,\n identity: verifiedIdentity,\n title: normalizedInput.title,\n description: normalizedInput.description,\n },\n );\n if (branch === undefined) {\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: \"manual\",\n } satisfies DuplicatePrototypeValue;\n }\n\n await commitPath(\n this.#repository,\n repositoryRelativePath(this.#runtime, ref),\n `init: ${ref.slug}`,\n );\n const push = await pushBranch(\n this.#repository,\n this.#runtime.remote,\n branch,\n );\n\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: push.remoteStatus,\n } satisfies DuplicatePrototypeValue;\n },\n );\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"local-prototype-lifecycle.d.ts","sourceRoot":"","sources":["../../src/lifecycle/local-prototype-lifecycle.ts"],"names":[],"mappings":"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","sourcesContent":["import { constants } from \"node:fs\";\nimport { lstat, open, rename, rm, writeFile } from \"node:fs/promises\";\nimport { randomUUID } from \"node:crypto\";\nimport { join, relative, sep } from \"node:path\";\nimport {\n definePrototypeRef,\n getPrototypeUrl,\n LifecycleInputError,\n lifecycleFailure,\n lifecycleSuccess,\n parseCreatePrototypeInput,\n parseDuplicatePrototypeInput,\n parseEditPrototypeInput,\n parseBonfireIdentity,\n parsePrototypeCommandInput,\n parsePrototypeMetadata,\n slugifyPrototypeTitle,\n PrototypeRefError,\n type BranchPreparationValue,\n type CreatePrototypeInput,\n type CreatePrototypeValue,\n type DeletePrototypeValue,\n type DeploymentValue,\n type DeploymentWarningCode,\n type DirtyObservationValue,\n type DiscardPrototypeValue,\n type DuplicatePrototypeInput,\n type DuplicatePrototypeValue,\n type EditPrototypeInput,\n type EditPrototypeValue,\n parseGitModeSetting,\n type GitMode,\n type LifecycleErrorCode,\n type LifecycleResult,\n type BonfireIdentity,\n type PrototypeCommandInput,\n type SavePrototypeValue,\n type RepositorySummary,\n type ThemeRegistry,\n} from \"@ebay-dt/bonfire-core\";\nimport {\n commitPath,\n discardPath,\n GitRepositoryError,\n pushBranch,\n pushCommitToBranch,\n type GitRepository,\n} from \"../git-repository.js\";\nimport { duplicatePrototype, PrototypeDuplicateError } from \"../duplicate.js\";\nimport {\n PrototypeCatalog,\n type PrototypeCatalogLike,\n} from \"../prototype-catalog.js\";\nimport { PrototypeScaffoldError, scaffoldPrototype } from \"../scaffold.js\";\nimport {\n createPrototypeLifecycleRuntime,\n type PrototypeLifecycleRuntimeContext,\n} from \"./runtime-context.js\";\nimport type {\n PreparePrototypeFiles,\n PreparePrototypeFilesRequest,\n} from \"../prepare-prototype-files.js\";\nimport {\n cleanupOwnedDestination,\n validateOwnedDestination,\n type OwnedDestinationReceipt,\n} from \"../internal/owned-destination.js\";\nimport {\n RepositoryBusyError,\n withRepositoryOperation,\n} from \"./operation-gate.js\";\nimport {\n DeploymentProviderError,\n type DeploymentProvider,\n} from \"../deployment-provider.js\";\nimport {\n assertIdentityBranchReady,\n synchronizeIdentityBranch,\n} from \"./repository-synchronization.js\";\nimport { getRepositorySummary } from \"./repository-summary.js\";\nimport {\n DeploymentWorktreeError,\n preflightDeploymentSource,\n publishDeploymentSource,\n reportDeploymentIssue,\n} from \"./deployment-worktree.js\";\n\ntype LifecycleMethod<Input, Value> = (\n identity: BonfireIdentity,\n input: Input,\n) => Promise<LifecycleResult<Value>>;\n\nexport type PrototypeLifecycle = {\n /** Declares whether this lifecycle enforces the selected Git mode. */\n readonly gitMode?: GitMode;\n prepareBranch(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<BranchPreparationValue>>;\n create: LifecycleMethod<CreatePrototypeInput, CreatePrototypeValue>;\n duplicate: LifecycleMethod<DuplicatePrototypeInput, DuplicatePrototypeValue>;\n edit: LifecycleMethod<EditPrototypeInput, EditPrototypeValue>;\n delete: LifecycleMethod<PrototypeCommandInput, DeletePrototypeValue>;\n getDirtyState: LifecycleMethod<PrototypeCommandInput, DirtyObservationValue>;\n getRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>>;\n checkRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>>;\n save: LifecycleMethod<PrototypeCommandInput, SavePrototypeValue>;\n discard: LifecycleMethod<PrototypeCommandInput, DiscardPrototypeValue>;\n deploy: LifecycleMethod<PrototypeCommandInput, DeploymentValue>;\n retryPublish?: LifecycleMethod<PrototypeCommandInput, DeploymentValue>;\n};\n\nexport type LocalPrototypeLifecycleOptions = {\n repositoryRoot: string;\n prototypeRoot?: string;\n remote?: string;\n primaryBranch?: string;\n automaticBranchSwitch?: \"enabled\" | \"disabled\";\n gitMode?: GitMode;\n commitIdentity?: {\n name: string;\n email: string;\n };\n catalog?: PrototypeCatalogLike;\n preparePrototypeFiles?: PreparePrototypeFiles;\n deploymentProvider?: DeploymentProvider;\n applicationId?: string;\n themeRegistry?: ThemeRegistry;\n};\n\ntype InternalLocalPrototypeLifecycleOptions = LocalPrototypeLifecycleOptions & {\n runtime?: PrototypeLifecycleRuntimeContext;\n};\n\nfunction repositoryRelativePath(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n const absolutePath = join(runtime.prototypeRoot, ref.login, ref.slug);\n return relative(runtime.repositoryRoot, absolutePath).split(sep).join(\"/\");\n}\n\nfunction destinationRef(identity: BonfireIdentity, title: string) {\n const slug = slugifyPrototypeTitle(title);\n if (slug === null)\n throw new PrototypeRefError(\"title\", \"must produce a slug\");\n return definePrototypeRef({ login: identity.login, slug });\n}\n\nclass PrototypeNotFoundError extends Error {}\nclass PrototypeOwnershipError extends Error {}\nclass PrototypeMetadataFileError extends Error {}\nclass DeploymentUnavailableError extends Error {}\nclass DeploymentSourcePushError extends Error {}\nclass DeploymentNotAcceptedError extends Error {}\n\ntype PendingPublication = {\n identity: BonfireIdentity;\n input: PrototypeCommandInput;\n ref: { login: string; slug: string };\n path: string;\n previewUrl: string;\n deploymentId: string;\n sourceSha: string;\n readyState: DeploymentValue[\"readyState\"];\n synchronizationWarning: DeploymentWarningCode | undefined;\n};\n\nfunction metadataPath(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n return join(runtime.prototypeRoot, ref.login, ref.slug, \"metadata.json\");\n}\n\nasync function assertPrototypeDirectory(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n const paths = [\n runtime.prototypeRoot,\n join(runtime.prototypeRoot, ref.login),\n join(runtime.prototypeRoot, ref.login, ref.slug),\n ];\n\n try {\n for (const path of paths) {\n const stats = await lstat(path);\n if (stats.isSymbolicLink() || !stats.isDirectory()) {\n throw new PrototypeMetadataFileError();\n }\n }\n } catch (error) {\n if (error instanceof PrototypeMetadataFileError) throw error;\n throw new PrototypeMetadataFileError();\n }\n}\n\nasync function readMetadataDocument(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n themeRegistry?: ThemeRegistry,\n) {\n const path = metadataPath(runtime, ref);\n try {\n const file = await open(\n path,\n constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),\n );\n try {\n const source = await file.readFile(\"utf8\");\n const document: unknown = JSON.parse(source);\n parsePrototypeMetadata(document, { themeRegistry });\n if (\n typeof document !== \"object\" ||\n document === null ||\n Array.isArray(document)\n ) {\n throw new PrototypeMetadataFileError();\n }\n return { ...(document as Record<string, unknown>) };\n } finally {\n await file.close();\n }\n } catch (error) {\n if (error instanceof PrototypeMetadataFileError) throw error;\n throw new PrototypeMetadataFileError();\n }\n}\n\nasync function removePrototypeDirectory(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n) {\n await assertPrototypeDirectory(runtime, ref);\n try {\n await rm(join(runtime.prototypeRoot, ref.login, ref.slug), {\n force: false,\n recursive: true,\n });\n } catch {\n throw new PrototypeMetadataFileError();\n }\n}\n\nasync function writeMetadataDocument(\n runtime: PrototypeLifecycleRuntimeContext,\n ref: { login: string; slug: string },\n document: Record<string, unknown>,\n) {\n const path = metadataPath(runtime, ref);\n const temporaryPath = `${path}.${randomUUID()}.tmp`;\n\n try {\n const stats = await lstat(path);\n if (stats.isSymbolicLink() || !stats.isFile()) {\n throw new PrototypeMetadataFileError();\n }\n await writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}\\n`, {\n encoding: \"utf8\",\n flag: \"wx\",\n });\n await rename(temporaryPath, path);\n } catch (error) {\n if (error instanceof PrototypeMetadataFileError) throw error;\n throw new PrototypeMetadataFileError();\n } finally {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n }\n}\n\nfunction mapError(error: unknown): LifecycleErrorCode {\n if (error instanceof PrototypeNotFoundError) return \"not-found\";\n if (error instanceof PrototypeOwnershipError) return \"forbidden\";\n if (error instanceof PrototypeMetadataFileError) return \"filesystem-failure\";\n if (error instanceof RepositoryBusyError) return \"repository-busy\";\n if (\n error instanceof PrototypeRefError ||\n error instanceof LifecycleInputError\n ) {\n return \"invalid-input\";\n }\n if (error instanceof GitRepositoryError) {\n if (error.kind === \"timeout\") return \"timeout\";\n if (error.kind === \"conflict\") return \"working-tree-conflict\";\n if (error.kind === \"invalid-configuration\") return \"unexpected-failure\";\n return \"git-failure\";\n }\n if (error instanceof PrototypeScaffoldError) {\n if (error.kind === \"collision\") return \"already-exists\";\n if (error.kind === \"invalid-input\") return \"invalid-input\";\n return \"filesystem-failure\";\n }\n if (error instanceof PrototypeDuplicateError) {\n if (error.kind === \"source-not-found\") return \"not-found\";\n if (error.kind === \"collision\") return \"already-exists\";\n if (error.kind === \"invalid-source\") return \"invalid-input\";\n return \"filesystem-failure\";\n }\n if (error instanceof DeploymentUnavailableError) {\n return \"deployment-unavailable\";\n }\n if (\n error instanceof DeploymentSourcePushError ||\n error instanceof DeploymentNotAcceptedError ||\n error instanceof DeploymentWorktreeError\n ) {\n return \"deployment-failure\";\n }\n if (error instanceof DeploymentProviderError) {\n return error.kind === \"timeout\" ? \"timeout\" : \"deployment-failure\";\n }\n return \"unexpected-failure\";\n}\n\nfunction resultFrom<Value>(\n operation: () => Promise<Value>,\n): Promise<LifecycleResult<Value>> {\n return operation()\n .then(lifecycleSuccess)\n .catch((error: unknown) => lifecycleFailure(mapError(error)));\n}\n\nasync function diagnoseMetadataStep<Value>(\n step: \"metadata-read\" | \"metadata-write\" | \"metadata-commit\",\n operation: () => Promise<Value>,\n): Promise<Value> {\n try {\n return await operation();\n } catch (error) {\n reportDeploymentIssue(\"publication\", step, error);\n throw error;\n }\n}\n\nexport class LocalPrototypeLifecycle implements PrototypeLifecycle {\n readonly #runtime: PrototypeLifecycleRuntimeContext;\n readonly gitMode: GitMode;\n readonly #catalog: PrototypeCatalogLike;\n readonly #repository: GitRepository;\n readonly #preparePrototypeFiles: PreparePrototypeFiles;\n readonly #deploymentProvider?: DeploymentProvider;\n readonly #applicationId?: string;\n readonly #themeRegistry?: ThemeRegistry;\n #restartRequired = false;\n #pendingPublication?: PendingPublication;\n\n constructor(options: LocalPrototypeLifecycleOptions) {\n const internalOptions = options as InternalLocalPrototypeLifecycleOptions;\n const runtime =\n internalOptions.runtime ??\n createPrototypeLifecycleRuntime(internalOptions);\n this.#runtime = {\n ...runtime,\n gitMode: parseGitModeSetting(runtime.gitMode),\n };\n this.gitMode = this.#runtime.gitMode;\n this.#themeRegistry = options.themeRegistry;\n this.#catalog =\n internalOptions.catalog ??\n new PrototypeCatalog({\n ...this.#runtime,\n themeRegistry: this.#themeRegistry,\n });\n this.#repository = this.#runtime.gitRepository;\n this.#preparePrototypeFiles =\n internalOptions.preparePrototypeFiles ?? (() => undefined);\n this.#deploymentProvider = internalOptions.deploymentProvider;\n this.#applicationId = internalOptions.applicationId;\n if (\n this.#deploymentProvider &&\n (typeof this.#applicationId !== \"string\" ||\n this.#applicationId.length === 0)\n ) {\n throw new TypeError(\n \"applicationId must be provided when deploymentProvider is configured\",\n );\n }\n }\n\n async #verifiedIdentity(identity: BonfireIdentity) {\n return parseBonfireIdentity(identity);\n }\n\n async #currentBranch() {\n const branch = await this.#repository.currentBranch();\n if (branch === \"HEAD\" || branch.length === 0) {\n throw new GitRepositoryError(\"conflict\", \"current-branch\");\n }\n return branch;\n }\n\n async #ownedPrototype(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ) {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n if (normalizedInput.ref.login !== verifiedIdentity.login) {\n throw new PrototypeOwnershipError();\n }\n const summary = await this.#catalog.getSummary(normalizedInput.ref);\n if (!summary) {\n throw new PrototypeNotFoundError();\n }\n\n return {\n ref: normalizedInput.ref,\n path: repositoryRelativePath(this.#runtime, normalizedInput.ref),\n summary,\n };\n }\n\n async #prepareMutationBranch(login: string) {\n await assertIdentityBranchReady(this.#repository, login);\n return login;\n }\n\n async #prepareDestination(\n receipt: OwnedDestinationReceipt,\n expectedFiles: readonly string[],\n request: PreparePrototypeFilesRequest,\n ) {\n try {\n const frozenRequest = Object.freeze({\n ...request,\n ref: Object.freeze({ ...request.ref }),\n identity: Object.freeze({ ...request.identity }),\n ...(request.operation === \"duplicate\"\n ? { source: Object.freeze({ ...request.source }) }\n : {}),\n }) as PreparePrototypeFilesRequest;\n await this.#preparePrototypeFiles(frozenRequest);\n await validateOwnedDestination(\n receipt,\n expectedFiles,\n request.ref,\n this.#themeRegistry,\n );\n } catch {\n try {\n await cleanupOwnedDestination(receipt);\n } catch {\n // The failure remains a finite filesystem failure without retrying cleanup.\n }\n throw new PrototypeScaffoldError(\"filesystem\");\n }\n }\n\n async prepareBranch(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<BranchPreparationValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n if (\n this.#runtime.gitMode === \"manual\" ||\n this.#runtime.automaticBranchSwitch === \"disabled\"\n ) {\n await this.#currentBranch();\n return { status: \"disabled\" } satisfies BranchPreparationValue;\n }\n\n const synchronization = await synchronizeIdentityBranch(\n this.#repository,\n {\n identityBranch: verifiedIdentity.login,\n primaryBranch: this.#runtime.primaryBranch,\n remote: this.#runtime.remote,\n managedPaths: this.#runtime.managedRepositoryPaths,\n },\n );\n if (synchronization.status !== \"synchronized\") {\n return {\n status: synchronization.status,\n reason: synchronization.reason,\n ...(synchronization.affectedPaths.length > 0\n ? { affectedPaths: synchronization.affectedPaths }\n : {}),\n } satisfies BranchPreparationValue;\n }\n if (\n synchronization.affectedPaths.some((path) =>\n this.#runtime.managedRepositoryPaths.includes(path),\n )\n ) {\n this.#restartRequired = true;\n }\n return {\n status: \"ready\",\n ...(synchronization.affectedPaths.length > 0\n ? { affectedPaths: synchronization.affectedPaths }\n : {}),\n } satisfies BranchPreparationValue;\n },\n );\n });\n }\n\n async create(\n identity: BonfireIdentity,\n input: CreatePrototypeInput,\n ): Promise<LifecycleResult<CreatePrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parseCreatePrototypeInput(\n input,\n this.#themeRegistry,\n );\n const ref = destinationRef(verifiedIdentity, normalizedInput.title);\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n if (await this.#catalog.getSummary(ref)) {\n throw new PrototypeScaffoldError(\"collision\");\n }\n\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n const prepared = await scaffoldPrototype({\n paths: this.#runtime,\n ref,\n title: normalizedInput.title,\n description: normalizedInput.description,\n authorName: verifiedIdentity.name,\n theme: normalizedInput.theme,\n themeRegistry: this.#themeRegistry,\n });\n await this.#prepareDestination(\n prepared.receipt,\n prepared.expectedFiles,\n {\n operation: \"create\",\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n prototypePath: prepared.destination,\n ref,\n identity: verifiedIdentity,\n title: normalizedInput.title,\n description: normalizedInput.description,\n },\n );\n if (branch === undefined) {\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: \"manual\",\n } satisfies CreatePrototypeValue;\n }\n\n await commitPath(\n this.#repository,\n repositoryRelativePath(this.#runtime, ref),\n `init: ${ref.slug}`,\n );\n const push = await pushBranch(\n this.#repository,\n this.#runtime.remote,\n branch,\n );\n\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: push.remoteStatus,\n } satisfies CreatePrototypeValue;\n },\n );\n });\n }\n\n async edit(\n identity: BonfireIdentity,\n input: EditPrototypeInput,\n ): Promise<LifecycleResult<EditPrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parseEditPrototypeInput(\n input,\n this.#themeRegistry,\n );\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n const { ref, path } = await this.#ownedPrototype(verifiedIdentity, {\n ref: normalizedInput.ref,\n });\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n await assertPrototypeDirectory(this.#runtime, ref);\n const document = await readMetadataDocument(\n this.#runtime,\n ref,\n this.#themeRegistry,\n );\n const metadata = parsePrototypeMetadata(document, {\n themeRegistry: this.#themeRegistry,\n });\n if (metadata.authorLogin !== ref.login) {\n throw new PrototypeNotFoundError();\n }\n\n await writeMetadataDocument(this.#runtime, ref, {\n ...document,\n title: normalizedInput.title,\n description: normalizedInput.description,\n updatedAt: new Date().toISOString(),\n ...(normalizedInput.theme === undefined\n ? {}\n : { theme: normalizedInput.theme }),\n });\n if (branch !== undefined) {\n await commitPath(this.#repository, path, `edit: ${ref.slug}`);\n await pushBranch(this.#repository, this.#runtime.remote, branch);\n }\n\n return { edited: true, ref } satisfies EditPrototypeValue;\n },\n );\n });\n }\n\n async delete(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DeletePrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n const { ref, path } = await this.#ownedPrototype(\n verifiedIdentity,\n normalizedInput,\n );\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n await removePrototypeDirectory(this.#runtime, ref);\n if (branch !== undefined) {\n await commitPath(this.#repository, path, `delete: ${ref.slug}`);\n await pushBranch(this.#repository, this.#runtime.remote, branch);\n }\n\n return { deleted: true, ref } satisfies DeletePrototypeValue;\n },\n );\n });\n }\n\n async getDirtyState(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DirtyObservationValue>> {\n return resultFrom(async () => {\n const { path, summary } = await this.#ownedPrototype(identity, input);\n const status = await this.#repository.status([path]);\n return {\n status: status.files.length > 0 ? \"dirty\" : \"clean\",\n ...(summary.previewUrl !== undefined\n ? { previewUrl: summary.previewUrl }\n : {}),\n } satisfies DirtyObservationValue;\n });\n }\n\n async getRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n return getRepositorySummary({\n repository: this.#repository,\n catalog: this.#catalog,\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n identity: verifiedIdentity,\n mode: this.#runtime.gitMode,\n remote: this.#runtime.remote,\n primaryBranch: this.#runtime.primaryBranch,\n restartRequired: this.#restartRequired,\n });\n });\n }\n\n async checkRepositorySummary(\n identity: BonfireIdentity,\n ): Promise<LifecycleResult<RepositorySummary>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n return getRepositorySummary({\n repository: this.#repository,\n catalog: this.#catalog,\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n identity: verifiedIdentity,\n mode: this.#runtime.gitMode,\n remote: this.#runtime.remote,\n primaryBranch: this.#runtime.primaryBranch,\n restartRequired: this.#restartRequired,\n refresh: true,\n });\n });\n }\n\n async #headCommit() {\n const commit = (await this.#repository.raw([\"rev-parse\", \"HEAD\"])).trim();\n if (!/^[0-9a-f]{40,64}$/iu.test(commit)) {\n throw new GitRepositoryError(\"malformed-output\", \"head-commit\");\n }\n return commit.toLowerCase();\n }\n\n async #prepareDeploymentBranch(login: string) {\n if (this.#runtime.automaticBranchSwitch === \"disabled\") {\n await assertIdentityBranchReady(this.#repository, login);\n return \"updates-not-synchronized\" as const;\n }\n\n const synchronization = await synchronizeIdentityBranch(this.#repository, {\n identityBranch: login,\n primaryBranch: this.#runtime.primaryBranch,\n remote: this.#runtime.remote,\n managedPaths: this.#runtime.managedRepositoryPaths,\n });\n if (synchronization.status === \"unsafe\") {\n throw new GitRepositoryError(\"conflict\", \"deployment-preparation\");\n }\n await assertIdentityBranchReady(this.#repository, login);\n return synchronization.status === \"deferred\"\n ? (\"updates-not-synchronized\" as const)\n : undefined;\n }\n\n #workspaceRuntime(\n workspacePath: string,\n workspaceRepository: GitRepository,\n ): PrototypeLifecycleRuntimeContext {\n return {\n ...this.#runtime,\n repositoryRoot: workspacePath,\n repositoryRealPath: workspacePath,\n prototypeRoot: join(\n workspacePath,\n relative(this.#runtime.repositoryRoot, this.#runtime.prototypeRoot),\n ),\n gitRepository: workspaceRepository,\n };\n }\n\n async #publishDeploymentMetadata(pending: PendingPublication) {\n return publishDeploymentSource(\n this.#repository,\n {\n remote: this.#runtime.remote,\n primaryBranch: this.#runtime.primaryBranch,\n sourceSha: pending.sourceSha,\n commitIdentity: this.#runtime.commitIdentity,\n },\n async ({ path, repository }) => {\n const workspaceRuntime = this.#workspaceRuntime(path, repository);\n const mergedDocument = await diagnoseMetadataStep(\"metadata-read\", () =>\n readMetadataDocument(\n workspaceRuntime,\n pending.ref,\n this.#themeRegistry,\n ),\n );\n await diagnoseMetadataStep(\"metadata-write\", () =>\n writeMetadataDocument(workspaceRuntime, pending.ref, {\n ...mergedDocument,\n previewUrl: pending.previewUrl,\n updatedAt: new Date().toISOString(),\n }),\n );\n await diagnoseMetadataStep(\"metadata-commit\", () =>\n commitPath(\n repository,\n pending.path,\n `deploy: ${pending.ref.slug} (metadata)`,\n ),\n );\n },\n );\n }\n\n async #deploymentValue(\n pending: PendingPublication,\n publicationWarnings: readonly DeploymentWarningCode[] = [],\n ): Promise<DeploymentValue> {\n const warnings = [\n ...(pending.synchronizationWarning === undefined\n ? []\n : [pending.synchronizationWarning]),\n ...publicationWarnings,\n ];\n return {\n deployed: true,\n ref: pending.ref,\n previewUrl: pending.previewUrl,\n deploymentId: pending.deploymentId,\n sourceSha: pending.sourceSha,\n readyState: pending.readyState,\n warning: publicationWarnings[0] ?? pending.synchronizationWarning,\n ...(warnings.length === 0 ? {} : { warnings }),\n };\n }\n\n async deploy(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DeploymentValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n if (!this.#deploymentProvider) {\n throw new DeploymentUnavailableError();\n }\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n const { ref, path } = await this.#ownedPrototype(\n verifiedIdentity,\n normalizedInput,\n );\n const synchronizationWarning = await this.#prepareDeploymentBranch(\n verifiedIdentity.login,\n );\n await assertPrototypeDirectory(this.#runtime, ref);\n const document = await readMetadataDocument(\n this.#runtime,\n ref,\n this.#themeRegistry,\n );\n const metadata = parsePrototypeMetadata(document, {\n themeRegistry: this.#themeRegistry,\n });\n if (metadata.authorLogin !== ref.login) {\n throw new PrototypeNotFoundError();\n }\n\n await commitPath(this.#repository, path, `deploy: ${ref.slug}`);\n const sourceSha = await this.#headCommit();\n const sourcePush = await pushCommitToBranch(\n this.#repository,\n this.#runtime.remote,\n sourceSha,\n verifiedIdentity.login,\n );\n if (sourcePush.remoteStatus !== \"pushed\") {\n throw new DeploymentSourcePushError();\n }\n\n await preflightDeploymentSource(this.#repository, {\n remote: this.#runtime.remote,\n primaryBranch: this.#runtime.primaryBranch,\n sourceSha,\n commitIdentity: this.#runtime.commitIdentity,\n });\n\n const deployment = await this.#deploymentProvider!.deploy({\n applicationId: this.#applicationId!,\n login: verifiedIdentity.login,\n });\n if (\n deployment.commitSha.toLowerCase() !== sourceSha ||\n ![\"READY\", \"QUEUED\", \"INITIALIZING\", \"BUILDING\"].includes(\n deployment.readyState,\n )\n ) {\n throw new DeploymentNotAcceptedError();\n }\n const previewUrl = new URL(\n getPrototypeUrl(ref).slice(1),\n `${deployment.url.replace(/\\/+$/, \"\")}/`,\n ).toString();\n const pending: PendingPublication = {\n identity: verifiedIdentity,\n input: normalizedInput,\n ref,\n path,\n previewUrl,\n deploymentId: deployment.deploymentId,\n sourceSha,\n readyState: deployment.readyState,\n synchronizationWarning,\n };\n\n try {\n const publication = await this.#publishDeploymentMetadata(pending);\n this.#pendingPublication = undefined;\n return this.#deploymentValue(\n pending,\n publication.cleanupFailed ? [\"publication-cleanup-failed\"] : [],\n );\n } catch (error) {\n // Keep the accepted deployment retryable for every publication\n // failure, including metadata preparation failures.\n this.#pendingPublication = pending;\n return this.#deploymentValue(pending, [\n error instanceof DeploymentWorktreeError &&\n error.kind !== \"cleanup-failure\"\n ? \"primary-merge-failed\"\n : \"metadata-persistence-failed\",\n ...(error instanceof DeploymentWorktreeError &&\n error.cleanupFailed\n ? [\"publication-cleanup-failed\" as const]\n : []),\n ]);\n }\n },\n );\n });\n }\n\n async retryPublish(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DeploymentValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parsePrototypeCommandInput(input);\n const pending = this.#pendingPublication;\n if (\n pending === undefined ||\n pending.identity.login !== verifiedIdentity.login ||\n pending.ref.login !== normalizedInput.ref.login ||\n pending.ref.slug !== normalizedInput.ref.slug\n ) {\n throw new DeploymentUnavailableError();\n }\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n await assertIdentityBranchReady(\n this.#repository,\n verifiedIdentity.login,\n );\n const publication = await this.#publishDeploymentMetadata(pending);\n this.#pendingPublication = undefined;\n return this.#deploymentValue(\n pending,\n publication.cleanupFailed ? [\"publication-cleanup-failed\"] : [],\n );\n },\n );\n });\n }\n\n async save(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<SavePrototypeValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const { ref, path } = await this.#ownedPrototype(verifiedIdentity, input);\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n await this.#prepareMutationBranch(verifiedIdentity.login);\n await commitPath(this.#repository, path, `save: ${ref.slug}`);\n await pushBranch(\n this.#repository,\n this.#runtime.remote,\n verifiedIdentity.login,\n );\n return { saved: true } satisfies SavePrototypeValue;\n },\n );\n });\n }\n\n async discard(\n identity: BonfireIdentity,\n input: PrototypeCommandInput,\n ): Promise<LifecycleResult<DiscardPrototypeValue>> {\n if (this.#runtime.gitMode === \"manual\") {\n return lifecycleFailure(\"forbidden\");\n }\n\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const { path } = await this.#ownedPrototype(verifiedIdentity, input);\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n await discardPath(this.#repository, path);\n return { discarded: true } satisfies DiscardPrototypeValue;\n },\n );\n });\n }\n\n async duplicate(\n identity: BonfireIdentity,\n input: DuplicatePrototypeInput,\n ): Promise<LifecycleResult<DuplicatePrototypeValue>> {\n return resultFrom(async () => {\n const verifiedIdentity = await this.#verifiedIdentity(identity);\n const normalizedInput = parseDuplicatePrototypeInput(input);\n if (!(await this.#catalog.getSummary(normalizedInput.source))) {\n throw new PrototypeDuplicateError(\"source-not-found\");\n }\n const ref = destinationRef(verifiedIdentity, normalizedInput.title);\n\n return withRepositoryOperation(\n this.#runtime.repositoryRealPath,\n async () => {\n if (await this.#catalog.getSummary(ref)) {\n throw new PrototypeDuplicateError(\"collision\");\n }\n\n const branch =\n this.#runtime.gitMode === \"managed\"\n ? await this.#prepareMutationBranch(verifiedIdentity.login)\n : undefined;\n const prepared = await duplicatePrototype({\n paths: this.#runtime,\n source: normalizedInput.source,\n destination: ref,\n title: normalizedInput.title,\n description: normalizedInput.description,\n authorName: verifiedIdentity.name,\n themeRegistry: this.#themeRegistry,\n });\n await this.#prepareDestination(\n prepared.receipt,\n prepared.expectedFiles,\n {\n operation: \"duplicate\",\n repositoryRoot: this.#runtime.repositoryRoot,\n prototypeRoot: this.#runtime.prototypeRoot,\n prototypePath: prepared.destination,\n ref,\n source: normalizedInput.source,\n identity: verifiedIdentity,\n title: normalizedInput.title,\n description: normalizedInput.description,\n },\n );\n if (branch === undefined) {\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: \"manual\",\n } satisfies DuplicatePrototypeValue;\n }\n\n await commitPath(\n this.#repository,\n repositoryRelativePath(this.#runtime, ref),\n `init: ${ref.slug}`,\n );\n const push = await pushBranch(\n this.#repository,\n this.#runtime.remote,\n branch,\n );\n\n return {\n ref,\n url: getPrototypeUrl(ref),\n remoteStatus: push.remoteStatus,\n } satisfies DuplicatePrototypeValue;\n },\n );\n });\n }\n}\n"]}
|
|
@@ -3,7 +3,7 @@ import { lstat, open, rename, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { join, relative, sep } from "node:path";
|
|
5
5
|
import { definePrototypeRef, getPrototypeUrl, LifecycleInputError, lifecycleFailure, lifecycleSuccess, parseCreatePrototypeInput, parseDuplicatePrototypeInput, parseEditPrototypeInput, parseBonfireIdentity, parsePrototypeCommandInput, parsePrototypeMetadata, slugifyPrototypeTitle, PrototypeRefError, parseGitModeSetting, } from "@ebay-dt/bonfire-core";
|
|
6
|
-
import { commitPath, discardPath, GitRepositoryError, pushBranch, } from "../git-repository.js";
|
|
6
|
+
import { commitPath, discardPath, GitRepositoryError, pushBranch, pushCommitToBranch, } from "../git-repository.js";
|
|
7
7
|
import { duplicatePrototype, PrototypeDuplicateError } from "../duplicate.js";
|
|
8
8
|
import { PrototypeCatalog, } from "../prototype-catalog.js";
|
|
9
9
|
import { PrototypeScaffoldError, scaffoldPrototype } from "../scaffold.js";
|
|
@@ -13,6 +13,7 @@ import { RepositoryBusyError, withRepositoryOperation, } from "./operation-gate.
|
|
|
13
13
|
import { DeploymentProviderError, } from "../deployment-provider.js";
|
|
14
14
|
import { assertIdentityBranchReady, synchronizeIdentityBranch, } from "./repository-synchronization.js";
|
|
15
15
|
import { getRepositorySummary } from "./repository-summary.js";
|
|
16
|
+
import { DeploymentWorktreeError, preflightDeploymentSource, publishDeploymentSource, reportDeploymentIssue, } from "./deployment-worktree.js";
|
|
16
17
|
function repositoryRelativePath(runtime, ref) {
|
|
17
18
|
const absolutePath = join(runtime.prototypeRoot, ref.login, ref.slug);
|
|
18
19
|
return relative(runtime.repositoryRoot, absolutePath).split(sep).join("/");
|
|
@@ -31,6 +32,10 @@ class PrototypeMetadataFileError extends Error {
|
|
|
31
32
|
}
|
|
32
33
|
class DeploymentUnavailableError extends Error {
|
|
33
34
|
}
|
|
35
|
+
class DeploymentSourcePushError extends Error {
|
|
36
|
+
}
|
|
37
|
+
class DeploymentNotAcceptedError extends Error {
|
|
38
|
+
}
|
|
34
39
|
function metadataPath(runtime, ref) {
|
|
35
40
|
return join(runtime.prototypeRoot, ref.login, ref.slug, "metadata.json");
|
|
36
41
|
}
|
|
@@ -114,26 +119,6 @@ async function writeMetadataDocument(runtime, ref, document) {
|
|
|
114
119
|
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
|
-
async function mergeIdentityBranchToPrimary(repository, options) {
|
|
118
|
-
if (options.branch === options.primaryBranch)
|
|
119
|
-
return;
|
|
120
|
-
const hasRemote = (await repository.remotes()).some((remote) => remote.name === options.remote);
|
|
121
|
-
if (!hasRemote)
|
|
122
|
-
return;
|
|
123
|
-
await repository.checkout([options.primaryBranch]);
|
|
124
|
-
try {
|
|
125
|
-
await repository.merge([
|
|
126
|
-
options.branch,
|
|
127
|
-
"--no-ff",
|
|
128
|
-
"-m",
|
|
129
|
-
`merge: ${options.branch}`,
|
|
130
|
-
]);
|
|
131
|
-
await repository.push(options.remote, options.primaryBranch);
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
await repository.checkout([options.branch]);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
122
|
function mapError(error) {
|
|
138
123
|
if (error instanceof PrototypeNotFoundError)
|
|
139
124
|
return "not-found";
|
|
@@ -175,6 +160,11 @@ function mapError(error) {
|
|
|
175
160
|
if (error instanceof DeploymentUnavailableError) {
|
|
176
161
|
return "deployment-unavailable";
|
|
177
162
|
}
|
|
163
|
+
if (error instanceof DeploymentSourcePushError ||
|
|
164
|
+
error instanceof DeploymentNotAcceptedError ||
|
|
165
|
+
error instanceof DeploymentWorktreeError) {
|
|
166
|
+
return "deployment-failure";
|
|
167
|
+
}
|
|
178
168
|
if (error instanceof DeploymentProviderError) {
|
|
179
169
|
return error.kind === "timeout" ? "timeout" : "deployment-failure";
|
|
180
170
|
}
|
|
@@ -185,6 +175,15 @@ function resultFrom(operation) {
|
|
|
185
175
|
.then(lifecycleSuccess)
|
|
186
176
|
.catch((error) => lifecycleFailure(mapError(error)));
|
|
187
177
|
}
|
|
178
|
+
async function diagnoseMetadataStep(step, operation) {
|
|
179
|
+
try {
|
|
180
|
+
return await operation();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
reportDeploymentIssue("publication", step, error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
188
187
|
export class LocalPrototypeLifecycle {
|
|
189
188
|
#runtime;
|
|
190
189
|
gitMode;
|
|
@@ -194,9 +193,12 @@ export class LocalPrototypeLifecycle {
|
|
|
194
193
|
#deploymentProvider;
|
|
195
194
|
#applicationId;
|
|
196
195
|
#themeRegistry;
|
|
196
|
+
#restartRequired = false;
|
|
197
|
+
#pendingPublication;
|
|
197
198
|
constructor(options) {
|
|
198
199
|
const internalOptions = options;
|
|
199
|
-
const runtime = internalOptions.runtime ??
|
|
200
|
+
const runtime = internalOptions.runtime ??
|
|
201
|
+
createPrototypeLifecycleRuntime(internalOptions);
|
|
200
202
|
this.#runtime = {
|
|
201
203
|
...runtime,
|
|
202
204
|
gitMode: parseGitModeSetting(runtime.gitMode),
|
|
@@ -286,6 +288,7 @@ export class LocalPrototypeLifecycle {
|
|
|
286
288
|
identityBranch: verifiedIdentity.login,
|
|
287
289
|
primaryBranch: this.#runtime.primaryBranch,
|
|
288
290
|
remote: this.#runtime.remote,
|
|
291
|
+
managedPaths: this.#runtime.managedRepositoryPaths,
|
|
289
292
|
});
|
|
290
293
|
if (synchronization.status !== "synchronized") {
|
|
291
294
|
return {
|
|
@@ -296,7 +299,15 @@ export class LocalPrototypeLifecycle {
|
|
|
296
299
|
: {}),
|
|
297
300
|
};
|
|
298
301
|
}
|
|
299
|
-
|
|
302
|
+
if (synchronization.affectedPaths.some((path) => this.#runtime.managedRepositoryPaths.includes(path))) {
|
|
303
|
+
this.#restartRequired = true;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
status: "ready",
|
|
307
|
+
...(synchronization.affectedPaths.length > 0
|
|
308
|
+
? { affectedPaths: synchronization.affectedPaths }
|
|
309
|
+
: {}),
|
|
310
|
+
};
|
|
300
311
|
});
|
|
301
312
|
});
|
|
302
313
|
}
|
|
@@ -426,6 +437,7 @@ export class LocalPrototypeLifecycle {
|
|
|
426
437
|
mode: this.#runtime.gitMode,
|
|
427
438
|
remote: this.#runtime.remote,
|
|
428
439
|
primaryBranch: this.#runtime.primaryBranch,
|
|
440
|
+
restartRequired: this.#restartRequired,
|
|
429
441
|
});
|
|
430
442
|
});
|
|
431
443
|
}
|
|
@@ -441,10 +453,81 @@ export class LocalPrototypeLifecycle {
|
|
|
441
453
|
mode: this.#runtime.gitMode,
|
|
442
454
|
remote: this.#runtime.remote,
|
|
443
455
|
primaryBranch: this.#runtime.primaryBranch,
|
|
456
|
+
restartRequired: this.#restartRequired,
|
|
444
457
|
refresh: true,
|
|
445
458
|
});
|
|
446
459
|
});
|
|
447
460
|
}
|
|
461
|
+
async #headCommit() {
|
|
462
|
+
const commit = (await this.#repository.raw(["rev-parse", "HEAD"])).trim();
|
|
463
|
+
if (!/^[0-9a-f]{40,64}$/iu.test(commit)) {
|
|
464
|
+
throw new GitRepositoryError("malformed-output", "head-commit");
|
|
465
|
+
}
|
|
466
|
+
return commit.toLowerCase();
|
|
467
|
+
}
|
|
468
|
+
async #prepareDeploymentBranch(login) {
|
|
469
|
+
if (this.#runtime.automaticBranchSwitch === "disabled") {
|
|
470
|
+
await assertIdentityBranchReady(this.#repository, login);
|
|
471
|
+
return "updates-not-synchronized";
|
|
472
|
+
}
|
|
473
|
+
const synchronization = await synchronizeIdentityBranch(this.#repository, {
|
|
474
|
+
identityBranch: login,
|
|
475
|
+
primaryBranch: this.#runtime.primaryBranch,
|
|
476
|
+
remote: this.#runtime.remote,
|
|
477
|
+
managedPaths: this.#runtime.managedRepositoryPaths,
|
|
478
|
+
});
|
|
479
|
+
if (synchronization.status === "unsafe") {
|
|
480
|
+
throw new GitRepositoryError("conflict", "deployment-preparation");
|
|
481
|
+
}
|
|
482
|
+
await assertIdentityBranchReady(this.#repository, login);
|
|
483
|
+
return synchronization.status === "deferred"
|
|
484
|
+
? "updates-not-synchronized"
|
|
485
|
+
: undefined;
|
|
486
|
+
}
|
|
487
|
+
#workspaceRuntime(workspacePath, workspaceRepository) {
|
|
488
|
+
return {
|
|
489
|
+
...this.#runtime,
|
|
490
|
+
repositoryRoot: workspacePath,
|
|
491
|
+
repositoryRealPath: workspacePath,
|
|
492
|
+
prototypeRoot: join(workspacePath, relative(this.#runtime.repositoryRoot, this.#runtime.prototypeRoot)),
|
|
493
|
+
gitRepository: workspaceRepository,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async #publishDeploymentMetadata(pending) {
|
|
497
|
+
return publishDeploymentSource(this.#repository, {
|
|
498
|
+
remote: this.#runtime.remote,
|
|
499
|
+
primaryBranch: this.#runtime.primaryBranch,
|
|
500
|
+
sourceSha: pending.sourceSha,
|
|
501
|
+
commitIdentity: this.#runtime.commitIdentity,
|
|
502
|
+
}, async ({ path, repository }) => {
|
|
503
|
+
const workspaceRuntime = this.#workspaceRuntime(path, repository);
|
|
504
|
+
const mergedDocument = await diagnoseMetadataStep("metadata-read", () => readMetadataDocument(workspaceRuntime, pending.ref, this.#themeRegistry));
|
|
505
|
+
await diagnoseMetadataStep("metadata-write", () => writeMetadataDocument(workspaceRuntime, pending.ref, {
|
|
506
|
+
...mergedDocument,
|
|
507
|
+
previewUrl: pending.previewUrl,
|
|
508
|
+
updatedAt: new Date().toISOString(),
|
|
509
|
+
}));
|
|
510
|
+
await diagnoseMetadataStep("metadata-commit", () => commitPath(repository, pending.path, `deploy: ${pending.ref.slug} (metadata)`));
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async #deploymentValue(pending, publicationWarnings = []) {
|
|
514
|
+
const warnings = [
|
|
515
|
+
...(pending.synchronizationWarning === undefined
|
|
516
|
+
? []
|
|
517
|
+
: [pending.synchronizationWarning]),
|
|
518
|
+
...publicationWarnings,
|
|
519
|
+
];
|
|
520
|
+
return {
|
|
521
|
+
deployed: true,
|
|
522
|
+
ref: pending.ref,
|
|
523
|
+
previewUrl: pending.previewUrl,
|
|
524
|
+
deploymentId: pending.deploymentId,
|
|
525
|
+
sourceSha: pending.sourceSha,
|
|
526
|
+
readyState: pending.readyState,
|
|
527
|
+
warning: publicationWarnings[0] ?? pending.synchronizationWarning,
|
|
528
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
448
531
|
async deploy(identity, input) {
|
|
449
532
|
if (this.#runtime.gitMode === "manual") {
|
|
450
533
|
return lifecycleFailure("forbidden");
|
|
@@ -457,7 +540,7 @@ export class LocalPrototypeLifecycle {
|
|
|
457
540
|
}
|
|
458
541
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
459
542
|
const { ref, path } = await this.#ownedPrototype(verifiedIdentity, normalizedInput);
|
|
460
|
-
const
|
|
543
|
+
const synchronizationWarning = await this.#prepareDeploymentBranch(verifiedIdentity.login);
|
|
461
544
|
await assertPrototypeDirectory(this.#runtime, ref);
|
|
462
545
|
const document = await readMetadataDocument(this.#runtime, ref, this.#themeRegistry);
|
|
463
546
|
const metadata = parsePrototypeMetadata(document, {
|
|
@@ -467,45 +550,79 @@ export class LocalPrototypeLifecycle {
|
|
|
467
550
|
throw new PrototypeNotFoundError();
|
|
468
551
|
}
|
|
469
552
|
await commitPath(this.#repository, path, `deploy: ${ref.slug}`);
|
|
470
|
-
await
|
|
553
|
+
const sourceSha = await this.#headCommit();
|
|
554
|
+
const sourcePush = await pushCommitToBranch(this.#repository, this.#runtime.remote, sourceSha, verifiedIdentity.login);
|
|
555
|
+
if (sourcePush.remoteStatus !== "pushed") {
|
|
556
|
+
throw new DeploymentSourcePushError();
|
|
557
|
+
}
|
|
558
|
+
await preflightDeploymentSource(this.#repository, {
|
|
559
|
+
remote: this.#runtime.remote,
|
|
560
|
+
primaryBranch: this.#runtime.primaryBranch,
|
|
561
|
+
sourceSha,
|
|
562
|
+
commitIdentity: this.#runtime.commitIdentity,
|
|
563
|
+
});
|
|
471
564
|
const deployment = await this.#deploymentProvider.deploy({
|
|
472
565
|
applicationId: this.#applicationId,
|
|
473
566
|
login: verifiedIdentity.login,
|
|
474
567
|
});
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await writeMetadataDocument(this.#runtime, ref, {
|
|
479
|
-
...document,
|
|
480
|
-
previewUrl,
|
|
481
|
-
updatedAt: new Date().toISOString(),
|
|
482
|
-
});
|
|
483
|
-
await commitPath(this.#repository, path, `deploy: ${ref.slug} (metadata)`);
|
|
484
|
-
await pushBranch(this.#repository, this.#runtime.remote, branch);
|
|
568
|
+
if (deployment.commitSha.toLowerCase() !== sourceSha ||
|
|
569
|
+
!["READY", "QUEUED", "INITIALIZING", "BUILDING"].includes(deployment.readyState)) {
|
|
570
|
+
throw new DeploymentNotAcceptedError();
|
|
485
571
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
try {
|
|
491
|
-
await mergeIdentityBranchToPrimary(this.#repository, {
|
|
492
|
-
branch,
|
|
493
|
-
primaryBranch: this.#runtime.primaryBranch,
|
|
494
|
-
remote: this.#runtime.remote,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
catch {
|
|
498
|
-
warning = "primary-merge-failed";
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return {
|
|
502
|
-
deployed: true,
|
|
572
|
+
const previewUrl = new URL(getPrototypeUrl(ref).slice(1), `${deployment.url.replace(/\/+$/, "")}/`).toString();
|
|
573
|
+
const pending = {
|
|
574
|
+
identity: verifiedIdentity,
|
|
575
|
+
input: normalizedInput,
|
|
503
576
|
ref,
|
|
577
|
+
path,
|
|
504
578
|
previewUrl,
|
|
505
579
|
deploymentId: deployment.deploymentId,
|
|
580
|
+
sourceSha,
|
|
506
581
|
readyState: deployment.readyState,
|
|
507
|
-
|
|
582
|
+
synchronizationWarning,
|
|
508
583
|
};
|
|
584
|
+
try {
|
|
585
|
+
const publication = await this.#publishDeploymentMetadata(pending);
|
|
586
|
+
this.#pendingPublication = undefined;
|
|
587
|
+
return this.#deploymentValue(pending, publication.cleanupFailed ? ["publication-cleanup-failed"] : []);
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
// Keep the accepted deployment retryable for every publication
|
|
591
|
+
// failure, including metadata preparation failures.
|
|
592
|
+
this.#pendingPublication = pending;
|
|
593
|
+
return this.#deploymentValue(pending, [
|
|
594
|
+
error instanceof DeploymentWorktreeError &&
|
|
595
|
+
error.kind !== "cleanup-failure"
|
|
596
|
+
? "primary-merge-failed"
|
|
597
|
+
: "metadata-persistence-failed",
|
|
598
|
+
...(error instanceof DeploymentWorktreeError &&
|
|
599
|
+
error.cleanupFailed
|
|
600
|
+
? ["publication-cleanup-failed"]
|
|
601
|
+
: []),
|
|
602
|
+
]);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async retryPublish(identity, input) {
|
|
608
|
+
if (this.#runtime.gitMode === "manual") {
|
|
609
|
+
return lifecycleFailure("forbidden");
|
|
610
|
+
}
|
|
611
|
+
return resultFrom(async () => {
|
|
612
|
+
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
613
|
+
const normalizedInput = parsePrototypeCommandInput(input);
|
|
614
|
+
const pending = this.#pendingPublication;
|
|
615
|
+
if (pending === undefined ||
|
|
616
|
+
pending.identity.login !== verifiedIdentity.login ||
|
|
617
|
+
pending.ref.login !== normalizedInput.ref.login ||
|
|
618
|
+
pending.ref.slug !== normalizedInput.ref.slug) {
|
|
619
|
+
throw new DeploymentUnavailableError();
|
|
620
|
+
}
|
|
621
|
+
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
622
|
+
await assertIdentityBranchReady(this.#repository, verifiedIdentity.login);
|
|
623
|
+
const publication = await this.#publishDeploymentMetadata(pending);
|
|
624
|
+
this.#pendingPublication = undefined;
|
|
625
|
+
return this.#deploymentValue(pending, publication.cleanupFailed ? ["publication-cleanup-failed"] : []);
|
|
509
626
|
});
|
|
510
627
|
});
|
|
511
628
|
}
|
|
@@ -532,7 +649,6 @@ export class LocalPrototypeLifecycle {
|
|
|
532
649
|
const verifiedIdentity = await this.#verifiedIdentity(identity);
|
|
533
650
|
const { path } = await this.#ownedPrototype(verifiedIdentity, input);
|
|
534
651
|
return withRepositoryOperation(this.#runtime.repositoryRealPath, async () => {
|
|
535
|
-
await this.#prepareMutationBranch(verifiedIdentity.login);
|
|
536
652
|
await discardPath(this.#repository, path);
|
|
537
653
|
return { discarded: true };
|
|
538
654
|
});
|