diff --git a/backend/src/services/app-connection/railway/railway-connection-public-client.ts b/backend/src/services/app-connection/railway/railway-connection-public-client.ts index 1c8bd9cc2..85cc309da 100644 --- a/backend/src/services/app-connection/railway/railway-connection-public-client.ts +++ b/backend/src/services/app-connection/railway/railway-connection-public-client.ts @@ -75,7 +75,7 @@ class RailwayPublicClient { async send( query: string, options: RailwaySendReqOptions, - variables: Record> = {}, + variables: Record = {}, retryAttempt: number = 0 ): Promise { const body = { @@ -117,6 +117,22 @@ class RailwayPublicClient { } } + async getDeployments(config: RailwaySendReqOptions, variables: { input: { serviceId: string; environmentId: string }, first?: number }) { + return await this.send>( + `query deployments($input: DeploymentListInput!, $first: Int) { deployments(first: $first, input: $input) { edges { node { id } } } }`, + config, + variables + ); + } + + async redeployDeployment(config: RailwaySendReqOptions, variables: { input: { deploymentId: string } }) { + return await this.send>( + `mutation deploymentRedeploy($deploymentId: String!) { deploymentRedeploy(id: $deploymentId) { id } }`, + config, + { deploymentId: variables.input.deploymentId } + ); + } + async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) { const res = await this.send( `query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`, @@ -213,7 +229,7 @@ class RailwayPublicClient { async deleteVariable( config: RailwaySendReqOptions, - variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } } + variables: { input: { projectId: string; environmentId: string; name: string; skipDeploys?: boolean; serviceId?: string } } ) { await this.send }>>( `mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`, @@ -222,6 +238,17 @@ class RailwayPublicClient { ); } + async upsertCollection( + config: RailwaySendReqOptions, + variables: { input: { projectId: string; environmentId: string; variables: Record; skipDeploys?: boolean; serviceId?: string, replace?: boolean } } + ) { + return await this.send>( + `mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }`, + config, + variables, + ); + } + async upsertVariable( config: RailwaySendReqOptions, variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } } diff --git a/backend/src/services/secret-sync/railway/railway-sync-fns.ts b/backend/src/services/secret-sync/railway/railway-sync-fns.ts index 07862aeb5..ce18e526f 100644 --- a/backend/src/services/secret-sync/railway/railway-sync-fns.ts +++ b/backend/src/services/secret-sync/railway/railway-sync-fns.ts @@ -40,61 +40,63 @@ export const RailwaySyncFns = { } }, + /** + * Syncs secrets to Railway and redeploys the service if needed. + * + * Gets existing Railway vars, merges with new secrets (keeping Railway vars if deletion is disabled), + * then replaces every variable with the new values, if variable is not in the secretMap, it is deleted. + * If there's a service, triggers a redeploy to pick up the changes. + */ async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) { const { - environment, - syncOptions: { disableSecretDeletion, keySchema } + syncOptions: { disableSecretDeletion } } = secretSync; const railwaySecrets = await this.getSecrets(secretSync); const config = secretSync.destinationConfig; - for await (const key of Object.keys(secretMap)) { - try { - const existing = railwaySecrets[key]; + const railwaySecretsMap = Object.fromEntries(Object.entries(railwaySecrets).map(([key, secret]) => [key, secret.value])); + const secretMapMap = Object.fromEntries(Object.entries(secretMap).map(([key, secret]) => [key, secret.value])); - if (existing === undefined || existing.value !== secretMap[key].value) { - await RailwayPublicAPI.upsertVariable(secretSync.connection, { - input: { - projectId: config.projectId, - environmentId: config.environmentId, - serviceId: config.serviceId || undefined, - name: key, - value: secretMap[key].value ?? "" - } - }); - } - } catch (error) { - throw new SecretSyncError({ - error, - secretKey: key - }); + const toReplace = disableSecretDeletion + ? { ...railwaySecretsMap, ...secretMapMap } + : secretMapMap; + + const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, { + input: { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined, + skipDeploys: true, + variables: toReplace, + replace: true, } - } + }); - if (disableSecretDeletion) return; + if (!upserted) throw new SecretSyncError({ + message: "Failed to upsert secrets to Railway", + }) - for await (const key of Object.keys(railwaySecrets)) { - try { - // eslint-disable-next-line no-continue - if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + if (!config.serviceId) return; - if (!secretMap[key]) { - await RailwayPublicAPI.deleteVariable(secretSync.connection, { - input: { - projectId: config.projectId, - environmentId: config.environmentId, - serviceId: config.serviceId || undefined, - name: key - } - }); - } - } catch (error) { - throw new SecretSyncError({ - error, - secretKey: key - }); + const latestDeployment = await RailwayPublicAPI.getDeployments(secretSync.connection, { + input: { + serviceId: config.serviceId, + environmentId: config.environmentId + }, + first: 1, + }); + + const latestDeploymentId = latestDeployment?.deployments.edges[0].node.id; + + if (!latestDeploymentId) throw new SecretSyncError({ + message: "Failed to get latest deployment from Railway", + }) + + await RailwayPublicAPI.redeployDeployment(secretSync.connection, { + input: { + deploymentId: latestDeploymentId } - } + }); }, async removeSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {