Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-deploy-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
run: wasp-cli install

- name: Install Railway CLI
run: mise use -g github:railwayapp/cli@4.51.0
run: mise use -g github:railwayapp/cli@5.28.0

- name: Deploy app to Railway
working-directory: ${{ env.APP_TO_DEPLOY }}
Expand Down
2 changes: 1 addition & 1 deletion waspc/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

- Renamed the `wasp deploy fly` flags `--vm-size`, `--initial-cluster-size`, and `--volume-size` to `--db-vm-size`, `--db-initial-cluster-size`, and `--db-volume-size`, respectively. ([#4642](https://github.com/wasp-lang/wasp/pull/4642))
- `wasp deploy fly` now requires Fly CLI version 0.4.82 or newer. ([#4642](https://github.com/wasp-lang/wasp/pull/4642))
- Wasp Deploy for Railway now requires Railway CLI 4.51.0 or newer. ([#4647](https://github.com/wasp-lang/wasp/pull/4647))
- Wasp Deploy for Railway now requires Railway CLI 5.28.0 or newer. ([#4647](https://github.com/wasp-lang/wasp/pull/4647), [#4712](https://github.com/wasp-lang/wasp/pull/4712))
- Moved internal server-only import paths under the `wasp/server/...` prefix. These paths are not part of the documented public API, but if your app imported any of them, update the import path or switch to documented public imports like `wasp/server/auth`. ([#4557](https://github.com/wasp-lang/wasp/pull/4557))
- Removed the `wasp info` command, in favor of the new `wasp show` family of commands. ([#4622](https://github.com/wasp-lang/wasp/pull/4622))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "../../DeploymentInstructions.js";
import { getRailwayEnvVarValueReference } from "../../env.js";
import { clientAppPort, serverAppPort } from "../../ports.js";
import { getLinkedEnvironmentId } from "../../railwayEnvironment/cli.js";
import {
initRailwayProject,
linkRailwayProjectToWaspProjectDir,
Expand Down Expand Up @@ -138,12 +139,14 @@ async function setupDb({
}: DeploymentInstructions<SetupCmdOptions>): Promise<void> {
waspSays(`Setting up database using image: ${options.dbImage}`);

const environmentId = await getLinkedEnvironmentId(options);
const dbService = await createDatabaseService({
serviceName: dbServiceName,
imageSpec: {
image: options.dbImage,
volumeMountPath: options.dbVolumeMountPath,
},
environmentId,
railwayExe: options.railwayExe,
waspProjectDir: options.waspProjectDir,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export const RailwayCliProjectSchema = z.object({

export const RailwayProjectListSchema = z.array(RailwayCliProjectSchema);

export const RailwayCliEnvironmentListSchema = z.object({
environments: z.array(
z.object({
id: z.string(),
isLinked: z.boolean(),
}),
),
});

export const RailwayCliServiceListSchema = z.array(
RailwayCliServiceSchema.extend({
volumes: z.array(
Expand Down Expand Up @@ -57,6 +66,18 @@ export const RailwayCliServiceStatusSchema = z.object({
status: DeploymentStatusSchema.nullable().catch(null).default(null),
});

export const RailwayApiServiceInstanceUpdateResponseSchema = z.object({
data: z.object({
serviceInstanceUpdate: z.literal(true),
}),
});

export const RailwayApiServiceInstanceDeployV2ResponseSchema = z.object({
data: z.object({
serviceInstanceDeployV2: z.string(),
}),
});

export const RailwayCliDomainSchema = z.union([
// `railway domain` prints all existing domains when the service already
// has one...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import { RailwayCliExe, RailwayProjectName } from "./brandedTypes.js";
import { serviceNameSuffixes } from "./railwayService/nameGenerator.js";

// Wasp relies on the `--json` output added in Railway CLI 4.51.0.
const minSupportedRailwayCliVersion = new SemVer("4.51.0");
// Wasp relies on the `railway api` command added in Railway CLI 5.28.0.
const minSupportedRailwayCliVersion = new SemVer("5.28.0");

export async function ensureRailwayCliReady(
railwayExe: RailwayCliExe,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { WaspProjectDir } from "../../../common/brandedTypes.js";
import { createCommandWithCwd, runJsonCommand } from "../../../common/zx.js";
import { RailwayCliExe } from "../brandedTypes.js";
import { RailwayCliEnvironmentListSchema } from "../jsonOutputSchemas.js";

// `railway status --json` returns all of the project's environments without
// saying which one is linked, so we ask `railway environment list` instead.
export async function getLinkedEnvironmentId(options: {
railwayExe: RailwayCliExe;
waspProjectDir: WaspProjectDir;
}): Promise<string> {
const railwayCli = createCommandWithCwd(
options.railwayExe,
options.waspProjectDir,
);

const { environments } = await runJsonCommand(
railwayCli,
["environment", "list", "--json"],
RailwayCliEnvironmentListSchema,
);
const linkedEnvironment = environments.find(
(environment) => environment.isLinked,
);
if (linkedEnvironment === undefined) {
throw new Error(
"No Railway environment is linked to this directory. Run `railway environment` to link one.",
);
}
return linkedEnvironment.id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ import {
RailwayCliServiceListSchema,
RailwayCliServiceSchema,
} from "../jsonOutputSchemas.js";
import {
RailwayServiceInstance,
setServiceInstanceImage,
startServiceInstanceDeployment,
} from "./serviceInstance.js";

// Creating a service with an image immediately starts a deployment, and
// Postgres crashes when its volume isn't attached yet.
export async function createDatabaseService({
serviceName,
imageSpec,
environmentId,
railwayExe,
waspProjectDir,
}: {
Expand All @@ -20,22 +28,28 @@ export async function createDatabaseService({
image: string;
volumeMountPath: string;
};
environmentId: string;
railwayExe: RailwayCliExe;
waspProjectDir: WaspProjectDir;
}): Promise<RailwayCliService> {
const options = { railwayExe, waspProjectDir };
const dbService = await addDatabaseService(
const dbService = await addDatabaseServiceWithoutImage(
serviceName,
imageSpec.image,
imageSpec.volumeMountPath,
options,
);
const dbServiceInstance: RailwayServiceInstance = {
serviceId: dbService.id,
environmentId,
};

try {
await addDatabaseVolume(dbService, imageSpec.volumeMountPath, options);
} catch (volumeError) {
await deleteIncompleteDatabaseService(dbService, volumeError, options);
throw volumeError;
await setServiceInstanceImage(dbServiceInstance, imageSpec.image, options);
await startServiceInstanceDeployment(dbServiceInstance, options);
} catch (setupError) {
await deleteIncompleteDatabaseService(dbService, setupError, options);
throw setupError;
}

return dbService;
Expand All @@ -60,9 +74,8 @@ export async function assertDatabaseServiceHasVolume(
}
}

async function addDatabaseService(
async function addDatabaseServiceWithoutImage(
dbServiceName: DbServiceName,
dbImage: string,
dbVolumeMountPath: string,
options: {
railwayExe: RailwayCliExe;
Expand Down Expand Up @@ -92,13 +105,7 @@ async function addDatabaseService(

return runJsonCommand(
railwayCli,
[
"add",
...["--service", dbServiceName],
...["--image", dbImage],
...variableArgs,
"--json",
],
["add", ...["--service", dbServiceName], ...variableArgs, "--json"],
RailwayCliServiceSchema,
);
}
Expand Down Expand Up @@ -130,7 +137,7 @@ async function addDatabaseVolume(

async function deleteIncompleteDatabaseService(
dbService: RailwayCliService,
volumeError: unknown,
setupError: unknown,
options: {
railwayExe: RailwayCliExe;
waspProjectDir: WaspProjectDir;
Expand All @@ -151,7 +158,7 @@ async function deleteIncompleteDatabaseService(
[
`Wasp couldn't finish setting up Railway database service "${dbService.name}" (${dbService.id}).`,
"Wasp also couldn't remove the incomplete service. Remove it from Railway before trying again.",
`Volume error: ${getErrorMessage(volumeError)}`,
`Setup error: ${getErrorMessage(setupError)}`,
`Cleanup error: ${getErrorMessage(cleanupError)}`,
].join("\n"),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as z from "zod";
import { WaspProjectDir } from "../../../common/brandedTypes.js";
import { createCommandWithCwd, runJsonCommand } from "../../../common/zx.js";
import { RailwayCliExe } from "../brandedTypes.js";
import {
RailwayApiServiceInstanceDeployV2ResponseSchema,
RailwayApiServiceInstanceUpdateResponseSchema,
} from "../jsonOutputSchemas.js";

// Railway's name for a service in a specific environment.
export type RailwayServiceInstance = {
serviceId: string;
environmentId: string;
};

export async function setServiceInstanceImage(
serviceInstance: RailwayServiceInstance,
image: string,
options: {
railwayExe: RailwayCliExe;
waspProjectDir: WaspProjectDir;
},
): Promise<void> {
const serviceInstanceUpdateMutation = `
mutation ServiceInstanceUpdate(
$serviceId: String!
$environmentId: String
$input: ServiceInstanceUpdateInput!
) {
serviceInstanceUpdate(
serviceId: $serviceId
environmentId: $environmentId
input: $input
)
}
`;

const imageSourceInput = JSON.stringify({ input: { source: { image } } });
await runServiceInstanceMutation(
serviceInstance,
serviceInstanceUpdateMutation,
["--variables", imageSourceInput],
RailwayApiServiceInstanceUpdateResponseSchema,
options,
);
}

export async function startServiceInstanceDeployment(
serviceInstance: RailwayServiceInstance,
options: {
railwayExe: RailwayCliExe;
waspProjectDir: WaspProjectDir;
},
): Promise<void> {
const serviceInstanceDeployV2Mutation = `
mutation ServiceInstanceDeployV2($serviceId: String!, $environmentId: String!) {
serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId)
}
`;

await runServiceInstanceMutation(
serviceInstance,
serviceInstanceDeployV2Mutation,
[],
RailwayApiServiceInstanceDeployV2ResponseSchema,
options,
);
}

async function runServiceInstanceMutation<Schema extends z.ZodType>(
serviceInstance: RailwayServiceInstance,
mutation: string,
mutationArgs: string[],
responseSchema: Schema,
options: {
railwayExe: RailwayCliExe;
waspProjectDir: WaspProjectDir;
},
): Promise<z.infer<Schema>> {
const railwayCli = createCommandWithCwd(
options.railwayExe,
options.waspProjectDir,
);

return runJsonCommand(
railwayCli,
[
"api",
mutation,
...["--raw-var", `serviceId=${serviceInstance.serviceId}`],
...["--raw-var", `environmentId=${serviceInstance.environmentId}`],
...mutationArgs,
],
responseSchema,
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { describe, expect, test } from "vitest";
import {
RailwayCliDomainSchema,
RailwayCliProjectSchema,
RailwayCliServiceStatusSchema,
RailwayProjectListSchema,
} from "../../../src/providers/railway/jsonOutputSchemas.js";
import {
cliProjectWithServices,
cliProjectWithoutServices,
} from "./fixtures/railwayCliProject.js";

describe("RailwayCliDomainSchema", () => {
test("parses new format with domains array", () => {
Expand All @@ -35,20 +29,6 @@ describe("RailwayCliDomainSchema", () => {
});
});

describe("RailwayCliProjectSchema", () => {
test("parses a project with services", () => {
const result = RailwayCliProjectSchema.parse(cliProjectWithServices);
expect(result.id).toBe(cliProjectWithServices.id);
expect(result.name).toBe(cliProjectWithServices.name);
expect(result.services.edges).toHaveLength(2);
});

test("parses a project with no services", () => {
const result = RailwayCliProjectSchema.parse(cliProjectWithoutServices);
expect(result.services.edges).toEqual([]);
});
});

describe("RailwayCliServiceStatusSchema", () => {
test("treats a missing or unknown status as not ready", () => {
expect(RailwayCliServiceStatusSchema.parse({}).status).toBeNull();
Expand All @@ -58,15 +38,3 @@ describe("RailwayCliServiceStatusSchema", () => {
).toBeNull();
});
});

describe("RailwayProjectListSchema", () => {
test("parses a list of projects", () => {
const input = [cliProjectWithServices, cliProjectWithoutServices];
const result = RailwayProjectListSchema.parse(input);
expect(result).toHaveLength(2);
});

test("parses empty list", () => {
expect(RailwayProjectListSchema.parse([])).toEqual([]);
});
});
Loading
Loading