@tachyon-gg/railway-deploy 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -2
  2. package/dist/index.js +157 -78
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -209,11 +209,26 @@ variables:
209
209
  | `${ENV_VAR}` | At config load time | Reads from local environment (or `--env-file`) |
210
210
  | `${{service.VAR}}` | At Railway runtime | Railway reference variable (cross-service) |
211
211
  | `%{param}` | At config load time | Template parameter substitution |
212
+ | `%{service_name}` | At config load time | Built-in: the service's config key |
212
213
  | `null` | N/A | Marks a variable for deletion |
213
214
 
215
+ `%{param}` is expanded first, so it can be used inside `${{}}` Railway references. This is useful for templates that need to reference their own or other services' variables:
216
+
217
+ ```yaml
218
+ variables:
219
+ # Reference own service's variable (resolves %{service_name} at config time,
220
+ # Railway resolves the ${{}} reference at runtime)
221
+ DATABASE_URL: ${{%{service_name}.DATABASE_URL}}
222
+
223
+ # Reference another service by param
224
+ REDIS_URL: ${{%{cache_service}.REDIS_URL}}
225
+ ```
226
+
214
227
  ### Service templates
215
228
 
216
- Templates extract reusable service definitions with parameterized values:
229
+ Templates extract reusable service definitions with parameterized values.
230
+
231
+ The built-in `%{service_name}` param is always available and resolves to the service's key in the config (e.g., `web`, `api`). It cannot be overridden.
217
232
 
218
233
  ```yaml
219
234
  # services/web.yaml
@@ -228,10 +243,11 @@ source:
228
243
 
229
244
  variables:
230
245
  APP_VERSION: "%{tag}"
246
+ SERVICE_NAME: "%{service_name}"
231
247
  DATABASE_URL: ${{Postgres.DATABASE_URL}}
232
248
 
233
249
  domains:
234
- - "%{tag}.example.com"
250
+ - "%{service_name}.example.com"
235
251
 
236
252
  healthcheck:
237
253
  path: /health
package/dist/index.js CHANGED
@@ -23805,6 +23805,7 @@ var {
23805
23805
 
23806
23806
  // src/index.ts
23807
23807
  import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
23808
+ import { createRequire as createRequire2 } from "module";
23808
23809
  import { resolve as resolve2 } from "path";
23809
23810
  import { createInterface } from "readline";
23810
23811
 
@@ -37708,9 +37709,12 @@ function resolveService(name, entry, envDir, lenient = false) {
37708
37709
  validateServiceTemplate(parsed, templatePath);
37709
37710
  template = parsed;
37710
37711
  }
37711
- let params = {};
37712
+ if ("service_name" in (entry.params ?? {}) || "service_name" in (template?.params ?? {})) {
37713
+ throw new Error(`"service_name" is a built-in parameter and cannot be overridden (service "${name}")`);
37714
+ }
37715
+ let params = { service_name: name };
37712
37716
  if (template?.params) {
37713
- params = resolveParams(template.params, entry.params || {});
37717
+ params = { ...resolveParams(template.params, entry.params || {}), service_name: name };
37714
37718
  }
37715
37719
  let source = template?.source ? expandParamsDeep(template.source, params) : entry.source;
37716
37720
  const volume = template?.volume ? expandParamsDeep(template.volume, params) : entry.volume;
@@ -37739,9 +37743,8 @@ function resolveService(name, entry, envDir, lenient = false) {
37739
37743
  const railwayConfigFile = template?.railway_config_file ? expandParamsDeep(template.railway_config_file, params) : entry.railway_config_file;
37740
37744
  const staticOutboundIps = template?.static_outbound_ips ?? entry.static_outbound_ips;
37741
37745
  let templateDomains = [];
37742
- if (template) {
37743
- const tplDomains = template.domains ? expandParamsDeep(template.domains, params) : undefined;
37744
- templateDomains = normalizeDomains(tplDomains);
37746
+ if (template?.domains) {
37747
+ templateDomains = normalizeDomains(expandParamsDeep(template.domains, params));
37745
37748
  }
37746
37749
  const entryDomains = normalizeDomains(entry.domains);
37747
37750
  if (entry.source)
@@ -38446,8 +38449,8 @@ var EgressGatewayAssociationCreateDocument = { kind: "Document", definitions: [{
38446
38449
  var EgressGatewayAssociationsClearDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "mutation", name: { kind: "Name", value: "EgressGatewayAssociationsClear" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "input" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "EgressGatewayServiceTargetInput" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "egressGatewayAssociationsClear" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "input" }, value: { kind: "Variable", name: { kind: "Name", value: "input" } } }] }] } }] };
38447
38450
  var ListProjectsDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "ListProjects" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "projects" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "name" } }] } }] } }] } }] } }] };
38448
38451
  var GetProjectDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetProject" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "id" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "project" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "id" }, value: { kind: "Variable", name: { kind: "Name", value: "id" } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "name" } }, { kind: "Field", name: { kind: "Name", value: "environments" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "name" } }, { kind: "Field", name: { kind: "Name", value: "deploymentTriggers" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "branch" } }, { kind: "Field", name: { kind: "Name", value: "checkSuites" } }, { kind: "Field", name: { kind: "Name", value: "serviceId" } }, { kind: "Field", name: { kind: "Name", value: "repository" } }] } }] } }] } }] } }] } }] } }, { kind: "Field", name: { kind: "Name", value: "services" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "name" } }, { kind: "Field", name: { kind: "Name", value: "serviceInstances" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "environmentId" } }, { kind: "Field", name: { kind: "Name", value: "source" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "image" } }, { kind: "Field", name: { kind: "Name", value: "repo" } }] } }, { kind: "Field", name: { kind: "Name", value: "domains" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "customDomains" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "domain" } }, { kind: "Field", name: { kind: "Name", value: "targetPort" } }] } }, { kind: "Field", name: { kind: "Name", value: "serviceDomains" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "domain" } }, { kind: "Field", name: { kind: "Name", value: "targetPort" } }] } }] } }, { kind: "Field", name: { kind: "Name", value: "region" } }, { kind: "Field", name: { kind: "Name", value: "numReplicas" } }, { kind: "Field", name: { kind: "Name", value: "restartPolicyType" } }, { kind: "Field", name: { kind: "Name", value: "restartPolicyMaxRetries" } }, { kind: "Field", name: { kind: "Name", value: "healthcheckPath" } }, { kind: "Field", name: { kind: "Name", value: "healthcheckTimeout" } }, { kind: "Field", name: { kind: "Name", value: "cronSchedule" } }, { kind: "Field", name: { kind: "Name", value: "startCommand" } }, { kind: "Field", name: { kind: "Name", value: "buildCommand" } }, { kind: "Field", name: { kind: "Name", value: "rootDirectory" } }, { kind: "Field", name: { kind: "Name", value: "dockerfilePath" } }, { kind: "Field", name: { kind: "Name", value: "preDeployCommand" } }, { kind: "Field", name: { kind: "Name", value: "sleepApplication" } }, { kind: "Field", name: { kind: "Name", value: "builder" } }, { kind: "Field", name: { kind: "Name", value: "watchPatterns" } }, { kind: "Field", name: { kind: "Name", value: "drainingSeconds" } }, { kind: "Field", name: { kind: "Name", value: "overlapSeconds" } }, { kind: "Field", name: { kind: "Name", value: "ipv6EgressEnabled" } }, { kind: "Field", name: { kind: "Name", value: "railwayConfigFile" } }] } }] } }] } }] } }] } }] } }, { kind: "Field", name: { kind: "Name", value: "buckets" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "name" } }] } }] } }] } }, { kind: "Field", name: { kind: "Name", value: "volumes" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "name" } }, { kind: "Field", name: { kind: "Name", value: "volumeInstances" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "edges" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "node" }, selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "mountPath" } }, { kind: "Field", name: { kind: "Name", value: "environmentId" } }, { kind: "Field", name: { kind: "Name", value: "serviceId" } }] } }] } }] } }] } }] } }] } }] } }] } }] };
38449
- var GetVariablesDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetVariables" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "projectId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "serviceId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "variables" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "projectId" }, value: { kind: "Variable", name: { kind: "Name", value: "projectId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }, { kind: "Argument", name: { kind: "Name", value: "serviceId" }, value: { kind: "Variable", name: { kind: "Name", value: "serviceId" } } }] }] } }] };
38450
- var GetSharedVariablesDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetSharedVariables" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "projectId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "variables" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "projectId" }, value: { kind: "Variable", name: { kind: "Name", value: "projectId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }] }] } }] };
38452
+ var GetVariablesDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetVariables" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "projectId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "serviceId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "variables" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "projectId" }, value: { kind: "Variable", name: { kind: "Name", value: "projectId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }, { kind: "Argument", name: { kind: "Name", value: "serviceId" }, value: { kind: "Variable", name: { kind: "Name", value: "serviceId" } } }, { kind: "Argument", name: { kind: "Name", value: "unrendered" }, value: { kind: "BooleanValue", value: true } }] }] } }] };
38453
+ var GetSharedVariablesDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetSharedVariables" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "projectId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "variables" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "projectId" }, value: { kind: "Variable", name: { kind: "Name", value: "projectId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }, { kind: "Argument", name: { kind: "Name", value: "unrendered" }, value: { kind: "BooleanValue", value: true } }] }] } }] };
38451
38454
  var GetTcpProxiesDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetTcpProxies" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "serviceId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "tcpProxies" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "serviceId" }, value: { kind: "Variable", name: { kind: "Name", value: "serviceId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", name: { kind: "Name", value: "applicationPort" } }, { kind: "Field", name: { kind: "Name", value: "proxyPort" } }, { kind: "Field", name: { kind: "Name", value: "domain" } }] } }] } }] };
38452
38455
  var GetServiceInstanceLimitsDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetServiceInstanceLimits" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "serviceId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "serviceInstanceLimits" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "serviceId" }, value: { kind: "Variable", name: { kind: "Name", value: "serviceId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }] }] } }] };
38453
38456
  var GetEgressGatewaysDocument = { kind: "Document", definitions: [{ kind: "OperationDefinition", operation: "query", name: { kind: "Name", value: "GetEgressGateways" }, variableDefinitions: [{ kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "serviceId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }, { kind: "VariableDefinition", variable: { kind: "Variable", name: { kind: "Name", value: "environmentId" } }, type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "String" } } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "egressGateways" }, arguments: [{ kind: "Argument", name: { kind: "Name", value: "serviceId" }, value: { kind: "Variable", name: { kind: "Name", value: "serviceId" } } }, { kind: "Argument", name: { kind: "Name", value: "environmentId" }, value: { kind: "Variable", name: { kind: "Name", value: "environmentId" } } }], selectionSet: { kind: "SelectionSet", selections: [{ kind: "Field", name: { kind: "Name", value: "ipv4" } }, { kind: "Field", name: { kind: "Name", value: "region" } }] } }] } }] };
@@ -38890,7 +38893,19 @@ function yellow(text, noColor) {
38890
38893
  function dim(text, noColor) {
38891
38894
  return noColor ? text : `\x1B[2m${text}\x1B[0m`;
38892
38895
  }
38893
- var SENSITIVE_PATTERNS = ["PASSWORD", "SECRET", "TOKEN", "KEY", "PRIVATE", "CREDENTIAL"];
38896
+ var SENSITIVE_PATTERNS = [
38897
+ "PASSWORD",
38898
+ "PASSPHRASE",
38899
+ "SECRET",
38900
+ "TOKEN",
38901
+ "KEY",
38902
+ "PRIVATE",
38903
+ "CREDENTIAL",
38904
+ "JWT",
38905
+ "CERT",
38906
+ "SIGNING",
38907
+ "ENCRYPTION"
38908
+ ];
38894
38909
  function isSensitive(key) {
38895
38910
  const upper = key.toUpperCase();
38896
38911
  return SENSITIVE_PATTERNS.some((p) => upper.includes(p));
@@ -38910,22 +38925,25 @@ function describeChange(change) {
38910
38925
  if (change.cronSchedule)
38911
38926
  details.push(`cron: ${change.cronSchedule}`);
38912
38927
  return {
38913
- category: "Services",
38928
+ category: "Service",
38914
38929
  action: "create",
38915
- summary: `${change.name} (${details.join(", ")})`
38930
+ serviceName: change.name,
38931
+ summary: `create (${details.join(", ")})`
38916
38932
  };
38917
38933
  }
38918
38934
  case "delete-service":
38919
38935
  return {
38920
- category: "Services",
38936
+ category: "Service",
38921
38937
  action: "delete",
38922
- summary: `${change.name} (${change.serviceId})`
38938
+ serviceName: change.name,
38939
+ summary: `delete (${change.serviceId})`
38923
38940
  };
38924
38941
  case "update-service-settings":
38925
38942
  return {
38926
- category: "Service settings",
38943
+ category: "Settings",
38927
38944
  action: "update",
38928
- summary: `${change.serviceName}: ${Object.keys(change.settings).join(", ")}`
38945
+ serviceName: change.serviceName,
38946
+ summary: `settings: ${Object.keys(change.settings).join(", ")}`
38929
38947
  };
38930
38948
  case "update-deployment-trigger": {
38931
38949
  const parts = [];
@@ -38934,82 +38952,99 @@ function describeChange(change) {
38934
38952
  if (change.checkSuites !== undefined)
38935
38953
  parts.push(`checkSuites → ${change.checkSuites}`);
38936
38954
  return {
38937
- category: "Deployment triggers",
38955
+ category: "Trigger",
38938
38956
  action: "update",
38939
- summary: `${change.serviceName}: ${parts.join(", ")}`
38957
+ serviceName: change.serviceName,
38958
+ summary: `trigger: ${parts.join(", ")}`
38940
38959
  };
38941
38960
  }
38942
38961
  case "upsert-variables":
38943
38962
  return {
38944
- category: "Service variables",
38963
+ category: "Variables",
38945
38964
  action: "update",
38946
- summary: `${change.serviceName}: set ${Object.keys(change.variables).length} var(s) — ${Object.keys(change.variables).join(", ")}`
38965
+ serviceName: change.serviceName,
38966
+ summary: `set ${Object.keys(change.variables).length} var(s) — ${Object.keys(change.variables).join(", ")}`
38947
38967
  };
38948
38968
  case "delete-variables":
38949
38969
  return {
38950
- category: "Service variables",
38970
+ category: "Variables",
38951
38971
  action: "delete",
38952
- summary: `${change.serviceName}: delete ${change.variableNames.length} var(s) — ${change.variableNames.join(", ")}`
38972
+ serviceName: change.serviceName,
38973
+ summary: `delete ${change.variableNames.length} var(s) — ${change.variableNames.join(", ")}`
38953
38974
  };
38954
38975
  case "upsert-shared-variables":
38955
38976
  return {
38956
38977
  category: "Shared variables",
38957
38978
  action: "update",
38979
+ serviceName: null,
38958
38980
  summary: `set ${Object.keys(change.variables).length} var(s) — ${Object.keys(change.variables).join(", ")}`
38959
38981
  };
38960
38982
  case "delete-shared-variables":
38961
38983
  return {
38962
38984
  category: "Shared variables",
38963
38985
  action: "delete",
38986
+ serviceName: null,
38964
38987
  summary: `delete ${change.variableNames.length} var(s) — ${change.variableNames.join(", ")}`
38965
38988
  };
38966
38989
  case "create-domain": {
38967
38990
  const port = change.targetPort ? ` (port ${change.targetPort})` : "";
38968
38991
  return {
38969
- category: "Domains",
38992
+ category: "Domain",
38970
38993
  action: "create",
38971
- summary: `${change.serviceName}: ${change.domain}${port}`
38994
+ serviceName: change.serviceName,
38995
+ summary: `domain: ${change.domain}${port}`
38972
38996
  };
38973
38997
  }
38974
38998
  case "delete-domain":
38975
38999
  return {
38976
- category: "Domains",
39000
+ category: "Domain",
38977
39001
  action: "delete",
38978
- summary: `${change.serviceName}: ${change.domain}`
39002
+ serviceName: change.serviceName,
39003
+ summary: `domain: ${change.domain}`
38979
39004
  };
38980
39005
  case "create-service-domain": {
38981
39006
  const port = change.targetPort ? ` (port ${change.targetPort})` : "";
38982
39007
  return {
38983
- category: "Railway domains",
39008
+ category: "Railway domain",
38984
39009
  action: "create",
38985
- summary: `${change.serviceName}${port}`
39010
+ serviceName: change.serviceName,
39011
+ summary: `railway domain${port}`
38986
39012
  };
38987
39013
  }
38988
39014
  case "delete-service-domain":
38989
- return { category: "Railway domains", action: "delete", summary: change.serviceName };
39015
+ return {
39016
+ category: "Railway domain",
39017
+ action: "delete",
39018
+ serviceName: change.serviceName,
39019
+ summary: "railway domain"
39020
+ };
38990
39021
  case "create-volume":
38991
39022
  return {
38992
- category: "Volumes",
39023
+ category: "Volume",
38993
39024
  action: "create",
38994
- summary: `${change.serviceName}: ${change.mount}`
39025
+ serviceName: change.serviceName,
39026
+ summary: `volume: ${change.mount}`
38995
39027
  };
38996
39028
  case "delete-volume":
38997
39029
  return {
38998
- category: "Volumes",
39030
+ category: "Volume",
38999
39031
  action: "delete",
39000
- summary: `${change.serviceName} (${change.volumeId})`
39032
+ serviceName: change.serviceName,
39033
+ summary: `volume (${change.volumeId})`
39001
39034
  };
39002
39035
  case "create-tcp-proxy":
39003
39036
  return {
39004
- category: "TCP proxies",
39037
+ category: "TCP proxy",
39005
39038
  action: "create",
39006
- summary: `${change.serviceName}: port ${change.applicationPort}`
39039
+ serviceName: change.serviceName,
39040
+ summary: `tcp proxy: port ${change.applicationPort}`
39007
39041
  };
39008
39042
  case "delete-tcp-proxy":
39009
39043
  return {
39010
- category: "TCP proxies",
39044
+ category: "TCP proxy",
39011
39045
  action: "delete",
39012
- summary: `${change.serviceName}: proxy ${change.proxyId}`
39046
+ serviceName: change.serviceName,
39047
+ summary: `tcp proxy: ${change.proxyId}`
39013
39048
  };
39014
39049
  case "update-service-limits": {
39015
39050
  const parts = [];
@@ -39018,36 +39053,50 @@ function describeChange(change) {
39018
39053
  if (change.limits.vCPUs !== undefined)
39019
39054
  parts.push(`vCPUs: ${change.limits.vCPUs ?? "unset"}`);
39020
39055
  return {
39021
- category: "Resource limits",
39056
+ category: "Limits",
39022
39057
  action: "update",
39023
- summary: `${change.serviceName}: ${parts.join(", ")}`
39058
+ serviceName: change.serviceName,
39059
+ summary: `limits: ${parts.join(", ")}`
39024
39060
  };
39025
39061
  }
39026
39062
  case "enable-static-ips":
39027
39063
  return {
39028
- category: "Static outbound IPs",
39064
+ category: "Static IPs",
39029
39065
  action: "create",
39030
- summary: `${change.serviceName}: enable`
39066
+ serviceName: change.serviceName,
39067
+ summary: "static outbound IPs: enable"
39031
39068
  };
39032
39069
  case "disable-static-ips":
39033
39070
  return {
39034
- category: "Static outbound IPs",
39071
+ category: "Static IPs",
39035
39072
  action: "delete",
39036
- summary: `${change.serviceName}: disable`
39073
+ serviceName: change.serviceName,
39074
+ summary: "static outbound IPs: disable"
39037
39075
  };
39038
39076
  case "create-bucket":
39039
- return { category: "Buckets", action: "create", summary: change.bucketName };
39077
+ return {
39078
+ category: "Buckets",
39079
+ action: "create",
39080
+ serviceName: null,
39081
+ summary: change.bucketName
39082
+ };
39040
39083
  case "delete-bucket":
39041
- return { category: "Buckets", action: "delete", summary: change.name };
39084
+ return { category: "Buckets", action: "delete", serviceName: null, summary: change.name };
39042
39085
  default: {
39043
39086
  const _exhaustive = change;
39044
- return { category: "Unknown", action: "update", summary: _exhaustive.type };
39087
+ return {
39088
+ category: "Unknown",
39089
+ action: "update",
39090
+ serviceName: null,
39091
+ summary: _exhaustive.type
39092
+ };
39045
39093
  }
39046
39094
  }
39047
39095
  }
39048
39096
  function changeLabel(change) {
39049
39097
  const desc = describeChange(change);
39050
- return `${change.type}: ${desc.summary}`;
39098
+ const prefix = desc.serviceName ? `${desc.serviceName}: ` : "";
39099
+ return `${prefix}${desc.summary}`;
39051
39100
  }
39052
39101
  var ACTION_ICON = {
39053
39102
  create: (nc) => green("+", nc),
@@ -39066,44 +39115,60 @@ ${green("No changes needed", noColor)} — Railway matches desired state.
39066
39115
  console.log(`
39067
39116
  Changeset (${changeset.changes.length} changes):
39068
39117
  `);
39069
- const groups = new Map;
39070
- for (const change of changeset.changes) {
39071
- const desc = describeChange(change);
39072
- let group = groups.get(desc.category);
39118
+ const described = changeset.changes.map((change) => ({ change, desc: describeChange(change) }));
39119
+ const serviceGroups = new Map;
39120
+ for (const entry of described) {
39121
+ const key = entry.desc.serviceName;
39122
+ let group = serviceGroups.get(key);
39073
39123
  if (!group) {
39074
39124
  group = [];
39075
- groups.set(desc.category, group);
39125
+ serviceGroups.set(key, group);
39126
+ }
39127
+ group.push(entry);
39128
+ }
39129
+ const projectChanges = serviceGroups.get(null);
39130
+ if (projectChanges) {
39131
+ for (const { change, desc } of projectChanges) {
39132
+ const icon = ACTION_ICON[desc.action](noColor);
39133
+ if (verbose && change.type === "upsert-shared-variables") {
39134
+ console.log(` ${yellow("~", noColor)} Shared variables:`);
39135
+ for (const [key, value] of Object.entries(change.variables)) {
39136
+ const oldVal = options?.currentState?.sharedVariables[key];
39137
+ const oldStr = oldVal !== undefined ? maskValue(key, oldVal) : "(unset)";
39138
+ console.log(` ${icon} ${key}: ${dim(`"${oldStr}"`, noColor)} → ${dim(`"${maskValue(key, value)}"`, noColor)}`);
39139
+ }
39140
+ } else {
39141
+ console.log(` ${icon} ${desc.category}: ${desc.summary}`);
39142
+ }
39076
39143
  }
39077
- group.push({ change, desc });
39144
+ console.log();
39145
+ serviceGroups.delete(null);
39078
39146
  }
39079
- for (const [category, entries] of groups) {
39080
- const actions = new Set(entries.map((e) => e.desc.action));
39081
- const headerAction = actions.size === 1 ? [...actions][0] : "update";
39147
+ for (const [serviceName, entries] of serviceGroups) {
39148
+ const hasCreate = entries.some((e) => e.desc.action === "create" && e.desc.category === "Service");
39149
+ const hasDelete = entries.some((e) => e.desc.action === "delete" && e.desc.category === "Service");
39150
+ const headerAction = hasCreate ? "create" : hasDelete ? "delete" : "update";
39082
39151
  const headerIcon = ACTION_ICON[headerAction](noColor);
39083
- console.log(` ${headerIcon} ${category.toUpperCase()}:`);
39152
+ console.log(` ${headerIcon} ${serviceName}:`);
39084
39153
  for (const { change, desc } of entries) {
39085
39154
  const icon = ACTION_ICON[desc.action](noColor);
39086
39155
  if (verbose && change.type === "update-service-settings") {
39087
- console.log(` ${icon} ${change.serviceName}:`);
39088
39156
  for (const [key, value] of Object.entries(change.settings)) {
39157
+ if (isSensitive(key)) {
39158
+ console.log(` ${icon} ${key}: ***`);
39159
+ continue;
39160
+ }
39089
39161
  const currentSvc = options?.currentState?.services[change.serviceName];
39090
39162
  const oldVal = currentSvc ? currentSvc[key] : undefined;
39091
39163
  const oldStr = oldVal !== undefined ? JSON.stringify(oldVal) : "(unset)";
39092
39164
  const newStr = value === null ? "(unset)" : JSON.stringify(value);
39093
- console.log(` ${key}: ${oldStr} → ${newStr}`);
39165
+ console.log(` ${icon} ${key}: ${oldStr} → ${newStr}`);
39094
39166
  }
39095
39167
  } else if (verbose && change.type === "upsert-variables") {
39096
- console.log(` ${change.serviceName}:`);
39097
39168
  for (const [key, value] of Object.entries(change.variables)) {
39098
39169
  const currentSvc = options?.currentState?.services[change.serviceName];
39099
39170
  const oldVal = currentSvc?.variables[key];
39100
39171
  const oldStr = oldVal !== undefined ? maskValue(key, oldVal) : "(unset)";
39101
- console.log(` ${icon} ${key}: ${dim(`"${oldStr}"`, noColor)} → ${dim(`"${maskValue(key, value)}"`, noColor)}`);
39102
- }
39103
- } else if (verbose && change.type === "upsert-shared-variables") {
39104
- for (const [key, value] of Object.entries(change.variables)) {
39105
- const oldVal = options?.currentState?.sharedVariables[key];
39106
- const oldStr = oldVal !== undefined ? maskValue(key, oldVal) : "(unset)";
39107
39172
  console.log(` ${icon} ${key}: ${dim(`"${oldStr}"`, noColor)} → ${dim(`"${maskValue(key, value)}"`, noColor)}`);
39108
39173
  }
39109
39174
  } else {
@@ -39115,15 +39180,13 @@ Changeset (${changeset.changes.length} changes):
39115
39180
  let createCount = 0;
39116
39181
  let updateCount = 0;
39117
39182
  let deleteCount = 0;
39118
- for (const entries of groups.values()) {
39119
- for (const { desc } of entries) {
39120
- if (desc.action === "create")
39121
- createCount++;
39122
- else if (desc.action === "update")
39123
- updateCount++;
39124
- else
39125
- deleteCount++;
39126
- }
39183
+ for (const { desc } of described) {
39184
+ if (desc.action === "create")
39185
+ createCount++;
39186
+ else if (desc.action === "update")
39187
+ updateCount++;
39188
+ else
39189
+ deleteCount++;
39127
39190
  }
39128
39191
  const parts = [];
39129
39192
  if (createCount > 0)
@@ -39630,12 +39693,12 @@ function diffServiceSettings(desired, current, changes) {
39630
39693
  if (desired.ipv6EgressEnabled !== current.ipv6EgressEnabled) {
39631
39694
  settings.ipv6EgressEnabled = desired.ipv6EgressEnabled ?? null;
39632
39695
  }
39633
- if (desired.registryCredentials) {
39634
- settings.registryCredentials = desired.registryCredentials;
39635
- }
39636
39696
  if (desired.railwayConfigFile !== current.railwayConfigFile) {
39637
39697
  settings.railwayConfigFile = desired.railwayConfigFile ?? null;
39638
39698
  }
39699
+ if (desired.registryCredentials) {
39700
+ settings.registryCredentials = desired.registryCredentials;
39701
+ }
39639
39702
  if (Object.keys(settings).length > 0) {
39640
39703
  changes.push({
39641
39704
  type: "update-service-settings",
@@ -39835,6 +39898,8 @@ function diffStaticOutboundIps(serviceName, desired, current, changes) {
39835
39898
  }
39836
39899
 
39837
39900
  // src/index.ts
39901
+ var require2 = createRequire2(import.meta.url);
39902
+ var { version: version2 } = require2("../package.json");
39838
39903
  async function confirm(message) {
39839
39904
  const rl = createInterface({ input: process.stdin, output: process.stdout });
39840
39905
  return new Promise((res) => {
@@ -39844,7 +39909,21 @@ async function confirm(message) {
39844
39909
  });
39845
39910
  });
39846
39911
  }
39847
- var program2 = new Command().name("railway-deploy").description("Declarative Railway infrastructure management").version("0.1.0").argument("<config>", "Path to environment YAML config file").option("--apply", "Execute changes (default: dry-run)").option("-y, --yes", "Skip confirmation prompts for destructive operations").option("--env-file <path>", "Load .env file for ${VAR} resolution").option("-v, --verbose", "Show detailed diffs (old → new values)").option("--no-color", "Disable ANSI color output").option("--validate", "Validate config without connecting to Railway").action(async (configPath, opts) => {
39912
+ var program2 = new Command().name("railway-deploy").description(`Declarative Railway infrastructure management.
39913
+
39914
+ Define your Railway services, variables, domains, volumes, and more in YAML,
39915
+ and railway-deploy will diff against the live state and apply changes.
39916
+
39917
+ Examples:
39918
+ $ railway-deploy environments/production.yaml
39919
+ $ railway-deploy --apply -y environments/staging.yaml
39920
+ $ railway-deploy --validate environments/production.yaml
39921
+ $ railway-deploy --env-file .env --apply environments/production.yaml
39922
+
39923
+ Environment:
39924
+ RAILWAY_TOKEN Railway API token (required for all operations except --validate)
39925
+
39926
+ Docs: https://github.com/tachyon-gg/railway-deploy`).version(version2).argument("<config>", "path to environment YAML config file").option("--apply", "execute changes (default is dry-run)").option("--env-file <path>", "load .env file for ${VAR} resolution").option("--no-color", "disable colored output").option("--validate", "validate config without connecting to Railway").option("-v, --verbose", "show detailed diffs with old and new values").option("-y, --yes", "skip confirmation prompts for destructive operations").action(async (configPath, opts) => {
39848
39927
  try {
39849
39928
  await run(configPath, opts);
39850
39929
  } catch (err) {
@@ -39906,7 +39985,7 @@ Fetching current state from Railway...`);
39906
39985
  } = await fetchCurrentState(client, projectId, environmentId);
39907
39986
  console.log(`Found ${Object.keys(currentState.services).length} existing service(s)`);
39908
39987
  const changeset = computeChangeset(desiredState, currentState, deletedVars, deletedSharedVars, domainMap, volumeMap, serviceDomainMap, tcpProxyMap);
39909
- const noColor = opts.noColor ?? false;
39988
+ const noColor = opts.color === false;
39910
39989
  const verbose = opts.verbose ?? false;
39911
39990
  printChangeset(changeset, {
39912
39991
  verbose,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tachyon-gg/railway-deploy",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",