Skip to content

Commit b54e3d1

Browse files
authored
Merge pull request #4548 from ThallesP/main
fix: use upsert collection instead of upserting each variable on Railway
2 parents 0e97a3c + d879c1e commit b54e3d1

File tree

2 files changed

+133
-60
lines changed

2 files changed

+133
-60
lines changed

backend/src/services/app-connection/railway/railway-connection-public-client.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class RailwayPublicClient {
7575
async send<T extends TRailwayResponse>(
7676
query: string,
7777
options: RailwaySendReqOptions,
78-
variables: Record<string, string | Record<string, string>> = {},
78+
variables: Record<string, unknown> = {},
7979
retryAttempt: number = 0
8080
): Promise<T["data"] | undefined> {
8181
const body = {
@@ -117,6 +117,25 @@ class RailwayPublicClient {
117117
}
118118
}
119119

120+
async getDeployments(
121+
config: RailwaySendReqOptions,
122+
variables: { input: { serviceId: string; environmentId: string }; first?: number }
123+
) {
124+
return this.send<TRailwayResponse<{ deployments: { edges: { node: { id: string } }[] } }>>(
125+
`query deployments($input: DeploymentListInput!, $first: Int) { deployments(first: $first, input: $input) { edges { node { id } } } }`,
126+
config,
127+
variables
128+
);
129+
}
130+
131+
async redeployDeployment(config: RailwaySendReqOptions, variables: { input: { deploymentId: string } }) {
132+
return this.send<TRailwayResponse<{ deploymentRedeploy: { id: string } }>>(
133+
`mutation deploymentRedeploy($deploymentId: String!) { deploymentRedeploy(id: $deploymentId) { id } }`,
134+
config,
135+
{ deploymentId: variables.input.deploymentId }
136+
);
137+
}
138+
120139
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
121140
const res = await this.send(
122141
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
@@ -213,7 +232,9 @@ class RailwayPublicClient {
213232

214233
async deleteVariable(
215234
config: RailwaySendReqOptions,
216-
variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } }
235+
variables: {
236+
input: { projectId: string; environmentId: string; name: string; skipDeploys?: boolean; serviceId?: string };
237+
}
217238
) {
218239
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
219240
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
@@ -222,6 +243,26 @@ class RailwayPublicClient {
222243
);
223244
}
224245

246+
async upsertCollection(
247+
config: RailwaySendReqOptions,
248+
variables: {
249+
input: {
250+
projectId: string;
251+
environmentId: string;
252+
variables: Record<string, string>;
253+
skipDeploys?: boolean;
254+
serviceId?: string;
255+
replace?: boolean;
256+
};
257+
}
258+
) {
259+
return this.send<TRailwayResponse<boolean>>(
260+
`mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }`,
261+
config,
262+
variables
263+
);
264+
}
265+
225266
async upsertVariable(
226267
config: RailwaySendReqOptions,
227268
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }

backend/src/services/secret-sync/railway/railway-sync-fns.ts

Lines changed: 90 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export const RailwaySyncFns = {
1212
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
1313
try {
1414
const config = secretSync.destinationConfig;
15+
const { keySchema } = secretSync.syncOptions;
16+
const { environment } = secretSync;
1517

1618
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
1719
projectId: config.projectId,
@@ -26,6 +28,10 @@ export const RailwaySyncFns = {
2628
// eslint-disable-next-line no-continue
2729
if (key.startsWith("RAILWAY_")) continue;
2830

31+
// Check if key matches the schema
32+
// eslint-disable-next-line no-continue
33+
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
34+
2935
entries[key] = {
3036
value
3137
};
@@ -40,85 +46,111 @@ export const RailwaySyncFns = {
4046
}
4147
},
4248

49+
/**
50+
* Syncs secrets to Railway and redeploys the service if needed.
51+
*
52+
* Gets existing Railway vars, merges with new secrets (keeping Railway vars if deletion is disabled),
53+
* then replaces every variable with the new values, if variable is not in the secretMap, it is deleted.
54+
* If there's a service, triggers a redeploy to pick up the changes.
55+
*/
4356
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
44-
const {
45-
environment,
46-
syncOptions: { disableSecretDeletion, keySchema }
47-
} = secretSync;
48-
const railwaySecrets = await this.getSecrets(secretSync);
49-
const config = secretSync.destinationConfig;
57+
try {
58+
const {
59+
syncOptions: { disableSecretDeletion }
60+
} = secretSync;
61+
const railwaySecrets = await this.getSecrets(secretSync);
62+
const config = secretSync.destinationConfig;
5063

51-
for await (const key of Object.keys(secretMap)) {
52-
try {
53-
const existing = railwaySecrets[key];
54-
55-
if (existing === undefined || existing.value !== secretMap[key].value) {
56-
await RailwayPublicAPI.upsertVariable(secretSync.connection, {
57-
input: {
58-
projectId: config.projectId,
59-
environmentId: config.environmentId,
60-
serviceId: config.serviceId || undefined,
61-
name: key,
62-
value: secretMap[key].value ?? ""
63-
}
64-
});
64+
const railwaySecretsMap = Object.fromEntries(
65+
Object.entries(railwaySecrets).map(([key, secret]) => [key, secret.value])
66+
);
67+
const secretMapMap = Object.fromEntries(Object.entries(secretMap).map(([key, secret]) => [key, secret.value]));
68+
69+
const toReplace = disableSecretDeletion ? { ...railwaySecretsMap, ...secretMapMap } : secretMapMap;
70+
71+
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
72+
input: {
73+
projectId: config.projectId,
74+
environmentId: config.environmentId,
75+
serviceId: config.serviceId || undefined,
76+
skipDeploys: true,
77+
variables: toReplace,
78+
replace: true
6579
}
66-
} catch (error) {
80+
});
81+
82+
if (!upserted)
6783
throw new SecretSyncError({
68-
error,
69-
secretKey: key
84+
message: "Failed to upsert secrets to Railway"
7085
});
71-
}
72-
}
7386

74-
if (disableSecretDeletion) return;
87+
if (!config.serviceId) return;
7588

76-
for await (const key of Object.keys(railwaySecrets)) {
77-
try {
78-
// eslint-disable-next-line no-continue
79-
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
89+
const latestDeployment = await RailwayPublicAPI.getDeployments(secretSync.connection, {
90+
input: {
91+
serviceId: config.serviceId,
92+
environmentId: config.environmentId
93+
},
94+
first: 1
95+
});
8096

81-
if (!secretMap[key]) {
82-
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
83-
input: {
84-
projectId: config.projectId,
85-
environmentId: config.environmentId,
86-
serviceId: config.serviceId || undefined,
87-
name: key
88-
}
89-
});
90-
}
91-
} catch (error) {
97+
const latestDeploymentId = latestDeployment?.deployments.edges[0].node.id;
98+
99+
if (!latestDeploymentId)
92100
throw new SecretSyncError({
93-
error,
94-
secretKey: key
101+
message: "Failed to get latest deployment from Railway"
95102
});
96-
}
103+
104+
await RailwayPublicAPI.redeployDeployment(secretSync.connection, {
105+
input: {
106+
deploymentId: latestDeploymentId
107+
}
108+
});
109+
} catch (error) {
110+
if (error instanceof SecretSyncError) throw error;
111+
112+
throw new SecretSyncError({
113+
error,
114+
message: "Failed to sync secrets to Railway"
115+
});
97116
}
98117
},
99118

100119
async removeSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
101120
const existing = await this.getSecrets(secretSync);
102121
const config = secretSync.destinationConfig;
103122

104-
for await (const secret of Object.keys(existing)) {
105-
try {
106-
if (secret in secretMap) {
107-
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
108-
input: {
109-
projectId: config.projectId,
110-
environmentId: config.environmentId,
111-
serviceId: config.serviceId || undefined,
112-
name: secret
113-
}
114-
});
123+
// Create a new variables object excluding secrets that exist in secretMap
124+
const remainingVariables = Object.fromEntries(
125+
Object.entries(existing)
126+
.filter(([key]) => !(key in secretMap))
127+
.map(([key, secret]) => [key, secret.value])
128+
);
129+
130+
try {
131+
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
132+
input: {
133+
projectId: config.projectId,
134+
environmentId: config.environmentId,
135+
serviceId: config.serviceId || undefined,
136+
skipDeploys: true,
137+
variables: remainingVariables,
138+
replace: true
115139
}
116-
} catch (error) {
140+
});
141+
142+
if (!upserted) {
117143
throw new SecretSyncError({
118-
error,
119-
secretKey: secret
144+
message: "Failed to remove secrets from Railway"
120145
});
121146
}
147+
} catch (error) {
148+
if (error instanceof SecretSyncError) throw error;
149+
150+
throw new SecretSyncError({
151+
error,
152+
message: "Failed to remove secrets from Railway"
153+
});
122154
}
123155
}
124156
};

0 commit comments

Comments
 (0)