use upsert collection instead of upserting every variable

This commit is contained in:
Thalles Passos
2025-09-17 21:57:16 -03:00
parent 2974938f70
commit fdfd4a5825
2 changed files with 74 additions and 45 deletions
@@ -75,7 +75,7 @@ class RailwayPublicClient {
async send<T extends TRailwayResponse>(
query: string,
options: RailwaySendReqOptions,
variables: Record<string, string | Record<string, string>> = {},
variables: Record<string, unknown> = {},
retryAttempt: number = 0
): Promise<T["data"] | undefined> {
const body = {
@@ -117,6 +117,22 @@ class RailwayPublicClient {
}
}
async getDeployments(config: RailwaySendReqOptions, variables: { input: { serviceId: string; environmentId: string }, first?: number }) {
return await this.send<TRailwayResponse<{ deployments: { edges: { node: { id: string } }[] } }>>(
`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<TRailwayResponse<{ deploymentRedeploy: { id: string } }>>(
`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<TRailwayResponse<{ variables: Record<string, string> }>>(
`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<string, string>; skipDeploys?: boolean; serviceId?: string, replace?: boolean } }
) {
return await this.send<TRailwayResponse<boolean>>(
`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 } }
@@ -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) {