diff --git a/components/dashboard/src/contexts/FeatureFlagContext.tsx b/components/dashboard/src/contexts/FeatureFlagContext.tsx index df57ac67ba7bbf..f6dc1acd4fade2 100644 --- a/components/dashboard/src/contexts/FeatureFlagContext.tsx +++ b/components/dashboard/src/contexts/FeatureFlagContext.tsx @@ -18,9 +18,11 @@ interface FeatureFlagConfig { const FeatureFlagContext = createContext<{ showPersistentVolumeClaimUI: boolean; showUsageView: boolean; + showUseLastSuccessfulPrebuild: boolean; }>({ showPersistentVolumeClaimUI: false, showUsageView: false, + showUseLastSuccessfulPrebuild: false, }); const FeatureFlagContextProvider: React.FC = ({ children }) => { @@ -31,6 +33,7 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => { const team = getCurrentTeam(location, teams); const [showPersistentVolumeClaimUI, setShowPersistentVolumeClaimUI] = useState(false); const [showUsageView, setShowUsageView] = useState(false); + const [showUseLastSuccessfulPrebuild, setShowUseLastSuccessfulPrebuild] = useState(false); useEffect(() => { if (!user) return; @@ -38,6 +41,7 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => { const featureFlags: FeatureFlagConfig = { persistent_volume_claim: { defaultValue: true, setter: setShowPersistentVolumeClaimUI }, usage_view: { defaultValue: false, setter: setShowUsageView }, + showUseLastSuccessfulPrebuild: { defaultValue: false, setter: setShowUseLastSuccessfulPrebuild }, }; for (const [flagName, config] of Object.entries(featureFlags)) { if (teams) { @@ -69,7 +73,9 @@ const FeatureFlagContextProvider: React.FC = ({ children }) => { }, [user, teams, team, project]); return ( - + {children} ); diff --git a/components/dashboard/src/start/CreateWorkspace.tsx b/components/dashboard/src/start/CreateWorkspace.tsx index 6da718bef90cc7..53286c474094cd 100644 --- a/components/dashboard/src/start/CreateWorkspace.tsx +++ b/components/dashboard/src/start/CreateWorkspace.tsx @@ -29,6 +29,7 @@ import { BillingAccountSelector } from "../components/BillingAccountSelector"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { TeamsContext } from "../teams/teams-context"; import Alert from "../components/Alert"; +import { FeatureFlagContext } from "../contexts/FeatureFlagContext"; export interface CreateWorkspaceProps { contextUrl: string; @@ -269,6 +270,9 @@ export default class CreateWorkspace extends React.Component + this.createWorkspace(CreateWorkspaceMode.UseLastSuccessfulPrebuild) + } onIgnorePrebuild={() => this.createWorkspace(CreateWorkspaceMode.ForceNew)} onPrebuildSucceeded={() => this.createWorkspace(CreateWorkspaceMode.UsePrebuild)} /> @@ -531,12 +535,14 @@ interface RunningPrebuildViewProps { starting: RunningWorkspacePrebuildStarting; sameCluster: boolean; }; + onUseLastSuccessfulPrebuild: () => void; onIgnorePrebuild: () => void; onPrebuildSucceeded: () => void; } function RunningPrebuildView(props: RunningPrebuildViewProps) { const workspaceId = props.runningPrebuild.workspaceID; + const { showUseLastSuccessfulPrebuild } = useContext(FeatureFlagContext); useEffect(() => { const disposables = new DisposableCollection(); @@ -565,6 +571,14 @@ function RunningPrebuildView(props: RunningPrebuildViewProps) { {/* TODO(gpl) Copied around in Start-/CreateWorkspace. This should properly go somewhere central. */}
+ {showUseLastSuccessfulPrebuild && ( + + )} diff --git a/components/gitpod-protocol/src/experiments/configcat.ts b/components/gitpod-protocol/src/experiments/configcat.ts index e2b227235da568..beb4c2794273ab 100644 --- a/components/gitpod-protocol/src/experiments/configcat.ts +++ b/components/gitpod-protocol/src/experiments/configcat.ts @@ -13,7 +13,6 @@ export const USER_ID_ATTRIBUTE = "user_id"; export const PROJECT_ID_ATTRIBUTE = "project_id"; export const TEAM_ID_ATTRIBUTE = "team_id"; export const TEAM_NAME_ATTRIBUTE = "team_name"; -export const TEAM_NAMES_ATTRIBUTE = "team_names"; export const BILLING_TIER_ATTRIBUTE = "billing_tier"; export class ConfigCatClient implements Client { diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 3e97767421e660..2c98248ebcba4c 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -1111,13 +1111,16 @@ export namespace SnapshotContext { } } -export interface StartPrebuildContext extends WorkspaceContext { - actual: WorkspaceContext; +export interface WithCommitHistory { commitHistory?: string[]; additionalRepositoryCommitHistories?: { cloneUrl: string; commitHistory: string[]; }[]; +} + +export interface StartPrebuildContext extends WorkspaceContext, WithCommitHistory { + actual: WorkspaceContext; project?: Project; branch?: string; } @@ -1382,6 +1385,8 @@ export enum CreateWorkspaceMode { UsePrebuild = "use-prebuild", // SelectIfRunning returns a list of currently running workspaces for the context URL if there are any, otherwise falls back to Default mode SelectIfRunning = "select-if-running", + // UseLastSuccessfulPrebuild returns ... + UseLastSuccessfulPrebuild = "use-last-successful-prebuild", } export namespace WorkspaceCreationResult { diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index 9581c434069311..7dce1de91e1912 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -22,6 +22,7 @@ import { PrebuildStatusMaintainer } from "./prebuilds/prebuilt-status-maintainer import { GitLabApp } from "./prebuilds/gitlab-app"; import { BitbucketApp } from "./prebuilds/bitbucket-app"; import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app"; +import { IncrementalPrebuildsService } from "./prebuilds/incremental-prebuilds-service"; import { IPrefixContextParser } from "../../src/workspace/context-parser"; import { StartPrebuildContextParser } from "./prebuilds/start-prebuild-context-parser"; import { WorkspaceFactory } from "../../src/workspace/workspace-factory"; @@ -83,6 +84,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is bind(BitbucketAppSupport).toSelf().inSingletonScope(); bind(GitHubEnterpriseApp).toSelf().inSingletonScope(); bind(BitbucketServerApp).toSelf().inSingletonScope(); + bind(IncrementalPrebuildsService).toSelf().inSingletonScope(); bind(UserCounter).toSelf().inSingletonScope(); diff --git a/components/server/ee/src/prebuilds/incremental-prebuilds-service.ts b/components/server/ee/src/prebuilds/incremental-prebuilds-service.ts new file mode 100644 index 00000000000000..84fbeffdefb7e5 --- /dev/null +++ b/components/server/ee/src/prebuilds/incremental-prebuilds-service.ts @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { inject, injectable } from "inversify"; +import { + CommitContext, + PrebuiltWorkspace, + TaskConfig, + User, + Workspace, + WorkspaceConfig, + WorkspaceImageSource, +} from "@gitpod/gitpod-protocol"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { WithCommitHistory } from "@gitpod/gitpod-protocol/src/protocol"; +import { WorkspaceDB } from "@gitpod/gitpod-db/lib"; +import { Config } from "../../../src/config"; +import { ConfigProvider } from "../../../src/workspace/config-provider"; +import { HostContextProvider } from "../../../src/auth/host-context-provider"; +import { ImageSourceProvider } from "../../../src/workspace/image-source-provider"; + +@injectable() +export class IncrementalPrebuildsService { + @inject(Config) protected readonly config: Config; + @inject(ConfigProvider) protected readonly configProvider: ConfigProvider; + @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; + @inject(ImageSourceProvider) protected readonly imageSourceProvider: ImageSourceProvider; + @inject(WorkspaceDB) protected readonly workspaceDB: WorkspaceDB; + + public async getCommitHistoryForContext(context: CommitContext, user: User): Promise { + const maxDepth = this.config.incrementalPrebuilds.commitHistory; + const hostContext = this.hostContextProvider.get(context.repository.host); + const repoProvider = hostContext?.services?.repositoryProvider; + if (!repoProvider) { + return {}; + } + const history: WithCommitHistory = {}; + history.commitHistory = await repoProvider.getCommitHistory( + user, + context.repository.owner, + context.repository.name, + context.revision, + maxDepth, + ); + if (context.additionalRepositoryCheckoutInfo && context.additionalRepositoryCheckoutInfo.length > 0) { + const histories = context.additionalRepositoryCheckoutInfo.map(async (info) => { + const commitHistory = await repoProvider.getCommitHistory( + user, + info.repository.owner, + info.repository.name, + info.revision, + maxDepth, + ); + return { + cloneUrl: info.repository.cloneUrl, + commitHistory, + }; + }); + history.additionalRepositoryCommitHistories = await Promise.all(histories); + } + return history; + } + + public async findGoodBaseForIncrementalBuild( + context: CommitContext, + history: WithCommitHistory, + user: User, + ): Promise { + if (!history.commitHistory || history.commitHistory.length < 1) { + return; + } + + const { config } = await this.configProvider.fetchConfig({}, user, context); + const imageSource = await this.imageSourceProvider.getImageSource({}, user, context, config); + + // Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality + // (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected) + const recentPrebuilds = await this.workspaceDB.findPrebuildsWithWorkpace(context.repository.cloneUrl); + for (const recentPrebuild of recentPrebuilds) { + if ( + await this.isGoodBaseforIncrementalBuild( + history, + config, + imageSource, + recentPrebuild.prebuild, + recentPrebuild.workspace, + ) + ) { + return recentPrebuild.prebuild; + } + } + } + + protected async isGoodBaseforIncrementalBuild( + history: WithCommitHistory, + config: WorkspaceConfig, + imageSource: WorkspaceImageSource, + candidatePrebuild: PrebuiltWorkspace, + candidateWorkspace: Workspace, + ): Promise { + if (!history.commitHistory || history.commitHistory.length === 0) { + return false; + } + if (!CommitContext.is(candidateWorkspace.context)) { + return false; + } + + // we are only considering available prebuilds + if (candidatePrebuild.state !== "available") { + return false; + } + + // we are only considering full prebuilds + if (!!candidateWorkspace.basedOnPrebuildId) { + return false; + } + + if ( + candidateWorkspace.context.additionalRepositoryCheckoutInfo?.length !== + history.additionalRepositoryCommitHistories?.length + ) { + // different number of repos + return false; + } + + const candidateCtx = candidateWorkspace.context; + if (!history.commitHistory.some((sha) => sha === candidateCtx.revision)) { + return false; + } + + // check the commits are included in the commit history + for (const subRepo of candidateWorkspace.context.additionalRepositoryCheckoutInfo || []) { + const matchIngRepo = history.additionalRepositoryCommitHistories?.find( + (repo) => repo.cloneUrl === subRepo.repository.cloneUrl, + ); + if (!matchIngRepo || !matchIngRepo.commitHistory.some((sha) => sha === subRepo.revision)) { + return false; + } + } + + // ensure the image source hasn't changed (skips older images) + if (JSON.stringify(imageSource) !== JSON.stringify(candidateWorkspace.imageSource)) { + log.debug(`Skipping parent prebuild: Outdated image`, { + imageSource, + parentImageSource: candidateWorkspace.imageSource, + }); + return false; + } + + // ensure the tasks haven't changed + const filterPrebuildTasks = (tasks: TaskConfig[] = []) => + tasks + .map((task) => + Object.keys(task) + .filter((key) => ["before", "init", "prebuild"].includes(key)) + // @ts-ignore + .reduce((obj, key) => ({ ...obj, [key]: task[key] }), {}), + ) + .filter((task) => Object.keys(task).length > 0); + const prebuildTasks = filterPrebuildTasks(config.tasks); + const parentPrebuildTasks = filterPrebuildTasks(candidateWorkspace.config.tasks); + if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) { + log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, { + prebuildTasks, + parentPrebuildTasks, + }); + return false; + } + + return true; + } +} diff --git a/components/server/ee/src/prebuilds/prebuild-manager.ts b/components/server/ee/src/prebuilds/prebuild-manager.ts index ec357a00e636f8..8d161ea8546b8d 100644 --- a/components/server/ee/src/prebuilds/prebuild-manager.ts +++ b/components/server/ee/src/prebuilds/prebuild-manager.ts @@ -33,6 +33,7 @@ import { inject, injectable } from "inversify"; import * as opentracing from "opentracing"; import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib"; import { error } from "console"; +import { IncrementalPrebuildsService } from "./incremental-prebuilds-service"; export class WorkspaceRunningError extends Error { constructor(msg: string, public instance: WorkspaceInstance) { @@ -59,6 +60,7 @@ export class PrebuildManager { @inject(ConfigProvider) protected readonly configProvider: ConfigProvider; @inject(Config) protected readonly config: Config; @inject(ProjectsService) protected readonly projectService: ProjectsService; + @inject(IncrementalPrebuildsService) protected readonly incrementalPrebuildsService: IncrementalPrebuildsService; async abortPrebuildsForBranch(ctx: TraceContext, project: Project, user: User, branch: string): Promise { const span = TraceContext.startSpan("abortPrebuildsForBranch", ctx); @@ -172,36 +174,15 @@ export class PrebuildManager { }; if (this.shouldPrebuildIncrementally(context.repository.cloneUrl, project)) { - const maxDepth = this.config.incrementalPrebuilds.commitHistory; - const hostContext = this.hostContextProvider.get(context.repository.host); - const repoProvider = hostContext?.services?.repositoryProvider; - if (repoProvider) { - prebuildContext.commitHistory = await repoProvider.getCommitHistory( - user, - context.repository.owner, - context.repository.name, - context.revision, - maxDepth, - ); - if ( - context.additionalRepositoryCheckoutInfo && - context.additionalRepositoryCheckoutInfo.length > 0 - ) { - const histories = context.additionalRepositoryCheckoutInfo.map(async (info) => { - const commitHistory = await repoProvider.getCommitHistory( - user, - info.repository.owner, - info.repository.name, - info.revision, - maxDepth, - ); - return { - cloneUrl: info.repository.cloneUrl, - commitHistory, - }; - }); - prebuildContext.additionalRepositoryCommitHistories = await Promise.all(histories); - } + // We store the commit histories in the `StartPrebuildContext` in order to pass them down to + // `WorkspaceFactoryEE.createForStartPrebuild`. + const { commitHistory, additionalRepositoryCommitHistories } = + await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); + if (commitHistory) { + prebuildContext.commitHistory = commitHistory; + } + if (additionalRepositoryCommitHistories) { + prebuildContext.additionalRepositoryCommitHistories = additionalRepositoryCommitHistories; } } diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 201b55362b52f6..0917ee69234228 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -116,10 +116,12 @@ import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billin import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; import { BillingModes } from "../billing/billing-mode"; import { UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb"; +import { IncrementalPrebuildsService } from "../prebuilds/incremental-prebuilds-service"; @injectable() export class GitpodServerEEImpl extends GitpodServerImpl { @inject(PrebuildManager) protected readonly prebuildManager: PrebuildManager; + @inject(IncrementalPrebuildsService) protected readonly incrementalPrebuildsService: IncrementalPrebuildsService; @inject(LicenseDB) protected readonly licenseDB: LicenseDB; @inject(LicenseKeySource) protected readonly licenseKeySource: LicenseKeySource; @@ -979,6 +981,14 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const logPayload = { mode, cloneUrl, commit: commitSHAs, prebuiltWorkspace }; log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload); + if (prebuiltWorkspace?.state !== "available" && mode === CreateWorkspaceMode.UseLastSuccessfulPrebuild) { + const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user); + prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( + context, + history, + user, + ); + } if (!prebuiltWorkspace) { return; } diff --git a/components/server/ee/src/workspace/workspace-factory.ts b/components/server/ee/src/workspace/workspace-factory.ts index ca195c502ad5e6..8596054fd351b4 100644 --- a/components/server/ee/src/workspace/workspace-factory.ts +++ b/components/server/ee/src/workspace/workspace-factory.ts @@ -17,10 +17,6 @@ import { WorkspaceContext, WithSnapshot, WithPrebuild, - TaskConfig, - PrebuiltWorkspace, - WorkspaceConfig, - WorkspaceImageSource, OpenPrebuildContext, } from "@gitpod/gitpod-protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @@ -35,6 +31,7 @@ import { increasePrebuildsStartedCounter } from "../../../src/prometheus-metrics import { DeepPartial } from "@gitpod/gitpod-protocol/lib/util/deep-partial"; import { EntitlementService } from "../../../src/billing/entitlement-service"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; +import { IncrementalPrebuildsService } from "../prebuilds/incremental-prebuilds-service"; @injectable() export class WorkspaceFactoryEE extends WorkspaceFactory { @@ -42,6 +39,7 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; @inject(UserCounter) protected readonly userCounter: UserCounter; @inject(EntitlementService) protected readonly entitlementService: EntitlementService; + @inject(IncrementalPrebuildsService) protected readonly incrementalPrebuildsService: IncrementalPrebuildsService; @inject(UserDB) protected readonly userDB: UserDB; @@ -118,68 +116,46 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { await assertNoPrebuildIsRunningForSameCommit(); const { config } = await this.configProvider.fetchConfig({ span }, user, context.actual); - const imageSource = await this.imageSourceProvider.getImageSource(ctx, user, context.actual, config); - // Walk back the last prebuilds and check if they are valid ancestor. + // If an incremental prebuild was requested, see if we can find a recent prebuild to act as a base. let ws; - if (context.commitHistory && context.commitHistory.length > 0) { - // Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality - // (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected) - const recentPrebuilds = await this.db - .trace({ span }) - .findPrebuildsWithWorkpace(commitContext.repository.cloneUrl); - + const recentPrebuild = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild( + commitContext, + context, + user, + ); + if (recentPrebuild) { const loggedContext = filterForLogging(context); - for (const recentPrebuild of recentPrebuilds) { - if ( - !(await this.isGoodBaseforIncrementalPrebuild( - context, - config, - imageSource, - recentPrebuild.prebuild, - recentPrebuild.workspace, - )) - ) { - log.debug({ userId: user.id }, "Not using incremental prebuild base", { - candidatePrebuildId: recentPrebuild.prebuild.id, - context: loggedContext, - }); - continue; - } - - log.info({ userId: user.id }, "Using incremental prebuild base", { - basePrebuildId: recentPrebuild.prebuild.id, - context: loggedContext, - }); - - const incrementalPrebuildContext: PrebuiltWorkspaceContext = { - title: `Incremental prebuild of "${commitContext.title}"`, - originalContext: commitContext, - prebuiltWorkspace: recentPrebuild.prebuild, - }; - - // repeated assertion on prebuilds triggered for same commit here, in order to - // reduce likelihood of duplicates if for instance handled by two different - // server pods. - await assertNoPrebuildIsRunningForSameCommit(); - - ws = await this.createForPrebuiltWorkspace( - { span }, - user, - incrementalPrebuildContext, - normalizedContextURL, - ); - // Overwrite the config from the parent prebuild: - // `createForPrebuiltWorkspace` 1:1 copies the config from the parent prebuild. - // Above, we've made sure that the parent's prebuild tasks (before/init/prebuild) are still the same as now. - // However, other non-prebuild config items might be outdated (e.g. any command task, VS Code extension, ...) - // To fix this, we overwrite the new prebuild's config with the most-recently fetched config. - // See also: https://github.com/gitpod-io/gitpod/issues/7475 - //TODO(sven) doing side effects on objects back and forth is complicated and error-prone. We should rather make sure we pass in the config when creating the prebuiltWorkspace. - ws.config = config; - - break; - } + log.info({ userId: user.id }, "Using incremental prebuild base", { + basePrebuildId: recentPrebuild.id, + context: loggedContext, + }); + + const incrementalPrebuildContext: PrebuiltWorkspaceContext = { + title: `Incremental prebuild of "${commitContext.title}"`, + originalContext: commitContext, + prebuiltWorkspace: recentPrebuild, + }; + + // repeated assertion on prebuilds triggered for same commit here, in order to + // reduce likelihood of duplicates if for instance handled by two different + // server pods. + await assertNoPrebuildIsRunningForSameCommit(); + + ws = await this.createForPrebuiltWorkspace( + { span }, + user, + incrementalPrebuildContext, + normalizedContextURL, + ); + // Overwrite the config from the parent prebuild: + // `createForPrebuiltWorkspace` 1:1 copies the config from the parent prebuild. + // Above, we've made sure that the parent's prebuild tasks (before/init/prebuild) are still the same as now. + // However, other non-prebuild config items might be outdated (e.g. any command task, VS Code extension, ...) + // To fix this, we overwrite the new prebuild's config with the most-recently fetched config. + // See also: https://github.com/gitpod-io/gitpod/issues/7475 + //TODO(sven) doing side effects on objects back and forth is complicated and error-prone. We should rather make sure we pass in the config when creating the prebuiltWorkspace. + ws.config = config; } // repeated assertion on prebuilds triggered for same commit here, in order to @@ -237,85 +213,6 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { } } - private async isGoodBaseforIncrementalPrebuild( - context: StartPrebuildContext, - config: WorkspaceConfig, - imageSource: WorkspaceImageSource, - candidatePrebuild: PrebuiltWorkspace, - candidate: Workspace, - ): Promise { - if (!context.commitHistory || context.commitHistory.length === 0) { - return false; - } - if (!CommitContext.is(candidate.context)) { - return false; - } - - // we are only considering available prebuilds - if (candidatePrebuild.state !== "available") { - return false; - } - - // we are only considering full prebuilds - if (!!candidate.basedOnPrebuildId) { - return false; - } - - const candidateCtx = candidate.context; - if ( - candidateCtx.additionalRepositoryCheckoutInfo?.length !== - context.additionalRepositoryCommitHistories?.length - ) { - // different number of repos - return false; - } - - if (!context.commitHistory.some((sha) => sha === candidateCtx.revision)) { - return false; - } - - // check the commits are included in the commit history - for (const subRepo of candidateCtx.additionalRepositoryCheckoutInfo || []) { - const matchIngRepo = context.additionalRepositoryCommitHistories?.find( - (repo) => repo.cloneUrl === subRepo.repository.cloneUrl, - ); - if (!matchIngRepo || !matchIngRepo.commitHistory.some((sha) => sha === subRepo.revision)) { - return false; - } - } - - // ensure the image source hasn't changed (skips older images) - if (JSON.stringify(imageSource) !== JSON.stringify(candidate.imageSource)) { - log.debug(`Skipping parent prebuild: Outdated image`, { - imageSource, - parentImageSource: candidate.imageSource, - }); - return false; - } - - // ensure the tasks haven't changed - const filterPrebuildTasks = (tasks: TaskConfig[] = []) => - tasks - .map((task) => - Object.keys(task) - .filter((key) => ["before", "init", "prebuild"].includes(key)) - // @ts-ignore - .reduce((obj, key) => ({ ...obj, [key]: task[key] }), {}), - ) - .filter((task) => Object.keys(task).length > 0); - const prebuildTasks = filterPrebuildTasks(config.tasks); - const parentPrebuildTasks = filterPrebuildTasks(candidate.config.tasks); - if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) { - log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, { - prebuildTasks, - parentPrebuildTasks, - }); - return false; - } - - return true; - } - protected async createForPrebuiltWorkspace( ctx: TraceContext, user: User,