Projects ✨
- Deploy a new VM when a push event comes through - Live updating project overview page
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
SDK now passes through the project ID from the env var
|
||||
+2
-1
@@ -45,4 +45,5 @@ yarn-error.log*
|
||||
.output
|
||||
apps/**/public/build
|
||||
.tests-container-id.txt
|
||||
.sentryclirc
|
||||
.sentryclirc
|
||||
.buildt
|
||||
@@ -52,6 +52,9 @@ windows:
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-pizzly.sh <your-pizzly-dev>
|
||||
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
|
||||
commands:
|
||||
- exec: ./scripts/proxy-wss.sh <your-wss-dev>
|
||||
- title: wss and examples
|
||||
layout:
|
||||
split_direction: horizontal
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useLocales } from "./LocaleProvider";
|
||||
|
||||
type IntlDateProps = {
|
||||
date: Date;
|
||||
timeZone?: string;
|
||||
};
|
||||
|
||||
export const IntlDate = ({ date, timeZone }: IntlDateProps) => {
|
||||
const locales = useLocales();
|
||||
const isoString = date.toISOString();
|
||||
const formattedDate = new Intl.DateTimeFormat(locales, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone,
|
||||
}).format(date);
|
||||
|
||||
const formattedTime = new Intl.DateTimeFormat(locales, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
timeZone,
|
||||
}).format(date);
|
||||
|
||||
return (
|
||||
<time dateTime={isoString}>
|
||||
{formattedDate} at {formattedTime}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useContext } from "react";
|
||||
import { createContext } from "react";
|
||||
|
||||
type LocaleContext = {
|
||||
locales: string[];
|
||||
};
|
||||
|
||||
type LocaleContextProviderProps = {
|
||||
locales: string[];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Context = createContext<LocaleContext | null>(null);
|
||||
|
||||
export const LocaleContextProvider = ({
|
||||
locales,
|
||||
children,
|
||||
}: LocaleContextProviderProps) => {
|
||||
const value = { locales };
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
const throwIfNoProvider = () => {
|
||||
throw new Error("Please wrap your application in a LocaleContextProvider.");
|
||||
};
|
||||
|
||||
export const useLocales = () => {
|
||||
const { locales } = useContext(Context) ?? throwIfNoProvider();
|
||||
return locales;
|
||||
};
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BeakerIcon,
|
||||
ChevronLeftIcon,
|
||||
CloudArrowUpIcon,
|
||||
Cog6ToothIcon,
|
||||
EnvelopeIcon,
|
||||
ForwardIcon,
|
||||
PhoneArrowUpRightIcon,
|
||||
PlusCircleIcon,
|
||||
QueueListIcon,
|
||||
Squares2X2Icon,
|
||||
SquaresPlusIcon,
|
||||
UsersIcon,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
} from "~/hooks/useOrganizations";
|
||||
import { useCurrentWorkflow } from "~/hooks/useWorkflows";
|
||||
import { EnvironmentIcon } from "~/routes/resources/environment";
|
||||
import { CurrentProject } from "~/routes/__app/orgs/$organizationSlug/projects/$projectP";
|
||||
import { titleCase } from "~/utils";
|
||||
import { CopyTextPanel } from "../CopyTextButton";
|
||||
import { TertiaryA, TertiaryButton } from "../primitives/Buttons";
|
||||
@@ -152,6 +155,43 @@ export function WorkflowsSideMenu() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectSideMenu({
|
||||
project,
|
||||
backPath,
|
||||
}: {
|
||||
project: CurrentProject;
|
||||
backPath: string;
|
||||
}) {
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items: SideMenuItem[] = [
|
||||
{
|
||||
name: "Overview",
|
||||
icon: <ArrowsRightLeftIcon className={iconStyle} />,
|
||||
to: ``,
|
||||
},
|
||||
{
|
||||
name: "Deploys",
|
||||
icon: <CloudArrowUpIcon className={iconStyle} />,
|
||||
to: `deploys`,
|
||||
},
|
||||
{
|
||||
name: "Logs",
|
||||
icon: <QueueListIcon className={iconStyle} />,
|
||||
to: `logs`,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
icon: <Cog6ToothIcon className={iconStyle} />,
|
||||
to: `settings`,
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu title={project.name} items={items} backPath={backPath} />;
|
||||
}
|
||||
|
||||
const defaultStyle =
|
||||
"group flex items-center gap-2 px-3 py-3 text-base rounded transition text-slate-300 hover:bg-slate-850 hover:text-white";
|
||||
const activeStyle =
|
||||
|
||||
@@ -3,30 +3,36 @@ import { hydrateRoot } from "react-dom/client";
|
||||
import * as Sentry from "@sentry/remix";
|
||||
import { useEffect } from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { LocaleContextProvider } from "./components/LocaleProvider";
|
||||
|
||||
hydrateRoot(document, <RemixBrowser />);
|
||||
hydrateRoot(
|
||||
document,
|
||||
<LocaleContextProvider locales={window.navigator.languages as string[]}>
|
||||
<RemixBrowser />
|
||||
</LocaleContextProvider>
|
||||
);
|
||||
|
||||
//hack because the type is not exported
|
||||
type SentryIntegration = (typeof Sentry.defaultIntegrations)[number];
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Sentry.init({
|
||||
dsn: "https://bf96820b08004fa4b2e1506f2ac74a14@o4504419574087680.ingest.sentry.io/4504419607052288",
|
||||
tracesSampleRate: 1,
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.remixRouterInstrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useMatches
|
||||
),
|
||||
}),
|
||||
//casted because TypeScript is unhappy about the type from PostHog
|
||||
new posthog.SentryIntegration(
|
||||
posthog,
|
||||
"triggerdev",
|
||||
4504419607052288
|
||||
) as SentryIntegration,
|
||||
],
|
||||
});
|
||||
Sentry.init({
|
||||
dsn: "https://bf96820b08004fa4b2e1506f2ac74a14@o4504419574087680.ingest.sentry.io/4504419607052288",
|
||||
tracesSampleRate: 1,
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.remixRouterInstrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useMatches
|
||||
),
|
||||
}),
|
||||
//casted because TypeScript is unhappy about the type from PostHog
|
||||
new posthog.SentryIntegration(
|
||||
posthog,
|
||||
"triggerdev",
|
||||
4504419607052288
|
||||
) as SentryIntegration,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { renderToPipeableStream } from "react-dom/server";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { Response } from "@remix-run/node"; // or cloudflare/deno
|
||||
import type { EntryContext, Headers } from "@remix-run/node"; // or cloudflare/deno
|
||||
import { parseAcceptLanguage } from "intl-parse-accept-language";
|
||||
import isbot from "isbot";
|
||||
import { LocaleContextProvider } from "./components/LocaleProvider";
|
||||
|
||||
const ABORT_DELAY = 30000;
|
||||
|
||||
@@ -15,6 +17,11 @@ export default function handleRequest(
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
const acceptLanguage = request.headers.get("accept-language");
|
||||
const locales = parseAcceptLanguage(acceptLanguage, {
|
||||
validate: Intl.DateTimeFormat.supportedLocalesOf,
|
||||
});
|
||||
|
||||
// If the request is from a bot, we want to wait for the full
|
||||
// response to render before sending it to the client. This
|
||||
// ensures that bots can see the full page content.
|
||||
@@ -23,7 +30,8 @@ export default function handleRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
locales
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +39,8 @@ export default function handleRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
remixContext,
|
||||
locales
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,15 +48,18 @@ function serveTheBots(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
remixContext: EntryContext,
|
||||
locales: string[]
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>
|
||||
</LocaleContextProvider>,
|
||||
{
|
||||
// Use onAllReady to wait for the entire document to be ready
|
||||
onAllReady() {
|
||||
@@ -74,16 +86,19 @@ function serveBrowsers(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
remixContext: EntryContext,
|
||||
locales: string[]
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
<LocaleContextProvider locales={locales}>
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>
|
||||
</LocaleContextProvider>,
|
||||
{
|
||||
// use onShellReady to wait until a suspense boundary is triggered
|
||||
onShellReady() {
|
||||
|
||||
@@ -57,6 +57,8 @@ const EnvironmentSchema = z.object({
|
||||
GITHUB_APP_WEBHOOK_SECRET: z.string().optional(),
|
||||
INTEGRATIONS_API_KEY: z.string(),
|
||||
INTEGRATIONS_API_ORIGIN: z.string(),
|
||||
CAKEWORK_API_KEY: z.string(),
|
||||
TRIGGER_WSS_URL: z.string().default("wss://wss.trigger.dev/ws"),
|
||||
});
|
||||
|
||||
export type Environment = z.infer<typeof EnvironmentSchema>;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
CakeworkApiClient,
|
||||
CakeworkApiEnvironment,
|
||||
} from "@cakework/client/dist";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
export const cakework = new CakeworkApiClient({
|
||||
environment: CakeworkApiEnvironment.Production,
|
||||
xApiKey: env.CAKEWORK_API_KEY,
|
||||
});
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { getAppInstallation } from "~/services/github/githubApp.server";
|
||||
import { taskQueue } from "../messageBroker.server";
|
||||
import { getAppInstallation } from "~/features/ee/projects/github/githubApp.server";
|
||||
import { taskQueue } from "../../../../services/messageBroker.server";
|
||||
|
||||
export class AppInstallationCallback {
|
||||
#prismaClient: PrismaClient;
|
||||
+78
-6
@@ -1,10 +1,11 @@
|
||||
import type { Endpoints } from "@octokit/types";
|
||||
import type { PushEvent } from "@octokit/webhooks-types";
|
||||
import { verify } from "@octokit/webhooks-methods";
|
||||
import { sign as signJWT } from "jsonwebtoken";
|
||||
import { z } from "zod";
|
||||
import { env } from "~/env.server";
|
||||
import type { EmitterWebhookEventName } from "@octokit/webhooks";
|
||||
import { taskQueue } from "../messageBroker.server";
|
||||
import { taskQueue } from "../../../../services/messageBroker.server";
|
||||
|
||||
export async function verifyAndReceiveWebhook(request: Request) {
|
||||
if (!env.GITHUB_APP_WEBHOOK_SECRET) {
|
||||
@@ -38,7 +39,9 @@ export async function verifyAndReceiveWebhook(request: Request) {
|
||||
|
||||
const parsedPayload = JSON.parse(payload);
|
||||
|
||||
const name = `${hookName}.${parsedPayload.action}` as EmitterWebhookEventName;
|
||||
const name = (
|
||||
parsedPayload.action ? `${hookName}.${parsedPayload.action}` : hookName
|
||||
) as EmitterWebhookEventName;
|
||||
|
||||
console.log(`[webhooks.github] Received event`, {
|
||||
id,
|
||||
@@ -70,6 +73,22 @@ async function handleGithubEvent<TName extends EmitterWebhookEventName>({
|
||||
id: payload.installation.id,
|
||||
});
|
||||
}
|
||||
case "push": {
|
||||
const push = payload as PushEvent;
|
||||
|
||||
// Only on pushes to a branch
|
||||
if (!push.ref.startsWith("refs/heads/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const branch = push.ref.replace("refs/heads/", "");
|
||||
|
||||
await taskQueue.publish("GITHUB_PUSH", {
|
||||
branch: branch,
|
||||
commitSha: push.after,
|
||||
repository: push.repository.full_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +172,6 @@ export async function getInstallationRepositories(
|
||||
perPage
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Fetched page ${page} of repositories, total_count = ${response.total_count}, repositories.length = ${response.repositories.length}`
|
||||
);
|
||||
|
||||
repositories.push(...response.repositories);
|
||||
|
||||
if (response.repositories.length < perPage) {
|
||||
@@ -194,6 +209,63 @@ async function getInstallationRepositoriesPage(
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function getRepositoryContent(
|
||||
token: string,
|
||||
repo: string,
|
||||
path: string
|
||||
): Promise<string | undefined> {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${repo}/contents/${path}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: "application/vnd.github.raw",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 404) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get repository content: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
export type GetCommitEndpoint =
|
||||
Endpoints["GET /repos/{owner}/{repo}/commits/{ref}"];
|
||||
|
||||
export type GetCommitResponse = GetCommitEndpoint["response"]["data"];
|
||||
|
||||
export type GitHubCommit = GetCommitResponse;
|
||||
|
||||
export async function getCommit(
|
||||
token: string,
|
||||
repo: string,
|
||||
ref: string
|
||||
): Promise<GitHubCommit> {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${repo}/commits/${ref}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get commit: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export const AccountSchema = z.object({
|
||||
login: z.string(),
|
||||
id: z.number(),
|
||||
+14
-1
@@ -8,8 +8,21 @@ export type GitHubAppAuthorizationWithValidToken = GitHubAppAuthorization & {
|
||||
};
|
||||
|
||||
export async function refreshInstallationAccessToken(
|
||||
authorization: GitHubAppAuthorization
|
||||
authorizationOrId: GitHubAppAuthorization | string
|
||||
): Promise<GitHubAppAuthorizationWithValidToken> {
|
||||
const authorization =
|
||||
typeof authorizationOrId === "string"
|
||||
? await prisma.gitHubAppAuthorization.findUnique({
|
||||
where: {
|
||||
id: authorizationOrId,
|
||||
},
|
||||
})
|
||||
: authorizationOrId;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error("App authorization not found");
|
||||
}
|
||||
|
||||
// Make sure the access token is not expired, or less than 10 minutes from expiring
|
||||
const accessTokenExpired =
|
||||
!authorization.installationAccessTokenExpiresAt ||
|
||||
@@ -0,0 +1,207 @@
|
||||
import type {
|
||||
RepositoryProject,
|
||||
RuntimeEnvironment,
|
||||
ProjectDeployment,
|
||||
} from ".prisma/client";
|
||||
import { z } from "zod";
|
||||
import { parse as parseYAML } from "yaml";
|
||||
import { getRepositoryContent } from "~/features/ee/projects/github/githubApp.server";
|
||||
import { refreshInstallationAccessToken } from "~/features/ee/projects/github/refreshInstallationAccessToken.server";
|
||||
import { env } from "~/env.server";
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
export async function findProjectByRepo(name: string) {
|
||||
return await prisma.repositoryProject.findUnique({
|
||||
where: {
|
||||
name,
|
||||
},
|
||||
include: {
|
||||
organization: {
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function findProjectById(id: string) {
|
||||
return await prisma.repositoryProject.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
organization: {
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function statusTextForBuilding(
|
||||
deployment: ProjectDeployment,
|
||||
project: RepositoryProject
|
||||
) {
|
||||
return `Building ${project.name}#${project.branch} at ${truncateSha(
|
||||
deployment.commitHash
|
||||
)}: "${deployment.commitMessage}" by ${deployment.committer}`;
|
||||
}
|
||||
|
||||
export function statusTextForDeploying(
|
||||
deployment: ProjectDeployment,
|
||||
project: RepositoryProject
|
||||
) {
|
||||
return `Deploying ${project.name}#${project.branch} at ${truncateSha(
|
||||
deployment.commitHash
|
||||
)}: "${deployment.commitMessage}" by ${deployment.committer}`;
|
||||
}
|
||||
|
||||
export function statusTextForDeployed(
|
||||
deployment: ProjectDeployment,
|
||||
project: RepositoryProject
|
||||
) {
|
||||
return `Deployed ${project.name}#${project.branch} at ${truncateSha(
|
||||
deployment.commitHash
|
||||
)}: "${deployment.commitMessage}" by ${deployment.committer}`;
|
||||
}
|
||||
|
||||
function truncateSha(sha: string) {
|
||||
return sha.substring(0, 7);
|
||||
}
|
||||
|
||||
export function repositoryProjectReadyToDeploy(project: RepositoryProject) {
|
||||
return project.status === "PENDING" && hasAllEnvVars(project);
|
||||
}
|
||||
|
||||
export function buildEnvVars(
|
||||
deployment: ProjectDeployment,
|
||||
project: RepositoryProject,
|
||||
environment: RuntimeEnvironment
|
||||
): Record<string, string> {
|
||||
const envVars = BluePrintEnvVarsSchema.parse(project.envVars);
|
||||
|
||||
const result = envVars.reduce((acc, envVar) => {
|
||||
if (envVar.key === "TRIGGER_API_KEY") {
|
||||
return {
|
||||
...acc,
|
||||
[envVar.key]: environment.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
if (!envVar.value) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[envVar.key]: envVar.value,
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...result,
|
||||
TRIGGER_PROJECT_ID: project.id,
|
||||
TRIGGER_DEPLOYMENT_ID: deployment.id,
|
||||
TRIGGER_WSS_URL: env.TRIGGER_WSS_URL,
|
||||
};
|
||||
}
|
||||
|
||||
function hasAllEnvVars(project: RepositoryProject) {
|
||||
const envVars = BluePrintEnvVarsSchema.parse(project.envVars);
|
||||
|
||||
// Removing the TRIGGER_API_KEY environment var, are there any other env vars that don't have a value?
|
||||
return (
|
||||
envVars
|
||||
.filter((envVar) => envVar.key !== "TRIGGER_API_KEY")
|
||||
.filter((envVar) => !envVar.value).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
export async function serviceDefinitionFromRepository(
|
||||
appAuthorizationId: string,
|
||||
repoName: string
|
||||
) {
|
||||
const appAuthorization = await refreshInstallationAccessToken(
|
||||
appAuthorizationId
|
||||
);
|
||||
|
||||
const renderYamlContent = await getRepositoryContent(
|
||||
appAuthorization.installationAccessToken,
|
||||
repoName,
|
||||
"render.yaml"
|
||||
);
|
||||
|
||||
if (!renderYamlContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawRenderYaml = safeParseYAML(renderYamlContent);
|
||||
|
||||
if (!rawRenderYaml) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blueprint = BlueprintSchema.safeParse(rawRenderYaml);
|
||||
|
||||
if (!blueprint.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a worker service with env = node and an envVar with the name TRIGGER_API_KEY
|
||||
const workerService = blueprint.data.services.find(
|
||||
(service) =>
|
||||
service.type === "worker" &&
|
||||
service.env === "node" &&
|
||||
service.envVars.find((envVar) => envVar.key === "TRIGGER_API_KEY")
|
||||
);
|
||||
|
||||
return workerService;
|
||||
}
|
||||
|
||||
function safeParseYAML(content: string) {
|
||||
try {
|
||||
return parseYAML(content);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const BluePrintEnvVarsSchema = z.array(
|
||||
z
|
||||
.object({
|
||||
key: z.string(),
|
||||
value: z.any(),
|
||||
sync: z.boolean().default(true),
|
||||
})
|
||||
.passthrough()
|
||||
);
|
||||
|
||||
const BlueprintServiceSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
type: z.enum(["web", "worker", "pserv", "cron"]),
|
||||
env: z.enum([
|
||||
"node",
|
||||
"go",
|
||||
"python",
|
||||
"ruby",
|
||||
"php",
|
||||
"java",
|
||||
"docker",
|
||||
"rust",
|
||||
"static",
|
||||
]),
|
||||
buildCommand: z.string().optional(),
|
||||
startCommand: z.string().optional(),
|
||||
autoDeploy: z.boolean().default(true),
|
||||
envVars: BluePrintEnvVarsSchema,
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export type BlueprintService = z.infer<typeof BlueprintServiceSchema>;
|
||||
|
||||
const BlueprintSchema = z.object({
|
||||
services: z.array(BlueprintServiceSchema),
|
||||
});
|
||||
+5
-3
@@ -2,9 +2,9 @@ import type { GitHubAppAuthorization } from ".prisma/client";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { getRepositoryFromMetadata } from "~/models/workflow.server";
|
||||
import type { CreateInstallationAccessTokenResponse } from "~/services/github/githubApp.server";
|
||||
import { getInstallationRepositories } from "~/services/github/githubApp.server";
|
||||
import { refreshInstallationAccessToken } from "~/services/github/refreshInstallationAccessToken.server";
|
||||
import type { CreateInstallationAccessTokenResponse } from "~/features/ee/projects/github/githubApp.server";
|
||||
import { getInstallationRepositories } from "~/features/ee/projects/github/githubApp.server";
|
||||
import { refreshInstallationAccessToken } from "~/features/ee/projects/github/refreshInstallationAccessToken.server";
|
||||
|
||||
export type InstallationRepository = NonNullable<
|
||||
CreateInstallationAccessTokenResponse["repositories"]
|
||||
@@ -13,6 +13,7 @@ export type InstallationRepository = NonNullable<
|
||||
export type RepositoryWithStatus = {
|
||||
repository: InstallationRepository;
|
||||
status: "relevant" | "unknown";
|
||||
appAuthorizationId: string;
|
||||
};
|
||||
|
||||
export class NewProjectPresenter {
|
||||
@@ -73,6 +74,7 @@ export class NewProjectPresenter {
|
||||
return {
|
||||
repository,
|
||||
status,
|
||||
appAuthorizationId: authorization.id,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
import { LIVE_ENVIRONMENT } from "~/consts";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { getEnvironmentForOrganization } from "~/models/runtimeEnvironment.server";
|
||||
import { WorkflowsPresenter } from "~/presenters/workflowsPresenter.server";
|
||||
|
||||
export class ProjectOverviewPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
async data(organizationSlug: string, projectId: string) {
|
||||
const liveEnvironment = await getEnvironmentForOrganization(
|
||||
organizationSlug,
|
||||
LIVE_ENVIRONMENT
|
||||
);
|
||||
|
||||
if (!liveEnvironment) {
|
||||
throw new Error("No live environment found");
|
||||
}
|
||||
|
||||
const workflowsPresenter = new WorkflowsPresenter(this.#prismaClient);
|
||||
|
||||
const workflows = await workflowsPresenter.data(
|
||||
{
|
||||
repositoryProject: {
|
||||
id: projectId,
|
||||
},
|
||||
},
|
||||
liveEnvironment.id
|
||||
);
|
||||
|
||||
const deployments = await this.#prismaClient.projectDeployment.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
take: 5,
|
||||
});
|
||||
|
||||
return {
|
||||
workflows,
|
||||
organizationSlug,
|
||||
deployments,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import type { UseDataFunctionReturn } from "remix-typedjson";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Container } from "~/components/layout/Container";
|
||||
import {
|
||||
ProjectSideMenu,
|
||||
SideMenuContainer,
|
||||
} from "~/components/navigation/SideMenu";
|
||||
import { prisma } from "~/db.server";
|
||||
import { hydrateObject, useMatchesData } from "~/utils";
|
||||
|
||||
export async function loader({ params }: LoaderArgs) {
|
||||
const { organizationSlug, projectP } = params;
|
||||
invariant(organizationSlug, "organizationSlug not found");
|
||||
invariant(projectP, "projectP not found");
|
||||
|
||||
const project = await prisma.repositoryProject.findFirstOrThrow({
|
||||
where: {
|
||||
id: projectP,
|
||||
organization: {
|
||||
slug: organizationSlug,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
currentDeployment: true,
|
||||
},
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
project,
|
||||
organizationSlug,
|
||||
});
|
||||
}
|
||||
|
||||
export default function ProjectLayout() {
|
||||
const { project, organizationSlug } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideMenuContainer>
|
||||
<ProjectSideMenu
|
||||
project={project}
|
||||
backPath={`/orgs/${organizationSlug}`}
|
||||
/>
|
||||
<Container>
|
||||
<Outlet />
|
||||
</Container>
|
||||
</SideMenuContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCurrentProject() {
|
||||
const routeMatch = useMatchesData(
|
||||
"routes/__app/orgs/$organizationSlug/projects/$projectP"
|
||||
);
|
||||
|
||||
if (!routeMatch || !routeMatch.data.project) {
|
||||
throw new Error("Calling useCurrentProject outside of a project route");
|
||||
}
|
||||
|
||||
const result = hydrateObject<UseDataFunctionReturn<typeof loader>["project"]>(
|
||||
routeMatch.data.project
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export type CurrentProject = ReturnType<typeof useCurrentProject>;
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { ProjectDeployment } from ".prisma/client";
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ClockIcon,
|
||||
CloudArrowUpIcon,
|
||||
CloudIcon,
|
||||
CubeTransparentIcon,
|
||||
ExclamationTriangleIcon,
|
||||
NoSymbolIcon,
|
||||
StopCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { useEffect } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { z } from "zod";
|
||||
import { IntlDate } from "~/components/IntlDate";
|
||||
import { List } from "~/components/layout/List";
|
||||
import { Panel } from "~/components/layout/Panel";
|
||||
import { PanelHeader } from "~/components/layout/PanelHeader";
|
||||
import { TertiaryA } from "~/components/primitives/Buttons";
|
||||
import { SubTitle } from "~/components/primitives/text/SubTitle";
|
||||
import { Title } from "~/components/primitives/text/Title";
|
||||
import { WorkflowList } from "~/components/workflows/workflowList";
|
||||
import { ProjectOverviewPresenter } from "~/features/ee/projects/presenters/projectOverviewPresenter.server";
|
||||
import { useCurrentProject } from "../$projectP";
|
||||
|
||||
export async function loader({ params }: LoaderArgs) {
|
||||
const { projectP, organizationSlug } = z
|
||||
.object({ projectP: z.string(), organizationSlug: z.string() })
|
||||
.parse(params);
|
||||
|
||||
const presenter = new ProjectOverviewPresenter();
|
||||
|
||||
return typedjson(await presenter.data(organizationSlug, projectP));
|
||||
}
|
||||
|
||||
export default function ProjectOverviewPage() {
|
||||
const project = useCurrentProject();
|
||||
const { workflows, organizationSlug, deployments } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
const events = useEventSource(`/resources/projects/${project.id}`, {
|
||||
event: "update",
|
||||
});
|
||||
const revalidator = useRevalidator();
|
||||
|
||||
useEffect(() => {
|
||||
if (events !== null) {
|
||||
revalidator.revalidate();
|
||||
}
|
||||
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
|
||||
}, [events]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
let Icon = ExclamationTriangleIcon;
|
||||
|
||||
switch (project.status) {
|
||||
case "PENDING": {
|
||||
Icon = ClockIcon;
|
||||
break;
|
||||
}
|
||||
case "BUILDING": {
|
||||
Icon = CubeTransparentIcon;
|
||||
break;
|
||||
}
|
||||
case "DEPLOYING": {
|
||||
Icon = CloudArrowUpIcon;
|
||||
break;
|
||||
}
|
||||
case "DEPLOYED": {
|
||||
Icon = CloudIcon;
|
||||
break;
|
||||
}
|
||||
case "ERROR": {
|
||||
Icon = ExclamationTriangleIcon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Overview</Title>
|
||||
<SubTitle>
|
||||
{project.name}#{project.branch}
|
||||
</SubTitle>
|
||||
<Panel>
|
||||
<PanelHeader
|
||||
icon={
|
||||
<div className="mr-1 h-6 w-6">
|
||||
<Icon />
|
||||
</div>
|
||||
}
|
||||
title={project.statusText ? project.statusText : "No status"}
|
||||
startedAt={null}
|
||||
finishedAt={null}
|
||||
/>
|
||||
</Panel>
|
||||
<div className="mt-6 max-w-4xl">
|
||||
<div className="relative rounded-lg bg-slate-850">
|
||||
<SubTitle>Workflows</SubTitle>
|
||||
|
||||
<WorkflowList
|
||||
className="relative z-50 !mb-0"
|
||||
workflows={workflows}
|
||||
currentOrganizationSlug={organizationSlug}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 max-w-4xl">
|
||||
<div className="relative rounded-lg bg-slate-850">
|
||||
<SubTitle>Latest deploys</SubTitle>
|
||||
|
||||
<List className="relative z-50 !mb-0">
|
||||
{deployments.map((deployment) => (
|
||||
<DeploymentListItem
|
||||
key={deployment.id}
|
||||
deployment={deployment}
|
||||
repo={project.name}
|
||||
isCurrentDeployment={
|
||||
deployment.id === project.currentDeployment?.id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DeploymentListItem({
|
||||
deployment,
|
||||
repo,
|
||||
isCurrentDeployment,
|
||||
}: {
|
||||
deployment: ProjectDeployment;
|
||||
repo: string;
|
||||
isCurrentDeployment: boolean;
|
||||
}) {
|
||||
let Icon = ExclamationTriangleIcon;
|
||||
|
||||
switch (deployment.status) {
|
||||
case "PENDING": {
|
||||
Icon = ClockIcon;
|
||||
break;
|
||||
}
|
||||
case "BUILDING": {
|
||||
Icon = CubeTransparentIcon;
|
||||
break;
|
||||
}
|
||||
case "DEPLOYING": {
|
||||
Icon = CloudArrowUpIcon;
|
||||
break;
|
||||
}
|
||||
case "DEPLOYED": {
|
||||
Icon = CloudIcon;
|
||||
break;
|
||||
}
|
||||
case "ERROR": {
|
||||
Icon = ExclamationTriangleIcon;
|
||||
break;
|
||||
}
|
||||
case "CANCELLED": {
|
||||
Icon = NoSymbolIcon;
|
||||
break;
|
||||
}
|
||||
case "STOPPED": {
|
||||
Icon = StopCircleIcon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let timestamp = deployment.createdAt;
|
||||
|
||||
if (deployment.stoppedAt) {
|
||||
timestamp = deployment.stoppedAt;
|
||||
} else if (deployment.buildFinishedAt) {
|
||||
timestamp = deployment.buildFinishedAt;
|
||||
} else if (deployment.buildStartedAt) {
|
||||
timestamp = deployment.buildStartedAt;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={isCurrentDeployment ? "border-2 border-green-300" : ""}>
|
||||
<div className="flex flex-col flex-wrap justify-between py-4 pl-4 pr-4 lg:flex-row lg:flex-nowrap lg:items-center">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="relative flex items-center">
|
||||
<div className="mr-4 h-20 w-20 flex-shrink-0 self-start rounded-md bg-slate-850 p-3">
|
||||
<Icon className="h-12 w-12 text-slate-500" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm font-medium text-slate-200">
|
||||
<TertiaryA
|
||||
href={`https://github.com/${repo}/commit/${deployment.commitHash}`}
|
||||
target="_blank"
|
||||
>
|
||||
Commit #{deployment.commitHash.substring(0, 7)}{" "}
|
||||
<ArrowTopRightOnSquareIcon className="h-4 w-4" />
|
||||
</TertiaryA>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-200">
|
||||
{deployment.commitMessage}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="text-sm font-medium text-slate-200">
|
||||
{deployment.status.toLocaleLowerCase()}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-slate-200">
|
||||
<IntlDate date={timestamp} timeZone="UTC" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { FolderIcon } from "@heroicons/react/20/solid";
|
||||
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/outline";
|
||||
import { Await, Form, useLoaderData, useTransition } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { defer } from "@remix-run/server-runtime";
|
||||
import classNames from "classnames";
|
||||
import { Suspense } from "react";
|
||||
import { redirect, typedjson, useTypedActionData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { OctoKitty } from "~/components/GitHubLoginButton";
|
||||
import { Container } from "~/components/layout/Container";
|
||||
import { Panel } from "~/components/layout/Panel";
|
||||
import { PanelWarning } from "~/components/layout/PanelWarning";
|
||||
import { StepNumber } from "~/components/onboarding/StepNumber";
|
||||
import {
|
||||
PrimaryButton,
|
||||
PrimaryLink,
|
||||
SecondaryLink,
|
||||
} from "~/components/primitives/Buttons";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { Body } from "~/components/primitives/text/Body";
|
||||
import { SubTitle } from "~/components/primitives/text/SubTitle";
|
||||
import { Title } from "~/components/primitives/text/Title";
|
||||
import { NewProjectPresenter } from "~/features/ee/projects/presenters/newProjectPresenter.server";
|
||||
import { CreateProjectService } from "~/features/ee/projects/services/createProject.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
invariant(params.organizationSlug, "Organization slug is required");
|
||||
|
||||
const presenter = new NewProjectPresenter();
|
||||
|
||||
return defer(await presenter.data(userId, params.organizationSlug));
|
||||
}
|
||||
|
||||
export async function action({ request, params }: LoaderArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
invariant(params.organizationSlug, "Organization slug is required");
|
||||
|
||||
const payload = Object.fromEntries(await request.formData());
|
||||
|
||||
const service = new CreateProjectService();
|
||||
|
||||
const validation = await service.validate(payload);
|
||||
|
||||
switch (validation.type) {
|
||||
case "payloadError":
|
||||
case "serviceDefinitionError": {
|
||||
return typedjson(validation, { status: 422 });
|
||||
}
|
||||
case "success": {
|
||||
const result = await service.call(
|
||||
userId,
|
||||
params.organizationSlug,
|
||||
validation.data,
|
||||
validation.serviceDefinition
|
||||
);
|
||||
|
||||
if (result.type === "serviceError") {
|
||||
return typedjson(result, { status: 422 });
|
||||
}
|
||||
|
||||
return redirect(
|
||||
`/orgs/${params.organizationSlug}/projects/${result.project.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { appAuthorizations, redirectTo, repositories } =
|
||||
useLoaderData<typeof loader>();
|
||||
|
||||
const actionData = useTypedActionData<typeof action>();
|
||||
const transition = useTransition();
|
||||
|
||||
const isSubmittingOrLoading =
|
||||
(transition.state === "submitting" &&
|
||||
transition.type === "actionSubmission") ||
|
||||
(transition.state === "loading" && transition.type === "actionRedirect");
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Deploy a GitHub repository</Title>
|
||||
<div className="grid w-full grid-cols-3 gap-8">
|
||||
<Form method="post" className="col-span-2 max-w-4xl">
|
||||
{appAuthorizations.length === 0 ? (
|
||||
<>
|
||||
<ConnectToGithub redirectTo={redirectTo} />
|
||||
<ConfigureGithub />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{(!isSubmittingOrLoading &&
|
||||
actionData?.type === "serviceDefinitionError") ||
|
||||
actionData?.type === "serviceError" ? (
|
||||
<PanelWarning
|
||||
message={actionData.message}
|
||||
className="mb-4"
|
||||
></PanelWarning>
|
||||
) : !isSubmittingOrLoading &&
|
||||
actionData?.type === "payloadError" ? (
|
||||
<PanelWarning
|
||||
message="Something went wrong with your request. Please try again."
|
||||
className="mb-4"
|
||||
></PanelWarning>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Panel className="!p-4">
|
||||
<div className="mb-3 grid grid-cols-2 gap-4">
|
||||
<InputGroup>
|
||||
<Label htmlFor="appAuthorizationId">
|
||||
Select a GitHub repo
|
||||
</Label>
|
||||
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Await
|
||||
errorElement={<p>Error loading repositories</p>}
|
||||
resolve={repositories}
|
||||
>
|
||||
{(repos) => (
|
||||
<ul>
|
||||
{repos.map((repo) => (
|
||||
<li
|
||||
key={repo.repository.id}
|
||||
className={classNames(
|
||||
"flex items-center justify-between gap-2",
|
||||
repo.status === "relevant"
|
||||
? "bg-blue-500 text-white"
|
||||
: "text-slate-400"
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={repo.repository.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{repo.repository.full_name}#
|
||||
{repo.repository.default_branch}
|
||||
</a>
|
||||
<span>
|
||||
{repo.repository.private ? (
|
||||
<LockClosedIcon className="h-4 w-4 text-white" />
|
||||
) : (
|
||||
<LockOpenIcon className="h-4 w-4 text-white" />
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<Form method="post">
|
||||
<input
|
||||
type="hidden"
|
||||
name="repoId"
|
||||
value={repo.repository.id}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="repoName"
|
||||
value={repo.repository.full_name}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="appAuthorizationId"
|
||||
value={repo.appAuthorizationId}
|
||||
/>
|
||||
|
||||
<PrimaryButton type="submit" size="regular">
|
||||
Select
|
||||
</PrimaryButton>
|
||||
</Form>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel className="mt-4">
|
||||
<div className="mb-3 grid grid-cols-2 gap-4">
|
||||
{appAuthorizations.map((app) => (
|
||||
<SecondaryLink
|
||||
to={`/apps/github?redirectTo=${encodeURIComponent(
|
||||
redirectTo
|
||||
)}&authorizationId=${app.id}`}
|
||||
reloadDocument
|
||||
key={app.id}
|
||||
>
|
||||
Configure {app.accountName}
|
||||
</SecondaryLink>
|
||||
))}
|
||||
|
||||
<PrimaryLink
|
||||
size="large"
|
||||
to={`/apps/github?redirectTo=${encodeURIComponent(
|
||||
redirectTo
|
||||
)}`}
|
||||
>
|
||||
<OctoKitty className="mr-1 h-5 w-5" />
|
||||
Add another account
|
||||
</PrimaryLink>
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectToGithub({ redirectTo }: { redirectTo: string }) {
|
||||
return (
|
||||
<>
|
||||
<SubTitle className="flex items-center">
|
||||
<StepNumber active stepNumber="1" />
|
||||
Grant GitHub repo access to get started
|
||||
</SubTitle>
|
||||
<Panel className="mb-6 flex h-56 flex-col items-center justify-center gap-4">
|
||||
<PrimaryLink
|
||||
size="large"
|
||||
to={`/apps/github?redirectTo=${encodeURIComponent(redirectTo)}`}
|
||||
>
|
||||
<OctoKitty className="mr-1 h-5 w-5" />
|
||||
Grant access
|
||||
</PrimaryLink>
|
||||
<Body size="extra-small" className="flex items-center text-slate-400">
|
||||
To deploy a new project you need to authorize our GitHub app.{" "}
|
||||
<a
|
||||
href="https://docs.trigger.dev/faq#why-do-we-ask-for-github-access"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ml-1 underline decoration-slate-500 underline-offset-2 transition hover:cursor-pointer hover:text-slate-300"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</Body>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigureGithub() {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6">
|
||||
<SubTitle className="flex items-center">
|
||||
<StepNumber stepNumber="2" />
|
||||
Create your GitHub repository from a template
|
||||
</SubTitle>
|
||||
<Panel className="flex h-56 w-full max-w-4xl items-center justify-center gap-6">
|
||||
<OctoKitty className="h-10 w-10 text-slate-600" />
|
||||
<div className="h-[1px] w-16 border border-dashed border-slate-600"></div>
|
||||
<FolderIcon className="h-10 w-10 text-slate-600" />
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { randomUUID } from "crypto";
|
||||
import { eventStream } from "remix-utils";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export async function loader({ request, params }: LoaderArgs) {
|
||||
await requireUserId(request);
|
||||
|
||||
const { id } = z.object({ id: z.string() }).parse(params);
|
||||
|
||||
const project = await findProjectForUpdates(id);
|
||||
|
||||
if (!project) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
let lastUpdatedAt: number = project.updatedAt.getTime();
|
||||
let lastDeploymentId: string | null = project.deployments[0]?.id || null;
|
||||
let workflowCount = project._count.workflows;
|
||||
|
||||
return eventStream(request.signal, (send) => {
|
||||
const pinger = setInterval(() => {
|
||||
send({ event: "ping", data: new Date().toISOString() });
|
||||
}, 1000);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
// Get the updatedAt date from the projects database, and send it to the client if it's different from the last one
|
||||
findProjectForUpdates(id).then((project) => {
|
||||
if (project) {
|
||||
if (lastUpdatedAt !== project.updatedAt.getTime()) {
|
||||
send({ event: "update", data: project.updatedAt.toISOString() });
|
||||
} else if (lastDeploymentId !== project.deployments[0]?.id) {
|
||||
send({ event: "update", data: randomUUID() });
|
||||
} else if (workflowCount !== project._count.workflows) {
|
||||
send({ event: "update", data: randomUUID() });
|
||||
}
|
||||
|
||||
workflowCount = project._count.workflows;
|
||||
lastDeploymentId = project.deployments[0]?.id || null;
|
||||
lastUpdatedAt = project.updatedAt.getTime();
|
||||
}
|
||||
});
|
||||
}, 348);
|
||||
|
||||
return function clear() {
|
||||
clearInterval(pinger);
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function findProjectForUpdates(id: string) {
|
||||
return prisma.repositoryProject.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: {
|
||||
updatedAt: true,
|
||||
deployments: {
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
workflows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { z } from "zod";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { StartDeployment } from "./startDeployment.server";
|
||||
|
||||
const PayloadSchema = z.object({
|
||||
buildId: z.string(),
|
||||
imageId: z.string(),
|
||||
});
|
||||
|
||||
export class BuildComplete {
|
||||
#prismaClient: PrismaClient;
|
||||
#startDeployment = new StartDeployment();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public validate(payload: unknown) {
|
||||
return PayloadSchema.safeParse(payload);
|
||||
}
|
||||
|
||||
public async call(payload: z.infer<typeof PayloadSchema>) {
|
||||
console.log(`Build complete: ${payload.buildId} -> ${payload.imageId}`);
|
||||
|
||||
const deployment = await this.#updateDeployment(
|
||||
payload.buildId,
|
||||
payload.imageId
|
||||
);
|
||||
|
||||
if (!deployment) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only continue if this deployment is building
|
||||
if (deployment.status !== "BUILDING") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.#startDeployment.call({
|
||||
deployment,
|
||||
project: deployment.project,
|
||||
environment: deployment.environment,
|
||||
});
|
||||
}
|
||||
|
||||
async #updateDeployment(buildId: string, imageId: string) {
|
||||
try {
|
||||
return await this.#prismaClient.projectDeployment.update({
|
||||
where: {
|
||||
buildId,
|
||||
},
|
||||
data: {
|
||||
imageId,
|
||||
},
|
||||
include: {
|
||||
environment: true,
|
||||
project: {
|
||||
include: {
|
||||
currentDeployment: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error &&
|
||||
"code" in error &&
|
||||
error.code === "P2025"
|
||||
) {
|
||||
// Record to update not found
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { CakeworkApiError } from "@cakework/client/dist";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { cakework } from "../cakework.server";
|
||||
|
||||
export class CleanupDeployment {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
async call(id: string) {
|
||||
const deployment = await this.#prismaClient.projectDeployment.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deployment.status !== "DEPLOYED") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deployment.project.currentDeploymentId === id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!deployment.vmIdentifier) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`Stopping VM: ${deployment.vmIdentifier} for deployment ${id}`
|
||||
);
|
||||
|
||||
await cakework.stopVm(deployment.vmIdentifier);
|
||||
|
||||
console.log(
|
||||
`Stopped VM: ${deployment.vmIdentifier} for deployment ${id}`
|
||||
);
|
||||
|
||||
await this.#prismaClient.projectDeployment.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: "STOPPED",
|
||||
stoppedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof CakeworkApiError) {
|
||||
console.log(
|
||||
`Failed to stop VM: ${deployment.vmIdentifier} for deployment ${id}: ${error.statusCode} ${error.message}`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { z } from "zod";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import type { BlueprintService } from "~/features/ee/projects/models/repositoryProject.server";
|
||||
import {
|
||||
repositoryProjectReadyToDeploy,
|
||||
serviceDefinitionFromRepository,
|
||||
} from "~/features/ee/projects/models/repositoryProject.server";
|
||||
import { taskQueue } from "~/services/messageBroker.server";
|
||||
|
||||
const FormSchema = z.object({
|
||||
repoId: z.string(),
|
||||
repoName: z.string(),
|
||||
appAuthorizationId: z.string(),
|
||||
});
|
||||
|
||||
export type CreateProjectValidationResult =
|
||||
| {
|
||||
type: "payloadError";
|
||||
errors: z.ZodIssue[];
|
||||
}
|
||||
| {
|
||||
type: "serviceDefinitionError";
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: "success";
|
||||
data: z.infer<typeof FormSchema>;
|
||||
serviceDefinition: BlueprintService;
|
||||
};
|
||||
|
||||
export class CreateProjectService {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(
|
||||
userId: string,
|
||||
organizationSlug: string,
|
||||
data: z.infer<typeof FormSchema>,
|
||||
serviceDefinition: BlueprintService
|
||||
) {
|
||||
try {
|
||||
const project = await this.#prismaClient.repositoryProject.create({
|
||||
data: {
|
||||
name: data.repoName,
|
||||
url: `https://github.com/${data.repoName}`,
|
||||
authorization: {
|
||||
connect: {
|
||||
id: data.appAuthorizationId,
|
||||
},
|
||||
},
|
||||
branch: "main",
|
||||
organization: {
|
||||
connect: {
|
||||
slug: organizationSlug,
|
||||
},
|
||||
},
|
||||
buildCommand: serviceDefinition.buildCommand ?? "npm run build",
|
||||
startCommand: serviceDefinition.startCommand ?? "npm run start",
|
||||
envVars: serviceDefinition.envVars,
|
||||
},
|
||||
});
|
||||
|
||||
if (repositoryProjectReadyToDeploy(project)) {
|
||||
await taskQueue.publish("START_INITIAL_PROJECT_DEPLOYMENT", {
|
||||
id: project.id,
|
||||
});
|
||||
}
|
||||
|
||||
return { type: "success" as const, project };
|
||||
} catch (error) {
|
||||
// Handle Prisma unique constraint error (name must be unique)
|
||||
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error &&
|
||||
"code" in error &&
|
||||
error.code === "P2002"
|
||||
) {
|
||||
return {
|
||||
type: "serviceError" as const,
|
||||
message:
|
||||
"Cannot deploy this repository because it is already being used.",
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async validate(
|
||||
payload: unknown
|
||||
): Promise<CreateProjectValidationResult> {
|
||||
const payloadValidation = FormSchema.safeParse(payload);
|
||||
|
||||
if (!payloadValidation.success) {
|
||||
return {
|
||||
type: "payloadError" as const,
|
||||
errors: payloadValidation.error.issues,
|
||||
};
|
||||
}
|
||||
|
||||
const serviceDefinition = await serviceDefinitionFromRepository(
|
||||
payloadValidation.data.appAuthorizationId,
|
||||
payloadValidation.data.repoName
|
||||
);
|
||||
|
||||
if (!serviceDefinition) {
|
||||
return {
|
||||
type: "serviceDefinitionError" as const,
|
||||
message:
|
||||
"Could not find a service definition in the selected repository. We only support repositories based on our templates for now.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.#validateServiceMetadata(serviceDefinition)) {
|
||||
return {
|
||||
type: "serviceDefinitionError" as const,
|
||||
message:
|
||||
"Could not find a service definition in the selected repository. We only support repositories based on our templates for now.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "success" as const,
|
||||
data: payloadValidation.data,
|
||||
serviceDefinition,
|
||||
};
|
||||
}
|
||||
|
||||
// Make sure the service metadata is valid
|
||||
// env = node
|
||||
// type = worker
|
||||
// envVar with key TRIGGER_API_KEY
|
||||
#validateServiceMetadata(serviceMetadata: BlueprintService) {
|
||||
return (
|
||||
serviceMetadata.env === "node" &&
|
||||
serviceMetadata.type === "worker" &&
|
||||
serviceMetadata.envVars.find((envVar) => envVar.key === "TRIGGER_API_KEY")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { RepositoryProject, RuntimeEnvironment } from ".prisma/client";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { cakework } from "../cakework.server";
|
||||
import { taskQueue } from "~/services/messageBroker.server";
|
||||
import type { GitHubAppAuthorizationWithValidToken } from "../github/refreshInstallationAccessToken.server";
|
||||
import type { GitHubCommit } from "../github/githubApp.server";
|
||||
|
||||
export type CreateProjectDeploymentOptions = {
|
||||
project: RepositoryProject;
|
||||
authorization: GitHubAppAuthorizationWithValidToken;
|
||||
environment: RuntimeEnvironment;
|
||||
commit: GitHubCommit;
|
||||
};
|
||||
|
||||
export class CreateProjectDeployment {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
project,
|
||||
environment,
|
||||
authorization,
|
||||
commit,
|
||||
}: CreateProjectDeploymentOptions) {
|
||||
const dockerfile = formatFileContents(`
|
||||
FROM node:18-bullseye-slim
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN ${project.buildCommand}
|
||||
COPY . .
|
||||
CMD [${project.startCommand
|
||||
.split(" ")
|
||||
.map((s) => `"${s}"`)
|
||||
.join(", ")}]
|
||||
`);
|
||||
|
||||
const dockerIgnore = formatFileContents(`
|
||||
node_modules
|
||||
`);
|
||||
|
||||
console.log(
|
||||
`Building image for ${project.name} with token ${authorization.installationAccessToken}`
|
||||
);
|
||||
|
||||
const build = await cakework.buildImageFromGithub({
|
||||
dockerfile: dockerfile,
|
||||
dockerignore: dockerIgnore,
|
||||
token: authorization.installationAccessToken,
|
||||
repository: project.name,
|
||||
branch: project.branch,
|
||||
});
|
||||
|
||||
console.log(`Build started for ${project.name} with id ${build.buildId}`);
|
||||
|
||||
// Create the deployment
|
||||
// Setting the buildStartAt because even though this is a PENDING deployment,
|
||||
// we have already started to build it with Cakework (it can still end up not getting deployed if this deployment is cancelled)
|
||||
const deployment = await this.#prismaClient.projectDeployment.create({
|
||||
data: {
|
||||
buildId: build.buildId,
|
||||
buildStartedAt: new Date(),
|
||||
project: {
|
||||
connect: {
|
||||
id: project.id,
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environment.id,
|
||||
},
|
||||
},
|
||||
status: "PENDING",
|
||||
branch: project.branch,
|
||||
commitHash: commit.sha,
|
||||
commitMessage: commit.commit.message,
|
||||
committer: getCommitAuthor(commit),
|
||||
dockerfile,
|
||||
dockerIgnore,
|
||||
},
|
||||
});
|
||||
|
||||
await taskQueue.publish("PROJECT_DEPLOYMENT_CREATED", {
|
||||
id: deployment.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getCommitAuthor(commit: GitHubCommit) {
|
||||
if (commit.commit.author && commit.commit.author.name) {
|
||||
return commit.commit.author.name;
|
||||
}
|
||||
|
||||
if (commit.committer && commit.committer.login) {
|
||||
return commit.committer.login;
|
||||
}
|
||||
|
||||
if (commit.author && commit.author.login) {
|
||||
return commit.author.login;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// Remove newlines at the beginning of the file, and remove any leading whitespace on each line (make sure not to remove any other whitespace)
|
||||
// For example, the following input:
|
||||
//
|
||||
// FROM node:bullseye-slim
|
||||
// WORKDIR /app
|
||||
// COPY package*.json ./
|
||||
// RUN npm install && npm run build
|
||||
// COPY . .
|
||||
// CMD ["node", "dist/index.js"]
|
||||
//
|
||||
// Would be formatted to:
|
||||
// FROM node:bullseye-slim
|
||||
// WORKDIR /app
|
||||
// COPY package*.json ./
|
||||
// RUN npm install && npm run build
|
||||
// COPY . .
|
||||
// CMD ["node", "dist/index.js"]
|
||||
function formatFileContents(contents: string) {
|
||||
return contents
|
||||
.trimStart()
|
||||
.split("\n")
|
||||
.map((line) => line.trimStart())
|
||||
.join("\n");
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { statusTextForBuilding } from "~/features/ee/projects/models/repositoryProject.server";
|
||||
|
||||
export class DeploymentCreated {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call(id: string) {
|
||||
const deployment = await this.#prismaClient.projectDeployment.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the RepositoryProjec status is:
|
||||
// - PENDING: Update the project to be building
|
||||
// - DEPLOYED: Update the project to be building
|
||||
// - BUILDING: Do nothing
|
||||
// - ERROR: Update the project to be building
|
||||
// - DISABLED: Do nothing
|
||||
// - DEPLOYING: Do nothing
|
||||
|
||||
switch (deployment.project.status) {
|
||||
case "PENDING":
|
||||
case "DEPLOYED":
|
||||
case "ERROR": {
|
||||
// Set the project as "building"
|
||||
// will transition to "deploying" when the build is complete (see: buildComplete.server.ts)
|
||||
console.log(
|
||||
`Setting project ${deployment.project.id} and deployment ${deployment.id} to BUILDING`
|
||||
);
|
||||
|
||||
await this.#prismaClient.repositoryProject.update({
|
||||
where: {
|
||||
id: deployment.project.id,
|
||||
},
|
||||
data: {
|
||||
status: "BUILDING",
|
||||
statusText: statusTextForBuilding(deployment, deployment.project),
|
||||
},
|
||||
});
|
||||
|
||||
await this.#prismaClient.projectDeployment.update({
|
||||
where: {
|
||||
id: deployment.id,
|
||||
},
|
||||
data: {
|
||||
status: "BUILDING",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We also need to check if there are any other PENDING deployments for this project
|
||||
// and if so we should cancel them (because we have a newer push)
|
||||
await this.#prismaClient.projectDeployment.updateMany({
|
||||
where: {
|
||||
projectId: deployment.project.id,
|
||||
status: "PENDING",
|
||||
id: {
|
||||
not: deployment.id,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: "CANCELLED",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { LIVE_ENVIRONMENT } from "~/consts";
|
||||
import { findProjectById } from "~/features/ee/projects/models/repositoryProject.server";
|
||||
import { getCommit } from "../github/githubApp.server";
|
||||
import { refreshInstallationAccessToken } from "../github/refreshInstallationAccessToken.server";
|
||||
import { CreateProjectDeployment } from "./createProjectDeployment.server";
|
||||
|
||||
export class InitialProjectDeployment {
|
||||
#createProjectDeployment = new CreateProjectDeployment();
|
||||
|
||||
public async call(projectId: string) {
|
||||
const project = await findProjectById(projectId);
|
||||
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.status !== "PENDING") {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = project.organization.environments.find(
|
||||
(environment) => environment.slug === LIVE_ENVIRONMENT
|
||||
);
|
||||
|
||||
if (!environment) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the latest commit from the "main" branch in the repo
|
||||
const appAuthorization = await refreshInstallationAccessToken(
|
||||
project.authorizationId
|
||||
);
|
||||
|
||||
const latestCommit = await getCommit(
|
||||
appAuthorization.installationAccessToken,
|
||||
project.name,
|
||||
project.branch
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Creating deployment for latest commit for ${project.name}: ${latestCommit.sha}`
|
||||
);
|
||||
|
||||
await this.#createProjectDeployment.call({
|
||||
project,
|
||||
environment,
|
||||
authorization: appAuthorization,
|
||||
commit: latestCommit,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { LIVE_ENVIRONMENT } from "~/consts";
|
||||
import { findProjectByRepo } from "~/features/ee/projects/models/repositoryProject.server";
|
||||
import { getCommit } from "../github/githubApp.server";
|
||||
import { refreshInstallationAccessToken } from "../github/refreshInstallationAccessToken.server";
|
||||
import { CreateProjectDeployment } from "./createProjectDeployment.server";
|
||||
|
||||
export class ReceiveRepositoryPush {
|
||||
#createProjectDeployment = new CreateProjectDeployment();
|
||||
|
||||
public async call(data: {
|
||||
branch: string;
|
||||
commitSha: string;
|
||||
repository: string;
|
||||
}) {
|
||||
console.log(
|
||||
`Received push for ${data.repository} on ${data.branch}: ${data.commitSha}`
|
||||
);
|
||||
|
||||
const project = await findProjectByRepo(data.repository);
|
||||
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.branch !== data.branch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = project.organization.environments.find(
|
||||
(environment) => environment.slug === LIVE_ENVIRONMENT
|
||||
);
|
||||
|
||||
if (!environment) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the latest commit from the "main" branch in the repo
|
||||
const appAuthorization = await refreshInstallationAccessToken(
|
||||
project.authorizationId
|
||||
);
|
||||
|
||||
const commit = await getCommit(
|
||||
appAuthorization.installationAccessToken,
|
||||
project.name,
|
||||
data.commitSha
|
||||
);
|
||||
|
||||
console.log(`Received commit for ${project.name}: ${commit.sha}`);
|
||||
|
||||
await this.#createProjectDeployment.call({
|
||||
project,
|
||||
environment,
|
||||
authorization: appAuthorization,
|
||||
commit,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import type {
|
||||
ProjectDeployment,
|
||||
RepositoryProject,
|
||||
RuntimeEnvironment,
|
||||
} from ".prisma/client";
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import {
|
||||
buildEnvVars,
|
||||
statusTextForDeployed,
|
||||
statusTextForDeploying,
|
||||
} from "~/features/ee/projects/models/repositoryProject.server";
|
||||
import { taskQueue } from "~/services/messageBroker.server";
|
||||
import { cakework } from "../cakework.server";
|
||||
|
||||
export type StartDeploymentOptions = {
|
||||
deployment: ProjectDeployment;
|
||||
project: RepositoryProject;
|
||||
environment: RuntimeEnvironment;
|
||||
};
|
||||
|
||||
export class StartDeployment {
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
public async call({
|
||||
deployment,
|
||||
project,
|
||||
environment,
|
||||
}: StartDeploymentOptions) {
|
||||
if (!deployment.imageId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Starting deployment ${deployment.id} for ${project.id}`);
|
||||
|
||||
// Update the project and deployment status to deploying
|
||||
await this.#prismaClient.repositoryProject.update({
|
||||
where: {
|
||||
id: project.id,
|
||||
},
|
||||
data: {
|
||||
status: "DEPLOYING",
|
||||
statusText: statusTextForDeploying(deployment, project),
|
||||
},
|
||||
});
|
||||
|
||||
await this.#prismaClient.projectDeployment.update({
|
||||
where: {
|
||||
id: deployment.id,
|
||||
},
|
||||
data: {
|
||||
status: "DEPLOYING",
|
||||
},
|
||||
});
|
||||
|
||||
const vmStart = performance.now();
|
||||
|
||||
const envVars = buildEnvVars(deployment, project, environment);
|
||||
|
||||
console.log(
|
||||
`Starting VM for ${deployment.id} with envVars: ${JSON.stringify(
|
||||
envVars
|
||||
)}`
|
||||
);
|
||||
|
||||
try {
|
||||
const vm = await cakework.startVm({
|
||||
imageId: deployment.imageId,
|
||||
cpu: 1,
|
||||
memory: 256,
|
||||
envVars,
|
||||
});
|
||||
|
||||
const vmEnd = performance.now();
|
||||
|
||||
console.log(
|
||||
`Started VM for ${deployment.id} in ${(vmEnd - vmStart).toFixed(2)}ms`
|
||||
);
|
||||
|
||||
// Update the deployment with the VM
|
||||
await this.#prismaClient.projectDeployment.update({
|
||||
where: {
|
||||
id: deployment.id,
|
||||
},
|
||||
data: {
|
||||
vmIdentifier: vm.id,
|
||||
status: "DEPLOYED",
|
||||
buildFinishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Update the project
|
||||
await this.#prismaClient.repositoryProject.update({
|
||||
where: {
|
||||
id: project.id,
|
||||
},
|
||||
data: {
|
||||
status: "DEPLOYED",
|
||||
currentDeploymentId: deployment.id,
|
||||
statusText: statusTextForDeployed(deployment, project),
|
||||
},
|
||||
});
|
||||
|
||||
// If there are any pending deployments, we can start them now
|
||||
await taskQueue.publish("DEPLOYMENT_DEPLOYED", {
|
||||
id: deployment.id,
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
// Make sure to stop the previous deployment
|
||||
if (project.currentDeploymentId) {
|
||||
await taskQueue.publish("CLEANUP_DEPLOYMENT", {
|
||||
id: project.currentDeploymentId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Update the deployment to be errored
|
||||
await this.#prismaClient.projectDeployment.update({
|
||||
where: {
|
||||
id: deployment.id,
|
||||
},
|
||||
data: {
|
||||
status: "ERROR",
|
||||
error: JSON.parse(JSON.stringify(error)),
|
||||
},
|
||||
});
|
||||
|
||||
// Update the project to be errored
|
||||
await this.#prismaClient.repositoryProject.update({
|
||||
where: {
|
||||
id: project.id,
|
||||
},
|
||||
data: {
|
||||
status: "ERROR",
|
||||
statusText: `Error building project: ${
|
||||
error instanceof Error ? error.message : error
|
||||
}`,
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { PrismaClient } from "~/db.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { StartDeployment } from "./startDeployment.server";
|
||||
|
||||
export class StartPendingDeployment {
|
||||
#prismaClient: PrismaClient;
|
||||
#startDeployment = new StartDeployment();
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
async call(id: string) {
|
||||
const project = await this.#prismaClient.repositoryProject.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (project.status !== "DEPLOYED") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find latest pending deployment with an imageIdentifier
|
||||
const deployment = await this.#prismaClient.projectDeployment.findFirst({
|
||||
where: {
|
||||
projectId: project.id,
|
||||
status: "PENDING",
|
||||
imageId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: {
|
||||
environment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.#startDeployment.call({
|
||||
deployment,
|
||||
project,
|
||||
environment: deployment.environment,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,30 @@ export async function findEnvironmentByApiKey(apiKey: string) {
|
||||
return environment;
|
||||
}
|
||||
|
||||
export async function getEnvironmentForOrganization(
|
||||
organizationSlug: string,
|
||||
slug: string
|
||||
) {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
},
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = organization.environments.find(
|
||||
(environment) => environment.slug === slug
|
||||
);
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
export const { commitSession, getSession } = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__environment",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { Container } from "~/components/layout/Container";
|
||||
import { Title } from "~/components/primitives/text/Title";
|
||||
|
||||
export default function NewProjectPage() {
|
||||
return (
|
||||
<Container>
|
||||
<Title>Create a new project</Title>
|
||||
<Outlet />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import { FolderIcon } from "@heroicons/react/20/solid";
|
||||
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/outline";
|
||||
import { Await, Form, useLoaderData } from "@remix-run/react";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { defer } from "@remix-run/server-runtime";
|
||||
import classNames from "classnames";
|
||||
import { Suspense } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { OctoKitty } from "~/components/GitHubLoginButton";
|
||||
import { Panel } from "~/components/layout/Panel";
|
||||
import { StepNumber } from "~/components/onboarding/StepNumber";
|
||||
import {
|
||||
PrimaryButton,
|
||||
PrimaryLink,
|
||||
SecondaryLink,
|
||||
} from "~/components/primitives/Buttons";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { Body } from "~/components/primitives/text/Body";
|
||||
import { SubTitle } from "~/components/primitives/text/SubTitle";
|
||||
import { NewProjectPresenter } from "~/presenters/newProjectPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
invariant(params.organizationSlug, "Organization slug is required");
|
||||
|
||||
const presenter = new NewProjectPresenter();
|
||||
|
||||
return defer(await presenter.data(userId, params.organizationSlug));
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { appAuthorizations, redirectTo, repositories } =
|
||||
useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="grid w-full grid-cols-3 gap-8">
|
||||
<Form method="post" className="col-span-2 max-w-4xl">
|
||||
{appAuthorizations.length === 0 ? (
|
||||
<>
|
||||
<ConnectToGithub redirectTo={redirectTo} />
|
||||
<ConfigureGithub />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Panel className="!p-4">
|
||||
<div className="mb-3 grid grid-cols-2 gap-4">
|
||||
<InputGroup>
|
||||
<Label htmlFor="appAuthorizationId">
|
||||
Select a GitHub repo
|
||||
</Label>
|
||||
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Await
|
||||
errorElement={<p>Error loading repositories</p>}
|
||||
resolve={repositories}
|
||||
>
|
||||
{(repos) => (
|
||||
<ul>
|
||||
{repos.map((repo) => (
|
||||
<li
|
||||
key={repo.repository.id}
|
||||
className={classNames(
|
||||
"flex items-center justify-between gap-2",
|
||||
repo.status === "relevant"
|
||||
? "bg-blue-500 text-white"
|
||||
: "text-slate-400"
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href={repo.repository.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{repo.repository.full_name}#
|
||||
{repo.repository.default_branch}
|
||||
</a>
|
||||
<span>
|
||||
{repo.repository.private ? (
|
||||
<LockClosedIcon className="h-4 w-4 text-white" />
|
||||
) : (
|
||||
<LockOpenIcon className="h-4 w-4 text-white" />
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<PrimaryButton size="regular">
|
||||
Deploy
|
||||
</PrimaryButton>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</Panel>
|
||||
<Panel className="mt-4">
|
||||
<div className="mb-3 grid grid-cols-2 gap-4">
|
||||
{appAuthorizations.map((app) => (
|
||||
<SecondaryLink
|
||||
to={`/apps/github?redirectTo=${encodeURIComponent(
|
||||
redirectTo
|
||||
)}&authorizationId=${app.id}`}
|
||||
reloadDocument
|
||||
key={app.id}
|
||||
>
|
||||
Configure {app.accountName}
|
||||
</SecondaryLink>
|
||||
))}
|
||||
|
||||
<PrimaryLink
|
||||
size="large"
|
||||
to={`/apps/github?redirectTo=${encodeURIComponent(
|
||||
redirectTo
|
||||
)}`}
|
||||
>
|
||||
<OctoKitty className="mr-1 h-5 w-5" />
|
||||
Add another account
|
||||
</PrimaryLink>
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectToGithub({ redirectTo }: { redirectTo: string }) {
|
||||
return (
|
||||
<>
|
||||
<SubTitle className="flex items-center">
|
||||
<StepNumber active stepNumber="1" />
|
||||
Grant GitHub repo access to get started
|
||||
</SubTitle>
|
||||
<Panel className="mb-6 flex h-56 flex-col items-center justify-center gap-4">
|
||||
<PrimaryLink
|
||||
size="large"
|
||||
to={`/apps/github?redirectTo=${encodeURIComponent(redirectTo)}`}
|
||||
>
|
||||
<OctoKitty className="mr-1 h-5 w-5" />
|
||||
Grant access
|
||||
</PrimaryLink>
|
||||
<Body size="extra-small" className="flex items-center text-slate-400">
|
||||
To deploy a new project you need to authorize our GitHub app.{" "}
|
||||
<a
|
||||
href="https://docs.trigger.dev/faq#why-do-we-ask-for-github-access"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="ml-1 underline decoration-slate-500 underline-offset-2 transition hover:cursor-pointer hover:text-slate-300"
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</Body>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigureGithub() {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6">
|
||||
<SubTitle className="flex items-center">
|
||||
<StepNumber stepNumber="2" />
|
||||
Create your GitHub repository from a template
|
||||
</SubTitle>
|
||||
<Panel className="flex h-56 w-full max-w-4xl items-center justify-center gap-6">
|
||||
<OctoKitty className="h-10 w-10 text-slate-600" />
|
||||
<div className="h-[1px] w-16 border border-dashed border-slate-600"></div>
|
||||
<FolderIcon className="h-10 w-10 text-slate-600" />
|
||||
</Panel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "~/features/ee/projects/routes/select-repo";
|
||||
export { default } from "~/features/ee/projects/routes/select-repo";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "~/features/ee/projects/routes/$projectP";
|
||||
export { default } from "~/features/ee/projects/routes/$projectP";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "~/features/ee/projects/routes/$projectP/index";
|
||||
export { default } from "~/features/ee/projects/routes/$projectP/index";
|
||||
+2
-14
@@ -1,20 +1,13 @@
|
||||
import { EventRule } from ".prisma/client";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { BeakerIcon, CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
ArrowsRightLeftIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
Cog6ToothIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
typedjson,
|
||||
UseDataFunctionReturn,
|
||||
useTypedLoaderData,
|
||||
} from "remix-typedjson";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ApiLogoIcon } from "~/components/code/ApiLogoIcon";
|
||||
import CodeBlock from "~/components/code/CodeBlock";
|
||||
@@ -26,12 +19,7 @@ import { Panel } from "~/components/layout/Panel";
|
||||
import { PanelHeader } from "~/components/layout/PanelHeader";
|
||||
import { PanelInfo } from "~/components/layout/PanelInfo";
|
||||
import { PanelWarning } from "~/components/layout/PanelWarning";
|
||||
import {
|
||||
PrimaryLink,
|
||||
SecondaryLink,
|
||||
TertiaryLink,
|
||||
} from "~/components/primitives/Buttons";
|
||||
import { PlugIcon } from "~/components/primitives/IconPlug";
|
||||
import { SecondaryLink, TertiaryLink } from "~/components/primitives/Buttons";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { Body } from "~/components/primitives/text/Body";
|
||||
import { Header2 } from "~/components/primitives/text/Headers";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { taskQueue } from "~/services/messageBroker.server";
|
||||
import { BuildComplete } from "~/features/ee/projects/services/buildComplete.server";
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
const payload = await request.json();
|
||||
|
||||
const service = new BuildComplete();
|
||||
|
||||
const validation = service.validate(payload);
|
||||
|
||||
if (!validation.success) {
|
||||
return new Response(JSON.stringify(validation.error), {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
await taskQueue.publish("DEPLOYMENT_BUILD_COMPLETE", validation.data);
|
||||
|
||||
return new Response("OK", {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ActionArgs } from "@remix-run/server-runtime";
|
||||
import { verifyAndReceiveWebhook } from "~/services/github/githubApp.server";
|
||||
import { verifyAndReceiveWebhook } from "~/features/ee/projects/github/githubApp.server";
|
||||
|
||||
export async function action({ request }: ActionArgs) {
|
||||
return verifyAndReceiveWebhook(request);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { AppInstallationCallback } from "~/services/github/appInstallationCallback.server";
|
||||
import { AppInstallationCallback } from "~/features/ee/projects/github/appInstallationCallback.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
const ParamSchema = z.object({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { StartAppInstallation } from "~/services/github/startAppInstallation.server";
|
||||
import { StartAppInstallation } from "~/features/ee/projects/github/startAppInstallation.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
const SearchParamsSchema = z.object({
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { loader } from "~/features/ee/projects/routes/stream";
|
||||
@@ -50,6 +50,12 @@ import { RegisterExternalSource } from "./externalSources/registerExternalSource
|
||||
import { CreateFetchRequest } from "./fetches/createFetchRequest.server";
|
||||
import { PerformFetchRequest } from "./fetches/performFetchRequest.server";
|
||||
import { StartFetchRequest } from "./fetches/startFetchRequest.server";
|
||||
import { BuildComplete } from "../features/ee/projects/services/buildComplete.server";
|
||||
import { CleanupDeployment } from "../features/ee/projects/services/cleanupDeployment.server";
|
||||
import { DeploymentCreated } from "../features/ee/projects/services/deploymentCreated.server";
|
||||
import { InitialProjectDeployment } from "../features/ee/projects/services/initialProjectDeployment.server";
|
||||
import { ReceiveRepositoryPush } from "../features/ee/projects/services/receiveRepositoryPush.server";
|
||||
import { StartPendingDeployment } from "../features/ee/projects/services/startPendingDeployment.server";
|
||||
import type { PulsarClient } from "./pulsarClient.server";
|
||||
import { createPulsarClient } from "./pulsarClient.server";
|
||||
import { CreateIntegrationRequest } from "./requests/createIntegrationRequest.server";
|
||||
@@ -504,6 +510,34 @@ const taskQueueCatalog = {
|
||||
data: z.object({ id: z.string() }),
|
||||
properties: z.object({}),
|
||||
},
|
||||
START_INITIAL_PROJECT_DEPLOYMENT: {
|
||||
data: z.object({ id: z.string() }),
|
||||
properties: z.object({}),
|
||||
},
|
||||
DEPLOYMENT_BUILD_COMPLETE: {
|
||||
data: z.object({ buildId: z.string(), imageId: z.string() }),
|
||||
properties: z.object({}),
|
||||
},
|
||||
GITHUB_PUSH: {
|
||||
data: z.object({
|
||||
branch: z.string(),
|
||||
commitSha: z.string(),
|
||||
repository: z.string(),
|
||||
}),
|
||||
properties: z.object({}),
|
||||
},
|
||||
PROJECT_DEPLOYMENT_CREATED: {
|
||||
data: z.object({ id: z.string() }),
|
||||
properties: z.object({}),
|
||||
},
|
||||
DEPLOYMENT_DEPLOYED: {
|
||||
data: z.object({ id: z.string(), projectId: z.string() }),
|
||||
properties: z.object({}),
|
||||
},
|
||||
CLEANUP_DEPLOYMENT: {
|
||||
data: z.object({ id: z.string() }),
|
||||
properties: z.object({}),
|
||||
},
|
||||
};
|
||||
|
||||
function createTaskQueue() {
|
||||
@@ -891,6 +925,69 @@ function createTaskQueue() {
|
||||
const service = new WorkflowRunCreatedEvent();
|
||||
return service.call(data.id);
|
||||
},
|
||||
START_INITIAL_PROJECT_DEPLOYMENT: async (
|
||||
id,
|
||||
data,
|
||||
properties,
|
||||
attributes
|
||||
) => {
|
||||
const service = new InitialProjectDeployment();
|
||||
|
||||
await service.call(data.id);
|
||||
|
||||
return true;
|
||||
},
|
||||
DEPLOYMENT_BUILD_COMPLETE: async (id, data, properties, attributes) => {
|
||||
if (attributes.redeliveryCount >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const service = new BuildComplete();
|
||||
|
||||
return await service.call(data);
|
||||
},
|
||||
GITHUB_PUSH: async (id, data, properties, attributes) => {
|
||||
if (attributes.redeliveryCount >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const service = new ReceiveRepositoryPush();
|
||||
|
||||
await service.call(data);
|
||||
|
||||
return true;
|
||||
},
|
||||
PROJECT_DEPLOYMENT_CREATED: async (id, data, properties, attributes) => {
|
||||
if (attributes.redeliveryCount >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const service = new DeploymentCreated();
|
||||
|
||||
await service.call(data.id);
|
||||
|
||||
return true;
|
||||
},
|
||||
CLEANUP_DEPLOYMENT: async (id, data, properties, attributes) => {
|
||||
if (attributes.redeliveryCount >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const service = new CleanupDeployment();
|
||||
|
||||
return await service.call(data.id);
|
||||
},
|
||||
DEPLOYMENT_DEPLOYED: async (id, data, properties, attributes) => {
|
||||
if (attributes.redeliveryCount >= 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const service = new StartPendingDeployment();
|
||||
|
||||
await service.call(data.projectId);
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -71,6 +71,12 @@ export class RegisterWorkflow {
|
||||
environment
|
||||
);
|
||||
|
||||
if (isNew) {
|
||||
await taskQueue.publish("WORKFLOW_CREATED", {
|
||||
id: workflow.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success" as const,
|
||||
data: { workflow, environment, organization, isNew },
|
||||
@@ -122,6 +128,10 @@ export class RegisterWorkflow {
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = payload.metadata
|
||||
? JSON.parse(payload.metadata)
|
||||
: undefined;
|
||||
|
||||
const workflow = await this.#prismaClient.workflow.upsert({
|
||||
where: {
|
||||
organizationId_slug: {
|
||||
@@ -136,7 +146,8 @@ export class RegisterWorkflow {
|
||||
service: payload.trigger.service,
|
||||
eventNames: payload.trigger.name,
|
||||
triggerTtlInSeconds: payload.triggerTTL,
|
||||
metadata: payload.metadata ? JSON.parse(payload.metadata) : undefined,
|
||||
metadata,
|
||||
repositoryProjectId: metadata?.env?.PROJECT_ID,
|
||||
jsonSchema:
|
||||
"schema" in payload.trigger
|
||||
? payload.trigger.schema
|
||||
@@ -154,6 +165,8 @@ export class RegisterWorkflow {
|
||||
service: payload.trigger.service,
|
||||
eventNames: payload.trigger.name,
|
||||
triggerTtlInSeconds: payload.triggerTTL,
|
||||
metadata,
|
||||
repositoryProjectId: metadata?.env?.PROJECT_ID,
|
||||
jsonSchema:
|
||||
"schema" in payload.trigger
|
||||
? payload.trigger.schema
|
||||
@@ -167,13 +180,6 @@ export class RegisterWorkflow {
|
||||
});
|
||||
|
||||
if (!existingWorkflow) {
|
||||
await taskQueue.publish("WORKFLOW_CREATED", {
|
||||
id: workflow.id,
|
||||
});
|
||||
await taskQueue.publish("WORKFLOW_CREATED", {
|
||||
id: workflow.id,
|
||||
});
|
||||
|
||||
return { workflow, isNew: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.186.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.186.0",
|
||||
"@cakework/client": "^0.0.42",
|
||||
"@cfworker/json-schema": "^1.12.5",
|
||||
"@codemirror/autocomplete": "^6.3.1",
|
||||
"@codemirror/commands": "^6.1.2",
|
||||
@@ -95,6 +96,7 @@
|
||||
"internal-bridge": "workspace:*",
|
||||
"internal-platform": "workspace:*",
|
||||
"internal-pulsar": "workspace:*",
|
||||
"intl-parse-accept-language": "^1.0.0",
|
||||
"ioredis": "^5.2.4",
|
||||
"isbot": "^3.6.5",
|
||||
"javascript-time-ago": "^2.5.7",
|
||||
@@ -136,12 +138,14 @@
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"tsx": "^3.4.3",
|
||||
"ulid": "^2.3.0",
|
||||
"yaml": "^2.2.1",
|
||||
"zod": "^3.20.2",
|
||||
"zod-error": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@octokit/types": "^9.0.0",
|
||||
"@octokit/webhooks-types": "^6.10.0",
|
||||
"@remix-run/dev": "v1.11.0",
|
||||
"@remix-run/eslint-config": "v1.11.0",
|
||||
"@swc/core": "^1.3.4",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `GitHubRepository` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RepositoryProjectStatus" AS ENUM ('PENDING', 'DEPLOYING', 'DEPLOYED', 'ERROR', 'DISABLED');
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "GitHubRepository" DROP CONSTRAINT "GitHubRepository_authorizationId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "GitHubRepository" DROP CONSTRAINT "GitHubRepository_templateId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "GitHubRepository";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RepositoryProject" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"branch" TEXT NOT NULL DEFAULT 'main',
|
||||
"authorizationId" TEXT NOT NULL,
|
||||
"buildCommand" TEXT NOT NULL,
|
||||
"startCommand" TEXT NOT NULL,
|
||||
"autoDeploy" BOOLEAN NOT NULL DEFAULT true,
|
||||
"envVars" JSONB NOT NULL,
|
||||
"dockerDefinitionUrl" TEXT NOT NULL,
|
||||
"status" "RepositoryProjectStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "RepositoryProject_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RepositoryProject" ADD CONSTRAINT "RepositoryProject_authorizationId_fkey" FOREIGN KEY ("authorizationId") REFERENCES "GitHubAppAuthorization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[name]` on the table `RepositoryProject` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `organizationId` to the `RepositoryProject` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "RepositoryProject" ADD COLUMN "organizationId" TEXT NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RepositoryProject_name_key" ON "RepositoryProject"("name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RepositoryProject" ADD CONSTRAINT "RepositoryProject_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `dockerDefinitionUrl` on the `RepositoryProject` table. All the data in the column will be lost.
|
||||
- Added the required column `dockerIgnore` to the `RepositoryProject` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `dockerfile` to the `RepositoryProject` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "RepositoryProject" DROP COLUMN "dockerDefinitionUrl",
|
||||
ADD COLUMN "dockerIgnore" TEXT NOT NULL,
|
||||
ADD COLUMN "dockerfile" TEXT NOT NULL;
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `dockerIgnore` on the `RepositoryProject` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `dockerfile` on the `RepositoryProject` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "RepositoryProject" DROP COLUMN "dockerIgnore",
|
||||
DROP COLUMN "dockerfile";
|
||||
@@ -0,0 +1,25 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProjectDeploymentStatus" AS ENUM ('PENDING', 'DEPLOYING', 'DEPLOYED', 'ERROR');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProjectDeployment" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"environmentId" TEXT NOT NULL,
|
||||
"status" "ProjectDeploymentStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"imageIdentifier" TEXT,
|
||||
"branch" TEXT NOT NULL,
|
||||
"commitHash" TEXT NOT NULL,
|
||||
"commitMessage" TEXT NOT NULL,
|
||||
"committer" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProjectDeployment_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectDeployment" ADD CONSTRAINT "ProjectDeployment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "RepositoryProject"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectDeployment" ADD CONSTRAINT "ProjectDeployment_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "RuntimeEnvironment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "RepositoryProject" ADD COLUMN "statusText" TEXT;
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[projectId,commitHash]` on the table `ProjectDeployment` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectDeployment" ADD COLUMN "error" JSONB;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProjectDeployment_projectId_commitHash_key" ON "ProjectDeployment"("projectId", "commitHash");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectDeployment" ADD COLUMN "buildDuration" INTEGER;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectDeployment" ADD COLUMN "vmIdentifier" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "RepositoryProject" ADD COLUMN "currentVMIdentifier" TEXT;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "RepositoryProject" ADD COLUMN "currentDeploymentId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RepositoryProject" ADD CONSTRAINT "RepositoryProject_currentDeploymentId_fkey" FOREIGN KEY ("currentDeploymentId") REFERENCES "ProjectDeployment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `buildDuration` on the `ProjectDeployment` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `imageIdentifier` on the `ProjectDeployment` table. All the data in the column will be lost.
|
||||
- Added the required column `buildId` to the `ProjectDeployment` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectDeployment" DROP COLUMN "buildDuration",
|
||||
DROP COLUMN "imageIdentifier",
|
||||
ADD COLUMN "buildFinishedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "buildId" TEXT NOT NULL,
|
||||
ADD COLUMN "buildStartedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "imageId" TEXT;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[buildId]` on the table `ProjectDeployment` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProjectDeployment_buildId_key" ON "ProjectDeployment"("buildId");
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `dockerIgnore` to the `ProjectDeployment` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `dockerfile` to the `ProjectDeployment` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectDeployment" ADD COLUMN "dockerIgnore" TEXT NOT NULL,
|
||||
ADD COLUMN "dockerfile" TEXT NOT NULL;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Workflow" ADD COLUMN "repositoryProjectId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Workflow" ADD CONSTRAINT "Workflow_repositoryProjectId_fkey" FOREIGN KEY ("repositoryProjectId") REFERENCES "RepositoryProject"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "ProjectDeploymentStatus" ADD VALUE 'BUILDING';
|
||||
ALTER TYPE "ProjectDeploymentStatus" ADD VALUE 'CANCELLED';
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "RepositoryProjectStatus" ADD VALUE 'BUILDING';
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ProjectDeploymentStatus" ADD VALUE 'STOPPED';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ProjectDeployment" ADD COLUMN "stoppedAt" TIMESTAMP(3);
|
||||
@@ -46,16 +46,17 @@ model Organization {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
users User[]
|
||||
workflows Workflow[]
|
||||
environments RuntimeEnvironment[]
|
||||
apiConnections APIConnection[]
|
||||
events TriggerEvent[]
|
||||
externalSources ExternalSource[]
|
||||
eventRules EventRule[]
|
||||
schedulerSources SchedulerSource[]
|
||||
internalSources InternalSource[]
|
||||
templates OrganizationTemplate[]
|
||||
users User[]
|
||||
workflows Workflow[]
|
||||
environments RuntimeEnvironment[]
|
||||
apiConnections APIConnection[]
|
||||
events TriggerEvent[]
|
||||
externalSources ExternalSource[]
|
||||
eventRules EventRule[]
|
||||
schedulerSources SchedulerSource[]
|
||||
internalSources InternalSource[]
|
||||
templates OrganizationTemplate[]
|
||||
repositoryProjects RepositoryProject[]
|
||||
}
|
||||
|
||||
model APIConnection {
|
||||
@@ -112,6 +113,7 @@ model RuntimeEnvironment {
|
||||
eventRules EventRule[]
|
||||
schedulerSources SchedulerSource[]
|
||||
internalSources InternalSource[]
|
||||
deployments ProjectDeployment[]
|
||||
|
||||
@@unique([organizationId, slug])
|
||||
}
|
||||
@@ -155,6 +157,9 @@ model Workflow {
|
||||
organizationTemplate OrganizationTemplate? @relation(fields: [organizationTemplateId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
organizationTemplateId String?
|
||||
|
||||
repositoryProject RepositoryProject? @relation(fields: [repositoryProjectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
repositoryProjectId String?
|
||||
|
||||
@@unique([organizationId, slug])
|
||||
}
|
||||
|
||||
@@ -599,11 +604,11 @@ model GitHubAppAuthorization {
|
||||
installationAccessTokenExpiresAt DateTime?
|
||||
|
||||
organizationTemplates OrganizationTemplate[]
|
||||
repositories GitHubRepository[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
GitHubAppAuthorizationAttempt GitHubAppAuthorizationAttempt[]
|
||||
repositoryProjects RepositoryProject[]
|
||||
}
|
||||
|
||||
enum GitHubAccountType {
|
||||
@@ -611,20 +616,92 @@ enum GitHubAccountType {
|
||||
ORGANIZATION
|
||||
}
|
||||
|
||||
model GitHubRepository {
|
||||
model RepositoryProject {
|
||||
id String @id @default(cuid())
|
||||
|
||||
name String
|
||||
url String
|
||||
name String @unique
|
||||
url String
|
||||
branch String @default("main")
|
||||
|
||||
authorization GitHubAppAuthorization @relation(fields: [authorizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
authorizationId String
|
||||
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
templateId String?
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
organizationId String
|
||||
|
||||
buildCommand String
|
||||
startCommand String
|
||||
autoDeploy Boolean @default(true)
|
||||
envVars Json
|
||||
|
||||
status RepositoryProjectStatus @default(PENDING)
|
||||
statusText String?
|
||||
|
||||
currentVMIdentifier String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
deployments ProjectDeployment[] @relation("repoProject")
|
||||
|
||||
currentDeployment ProjectDeployment? @relation(fields: [currentDeploymentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
currentDeploymentId String?
|
||||
|
||||
workflows Workflow[]
|
||||
}
|
||||
|
||||
enum RepositoryProjectStatus {
|
||||
PENDING
|
||||
BUILDING
|
||||
DEPLOYING
|
||||
DEPLOYED
|
||||
ERROR
|
||||
DISABLED
|
||||
}
|
||||
|
||||
model ProjectDeployment {
|
||||
id String @id @default(cuid())
|
||||
|
||||
project RepositoryProject @relation("repoProject", fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
projectId String
|
||||
|
||||
environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
environmentId String
|
||||
|
||||
status ProjectDeploymentStatus @default(PENDING)
|
||||
|
||||
buildId String @unique
|
||||
imageId String?
|
||||
vmIdentifier String?
|
||||
buildStartedAt DateTime?
|
||||
buildFinishedAt DateTime?
|
||||
stoppedAt DateTime?
|
||||
|
||||
dockerfile String
|
||||
dockerIgnore String
|
||||
|
||||
branch String
|
||||
commitHash String
|
||||
commitMessage String
|
||||
committer String
|
||||
|
||||
error Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
RepositoryProject RepositoryProject[]
|
||||
|
||||
@@unique([projectId, commitHash])
|
||||
}
|
||||
|
||||
enum ProjectDeploymentStatus {
|
||||
PENDING
|
||||
BUILDING
|
||||
DEPLOYING
|
||||
DEPLOYED
|
||||
CANCELLED
|
||||
ERROR
|
||||
STOPPED
|
||||
}
|
||||
|
||||
model Template {
|
||||
@@ -649,7 +726,6 @@ model Template {
|
||||
isLive Boolean @default(true)
|
||||
|
||||
organizationTemplates OrganizationTemplate[]
|
||||
repositories GitHubRepository[]
|
||||
}
|
||||
|
||||
model OrganizationTemplate {
|
||||
|
||||
@@ -762,6 +762,7 @@ export class TriggerClient<TSchema extends z.ZodTypeAny> {
|
||||
}
|
||||
: undefined,
|
||||
packageMetadata,
|
||||
env: gatherEnvVars(process.env),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -871,3 +872,16 @@ function safeGetRepoInfo() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all env vars that are prefixed with TRIGGER_ (exccpt for TRIGGER_API_KEY)
|
||||
function gatherEnvVars(env: NodeJS.ProcessEnv): Record<string, string> {
|
||||
if (!env) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const envVars = Object.entries(env)
|
||||
.filter(([key]) => key.startsWith("TRIGGER_") && key !== "TRIGGER_API_KEY")
|
||||
.map(([key, value]) => [key.replace("TRIGGER_", ""), `${value}`]);
|
||||
|
||||
return Object.fromEntries(envVars);
|
||||
}
|
||||
|
||||
Generated
+49
@@ -132,6 +132,7 @@ importers:
|
||||
specifiers:
|
||||
'@aws-sdk/client-s3': ^3.186.0
|
||||
'@aws-sdk/s3-request-presigner': ^3.186.0
|
||||
'@cakework/client': ^0.0.42
|
||||
'@cfworker/json-schema': ^1.12.5
|
||||
'@codemirror/autocomplete': ^6.3.1
|
||||
'@codemirror/commands': ^6.1.2
|
||||
@@ -152,6 +153,7 @@ importers:
|
||||
'@octokit/types': ^9.0.0
|
||||
'@octokit/webhooks': ^10.4.0
|
||||
'@octokit/webhooks-methods': ^3.0.2
|
||||
'@octokit/webhooks-types': ^6.10.0
|
||||
'@prisma/client': ^4.3.0
|
||||
'@react-email/head': ^0.0.2
|
||||
'@remix-run/dev': v1.11.0
|
||||
@@ -231,6 +233,7 @@ importers:
|
||||
internal-bridge: workspace:*
|
||||
internal-platform: workspace:*
|
||||
internal-pulsar: workspace:*
|
||||
intl-parse-accept-language: ^1.0.0
|
||||
ioredis: ^5.2.4
|
||||
isbot: ^3.6.5
|
||||
javascript-time-ago: ^2.5.7
|
||||
@@ -289,11 +292,13 @@ importers:
|
||||
vite: ^3.1.4
|
||||
vite-tsconfig-paths: ^3.5.1
|
||||
vitest: ^0.23.4
|
||||
yaml: ^2.2.1
|
||||
zod: ^3.20.2
|
||||
zod-error: ^1.1.0
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3': 3.245.0
|
||||
'@aws-sdk/s3-request-presigner': 3.245.0
|
||||
'@cakework/client': 0.0.42
|
||||
'@cfworker/json-schema': 1.12.5
|
||||
'@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
|
||||
'@codemirror/commands': 6.1.3
|
||||
@@ -345,6 +350,7 @@ importers:
|
||||
internal-bridge: link:../../packages/internal-bridge
|
||||
internal-platform: link:../../packages/internal-platform
|
||||
internal-pulsar: link:../../packages/internal-pulsar
|
||||
intl-parse-accept-language: 1.0.0
|
||||
ioredis: 5.2.4
|
||||
isbot: 3.6.5
|
||||
javascript-time-ago: 2.5.9
|
||||
@@ -386,11 +392,13 @@ importers:
|
||||
tiny-invariant: 1.3.1
|
||||
tsx: 3.12.2
|
||||
ulid: 2.3.0
|
||||
yaml: 2.2.1
|
||||
zod: 3.20.2
|
||||
zod-error: 1.1.0
|
||||
devDependencies:
|
||||
'@faker-js/faker': 7.6.0
|
||||
'@octokit/types': 9.0.0
|
||||
'@octokit/webhooks-types': 6.10.0
|
||||
'@remix-run/dev': 1.11.0_biqbaboplfbrettd7655fr4n2y
|
||||
'@remix-run/eslint-config': 1.11.0_ol4nhuzbuflsbzk2mijpqykzba
|
||||
'@swc/core': 1.3.26
|
||||
@@ -3682,6 +3690,20 @@ packages:
|
||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
dev: true
|
||||
|
||||
/@cakework/client/0.0.42:
|
||||
resolution: {integrity: sha512-vnL30mqCFwTYvOWb2WfEfO/XlrMDbfACYuenvmQo3aXumtgv23k6/jGCKbQKI9b/Wpoi7cHwx2Naod14TyRnzA==}
|
||||
dependencies:
|
||||
'@types/basic-auth': 1.1.3
|
||||
'@types/url-join': 4.0.1
|
||||
axios: 0.27.2
|
||||
basic-auth: 2.0.1
|
||||
buffer: 6.0.3
|
||||
js-base64: 3.7.5
|
||||
url-join: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/@cfworker/json-schema/1.12.5:
|
||||
resolution: {integrity: sha512-k+ungOs1TxSfNfmfOxkwaiNL2EgRQjhk2SRweMYoKkBJZUYsGsY+XYpe2KpuS9EyjZWjZhq8QvVM+gwi3g1+hQ==}
|
||||
dev: false
|
||||
@@ -5264,6 +5286,10 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/@octokit/webhooks-types/6.10.0:
|
||||
resolution: {integrity: sha512-lDNv83BeEyxxukdQ0UttiUXawk9+6DkdjjFtm2GFED+24IQhTVaoSbwV9vWWKONyGLzRmCQqZmoEWkDhkEmPlw==}
|
||||
dev: true
|
||||
|
||||
/@octokit/webhooks-types/6.7.0:
|
||||
resolution: {integrity: sha512-bykm7UkSnxmb2uhSfcLM1Pity/LQ6ZBSdzy9HU0vXjR+2g+tzlmRhXb7Go8oj0TlgO+vDrTivGXju6zkzOGKjA==}
|
||||
dev: false
|
||||
@@ -6213,6 +6239,12 @@ packages:
|
||||
resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==}
|
||||
dev: true
|
||||
|
||||
/@types/basic-auth/1.1.3:
|
||||
resolution: {integrity: sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg==}
|
||||
dependencies:
|
||||
'@types/node': 18.14.0
|
||||
dev: false
|
||||
|
||||
/@types/bcryptjs/2.4.2:
|
||||
resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==}
|
||||
dev: true
|
||||
@@ -6636,6 +6668,10 @@ packages:
|
||||
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
||||
dev: true
|
||||
|
||||
/@types/url-join/4.0.1:
|
||||
resolution: {integrity: sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ==}
|
||||
dev: false
|
||||
|
||||
/@types/uuid/9.0.0:
|
||||
resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==}
|
||||
dev: true
|
||||
@@ -12522,6 +12558,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/js-base64/3.7.5:
|
||||
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
|
||||
dev: false
|
||||
|
||||
/js-beautify/1.14.7:
|
||||
resolution: {integrity: sha512-5SOX1KXPFKx+5f6ZrPsIPEY7NwKeQz47n3jm2i+XeHx9MoRsfQenlOP13FQhWvg8JRS0+XLO6XYUQ2GX+q+T9A==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -17784,6 +17824,10 @@ packages:
|
||||
resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==}
|
||||
deprecated: Please see https://github.com/lydell/urix#deprecated
|
||||
|
||||
/url-join/4.0.1:
|
||||
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
|
||||
dev: false
|
||||
|
||||
/url-parse-lax/3.0.0:
|
||||
resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -18530,6 +18574,11 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/yaml/2.2.1:
|
||||
resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/yargs-parser/18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
set -e -x
|
||||
|
||||
ngrok http --subdomain=${1:-$NGROK_SUBDOMAIN} --region ${2:-eu} 8889
|
||||
Reference in New Issue
Block a user