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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion actions/railway-redeploy/dist/index.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions actions/railway-redeploy/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ run = 'rr tsc'
[tasks."check"]
run = 'rr check'

[tasks.test]
description = 'unit tests (node:test with native type stripping)'
run = "node --test 'src/**/*.test.ts'"

[tasks.build]
description = 'bundle src into dist/ via ncc'
sources = ["src/**/*", "package.json", "tsconfig.json"]
Expand Down
33 changes: 33 additions & 0 deletions actions/railway-redeploy/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { describe, it } from "node:test";

const REQUIRED_INPUTS = {
project_id: "project",
environment: "staging",
service_name: "api",
api_token: "token",
};

// Runs the entry point the way the runner does: the inputs arrive as INPUT_* variables,
// and the step's error is the `::error::` line on stdout.
function runAction(inputs: Record<string, string>) {
const env = Object.fromEntries(Object.entries(inputs).map(([name, value]) => [`INPUT_${name.toUpperCase()}`, value]));

return spawnSync(process.execPath, [path.join(import.meta.dirname, "index.ts")], { env, encoding: "utf8" });
}

describe("required inputs", () => {
for (const name of Object.keys(REQUIRED_INPUTS)) {
it(`fails the step with a clear message when ${name} is missing`, () => {
const result = runAction(Object.fromEntries(Object.entries(REQUIRED_INPUTS).filter(([key]) => key !== name)));

assert.equal(result.status, 1);
assert.match(
result.stdout,
new RegExp(`^::error::🚨 Failed to redeploy railway service: Input required and not supplied: ${name}$`, "m"),
);
});
}
});
53 changes: 26 additions & 27 deletions actions/railway-redeploy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,41 +165,40 @@ async function main() {
return `https://railway.com/project/${deployment.projectId}/service/${deployment.serviceId}?environmentId=${deployment.environmentId}`;
}

try {
const envWithServices = await getEnviromentServices(environment);
const envWithServices = await getEnviromentServices(environment);

if (!envWithServices) {
throw new Error(`Environment "${environment}" not found`);
}

const serviceInstance = envWithServices.serviceInstances.edges.find((it) => eqString(it.node.serviceName, serviceName));
if (!envWithServices) {
throw new Error(`Environment "${environment}" not found`);
}

if (!serviceInstance) {
throw new Error(`Service "${serviceName}" not found in environment "${environment}"`);
}
const serviceInstance = envWithServices.serviceInstances.edges.find((it) => eqString(it.node.serviceName, serviceName));

const deployment = await getLastActiveOrSleepingOrFailedDeployment(serviceInstance.node.serviceId, envWithServices.id);
if (!serviceInstance) {
throw new Error(`Service "${serviceName}" not found in environment "${environment}"`);
}

if (!deployment) {
throw new Error("No active, sleeping, or failed deployments found");
}
const deployment = await getLastActiveOrSleepingOrFailedDeployment(serviceInstance.node.serviceId, envWithServices.id);

await deploymentRedeploy(deployment.id);
if (!deployment) {
throw new Error("No active, sleeping, or failed deployments found");
}

const serviceUrl = getServicePanelUrl(deployment);
await deploymentRedeploy(deployment.id);

core.info(`🚀 Deployment (ID: ${deployment.id}) redeploy launched: ${serviceUrl}`);
const serviceUrl = getServicePanelUrl(deployment);

core.setOutput("service_url", serviceUrl);
} catch (error) {
const msg = "Failed to redeploy railway service";
core.info(`🚀 Deployment (ID: ${deployment.id}) redeploy launched: ${serviceUrl}`);

if (error instanceof Error) {
core.setFailed(`🚨 ${msg}: ${error.message}`);
} else {
core.setFailed(`🚨 ${msg}`);
}
}
core.setOutput("service_url", serviceUrl);
}

main();
// The catch sits on the call so that everything main() throws, a missing input included, reaches setFailed.
main().catch((error) => {
const msg = "Failed to redeploy railway service";

if (error instanceof Error) {
core.setFailed(`🚨 ${msg}: ${error.message}`);
} else {
core.setFailed(`🚨 ${msg}`);
}
});
Loading