Skip to content

v3: deploy rollbacks #1154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ArrowPathIcon } from "@heroicons/react/20/solid";
import { Form, useNavigation } from "@remix-run/react";
import { Button } from "~/components/primitives/Buttons";
import {
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
} from "~/components/primitives/Dialog";

type RollbackDeploymentDialogProps = {
projectId: string;
deploymentShortCode: string;
redirectPath: string;
};

export function RollbackDeploymentDialog({
projectId,
deploymentShortCode,
redirectPath,
}: RollbackDeploymentDialogProps) {
const navigation = useNavigation();

const formAction = `/resources/${projectId}/deployments/${deploymentShortCode}/rollback`;
const isLoading = navigation.formAction === formAction;

return (
<DialogContent key="rollback">
<DialogHeader>Roll back to this deployment?</DialogHeader>
<DialogDescription>
This deployment will become the default for all future runs. Tasks triggered but not
included in this deploy will remain queued until you roll back to or create a new deployment
with these tasks included.
</DialogDescription>
<DialogFooter>
<Form
action={`/resources/${projectId}/deployments/${deploymentShortCode}/rollback`}
method="post"
>
<Button
type="submit"
name="redirectUrl"
value={redirectPath}
variant="primary/small"
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
disabled={isLoading}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{isLoading ? "Rolling back..." : "Roll back deployment"}
</Button>
</Form>
</DialogFooter>
</DialogContent>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { getUsername } from "~/utils/username";

const pageSize = 20;

export type DeploymentList = Awaited<ReturnType<DeploymentListPresenter["call"]>>;
export type DeploymentListItem = DeploymentList["deployments"][0];

export class DeploymentListPresenter {
#prismaClient: PrismaClient;

Expand Down Expand Up @@ -136,6 +139,8 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
deployedAt: deployment.deployedAt,
tasksCount: deployment.tasksCount ? Number(deployment.tasksCount) : null,
label: label?.label,
isCurrent: label?.label === "current",
isDeployed: deployment.status === "DEPLOYED",
environment: {
id: environment.id,
type: environment.type,
Expand Down
24 changes: 21 additions & 3 deletions apps/webapp/app/presenters/v3/TaskListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import type { User } from "~/models/user.server";
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
import {
filterOrphanedEnvironments,
onlyDevEnvironments,
exceptDevEnvironments,
sortEnvironments,
} from "~/utils/environmentSort";
import { logger } from "~/services/logger.server";
import { BasePresenter } from "./basePresenter.server";
import { TaskRunStatus } from "~/database-types";
import { CURRENT_DEPLOYMENT_LABEL } from "~/consts";

export type Task = {
slug: string;
Expand Down Expand Up @@ -72,6 +78,9 @@ export class TaskListPresenter extends BasePresenter {
},
});

const devEnvironments = onlyDevEnvironments(project.environments);
const nonDevEnvironments = exceptDevEnvironments(project.environments);

const tasks = await this._replica.$queryRaw<
{
id: string;
Expand All @@ -83,12 +92,21 @@ export class TaskListPresenter extends BasePresenter {
triggerSource: TaskTriggerSource;
}[]
>`
WITH workers AS (
WITH non_dev_workers AS (
SELECT wd."workerId" AS id
FROM ${sqlDatabaseSchema}."WorkerDeploymentPromotion" wdp
INNER JOIN ${sqlDatabaseSchema}."WorkerDeployment" wd
ON wd.id = wdp."deploymentId"
WHERE wdp."environmentId" IN (${Prisma.join(nonDevEnvironments.map((e) => e.id))})
AND wdp."label" = ${CURRENT_DEPLOYMENT_LABEL}
),
workers AS (
SELECT DISTINCT ON ("runtimeEnvironmentId") id, "runtimeEnvironmentId", version
FROM ${sqlDatabaseSchema}."BackgroundWorker"
WHERE "runtimeEnvironmentId" IN (${Prisma.join(
filterOrphanedEnvironments(project.environments).map((e) => e.id)
filterOrphanedEnvironments(devEnvironments).map((e) => e.id)
)})
OR id IN (SELECT id FROM non_dev_workers)
ORDER BY "runtimeEnvironmentId", "createdAt" DESC
)
SELECT tasks.id, slug, "filePath", "exportName", "triggerSource", tasks."runtimeEnvironmentId", tasks."createdAt"
Expand Down
74 changes: 38 additions & 36 deletions apps/webapp/app/presenters/v3/TestPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
import { TestSearchParams } from "~/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.test/route";
import { sortEnvironments } from "~/utils/environmentSort";
import { createSearchParams } from "~/utils/searchParams";
import { getUsername } from "~/utils/username";
import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server";
import { BasePresenter } from "./basePresenter.server";

type TaskListOptions = {
userId: string;
Expand All @@ -15,16 +16,10 @@ export type TaskList = Awaited<ReturnType<TestPresenter["call"]>>;
export type TaskListItem = NonNullable<TaskList["tasks"]>[0];
export type SelectedEnvironment = NonNullable<TaskList["selectedEnvironment"]>;

export class TestPresenter {
#prismaClient: PrismaClient;

constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}

export class TestPresenter extends BasePresenter {
public async call({ userId, projectSlug, url }: TaskListOptions) {
// Find the project scoped to the organization
const project = await this.#prismaClient.project.findFirstOrThrow({
const project = await this._replica.project.findFirstOrThrow({
select: {
id: true,
environments: {
Expand Down Expand Up @@ -85,31 +80,8 @@ export class TestPresenter {
};
}

//get all possible tasks
const tasks = await this.#prismaClient.$queryRaw<
{
id: string;
version: string;
taskIdentifier: string;
filePath: string;
exportName: string;
friendlyId: string;
triggerSource: TaskTriggerSource;
}[]
>`WITH workers AS (
SELECT
bw.*,
ROW_NUMBER() OVER(ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
FROM
${sqlDatabaseSchema}."BackgroundWorker" bw
WHERE "runtimeEnvironmentId" = ${matchingEnvironment.id}
),
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
SELECT bwt.id, version, slug as "taskIdentifier", "filePath", "exportName", bwt."friendlyId", bwt."triggerSource"
FROM latest_workers
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" bwt ON bwt."workerId" = latest_workers.id
ORDER BY bwt."exportName" ASC;
`;
const isDev = matchingEnvironment.type === "DEVELOPMENT";
const tasks = await this.#getTasks(matchingEnvironment.id, isDev);

return {
hasSelectedEnvironment: true as const,
Expand All @@ -118,8 +90,7 @@ export class TestPresenter {
tasks: tasks.map((task) => {
return {
id: task.id,
version: task.version,
taskIdentifier: task.taskIdentifier,
taskIdentifier: task.slug,
filePath: task.filePath,
exportName: task.exportName,
friendlyId: task.friendlyId,
Expand All @@ -128,4 +99,35 @@ export class TestPresenter {
}),
};
}

async #getTasks(envId: string, isDev: boolean) {
if (isDev) {
return await this._replica.$queryRaw<
{
id: string;
version: string;
slug: string;
filePath: string;
exportName: string;
friendlyId: string;
triggerSource: TaskTriggerSource;
}[]
>`WITH workers AS (
SELECT
bw.*,
ROW_NUMBER() OVER(ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
FROM
${sqlDatabaseSchema}."BackgroundWorker" bw
WHERE "runtimeEnvironmentId" = ${envId}
),
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
SELECT bwt.id, version, slug, "filePath", "exportName", bwt."friendlyId", bwt."triggerSource"
FROM latest_workers
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" bwt ON bwt."workerId" = latest_workers.id
ORDER BY bwt."exportName" ASC;`;
} else {
const currentDeployment = await findCurrentWorkerDeployment(envId);
return currentDeployment?.worker?.tasks ?? [];
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { CommandLineIcon, ServerIcon } from "@heroicons/react/20/solid";
import { Outlet, useParams } from "@remix-run/react";
import { ArrowPathIcon, CommandLineIcon, ServerIcon } from "@heroicons/react/20/solid";
import { Outlet, useLocation, useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { TerminalIcon, TerminalSquareIcon } from "lucide-react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { UserAvatar } from "~/components/UserProfilePhoto";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
import { LinkButton } from "~/components/primitives/Buttons";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
Expand All @@ -24,24 +24,26 @@ import {
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { TextLink } from "~/components/primitives/TextLink";
import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus";
import { RollbackDeploymentDialog } from "~/components/runs/v3/RollbackDeploymentDialog";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { DeploymentListPresenter } from "~/presenters/v3/DeploymentListPresenter.server";
import {
DeploymentListItem,
DeploymentListPresenter,
} from "~/presenters/v3/DeploymentListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import {
ProjectParamSchema,
docsPath,
v3DeploymentPath,
v3DeploymentsPath,
v3EnvironmentVariablesPath,
} from "~/utils/pathBuilder";
import { createSearchParams } from "~/utils/searchParams";
Expand Down Expand Up @@ -166,7 +168,7 @@ export default function Page() {
"–"
)}
</TableCell>
<TableCellChevron to={path} />
<DeploymentActionsCell deployment={deployment} path={path} />
</TableRow>
);
})
Expand Down Expand Up @@ -240,3 +242,35 @@ function CreateDeploymentInstructions() {
</MainCenteredContainer>
);
}

function DeploymentActionsCell({
deployment,
path,
}: {
deployment: DeploymentListItem;
path: string;
}) {
const location = useLocation();
const project = useProject();

if (deployment.isCurrent || !deployment.isDeployed) return <TableCell to={path}>{""}</TableCell>;

return (
<TableCellMenu isSticky>
{!deployment.isCurrent && deployment.isDeployed && (
<Dialog>
<DialogTrigger asChild>
<Button variant="small-menu-item" LeadingIcon={ArrowPathIcon}>
Rollback
</Button>
</DialogTrigger>
<RollbackDeploymentDialog
projectId={project.id}
deploymentShortCode={deployment.shortCode}
redirectPath={`${location.pathname}${location.search}`}
/>
</Dialog>
)}
</TableCellMenu>
);
}
Loading
Loading