Made "github:environment:create" action idempotent

Signed-off-by: Bogdan Nechyporenko <bnechyporenko@bol.com>
This commit is contained in:
Bogdan Nechyporenko
2025-03-17 22:32:05 +01:00
parent 5f66007d58
commit 3f45e0fe35
2 changed files with 113 additions and 53 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend-module-github': patch
---
Made "github:environment:create" action idempotent
@@ -194,9 +194,16 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin
});
const client = new Octokit(octokitOptions);
const repository = await client.rest.repos.get({
owner: owner,
repo: repo,
const repositoryId = await ctx.checkpoint({
key: `get.repo.${owner}.${repo}`,
fn: async () => {
const repository = await client.rest.repos.get({
owner: owner,
repo: repo,
});
return repository.data.id;
},
});
// convert reviewers from catalog entity to Github user or team
@@ -219,25 +226,39 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin
for (const reviewerEntityRef of reviewersEntityRefs) {
if (reviewerEntityRef?.kind === 'User') {
try {
const user = await client.rest.users.getByUsername({
username: reviewerEntityRef.metadata.name,
const userId = await ctx.checkpoint({
key: `get.user.${reviewerEntityRef.metadata.name}`,
fn: async () => {
const user = await client.rest.users.getByUsername({
username: reviewerEntityRef.metadata.name,
});
return user.data.id;
},
});
githubReviewers.push({
type: 'User',
id: user.data.id,
id: userId,
});
} catch (error) {
ctx.logger.error('User not found:', error);
}
} else if (reviewerEntityRef?.kind === 'Group') {
try {
const team = await client.rest.teams.getByName({
org: owner,
team_slug: reviewerEntityRef.metadata.name,
const teamId = await ctx.checkpoint({
key: `get.team.${reviewerEntityRef.metadata.name}`,
fn: async () => {
const team = await client.rest.teams.getByName({
org: owner,
team_slug: reviewerEntityRef.metadata.name,
});
return team.data.id;
},
});
githubReviewers.push({
type: 'Team',
id: team.data.id,
id: teamId,
});
} catch (error) {
ctx.logger.error('Team not found:', error);
@@ -246,63 +267,92 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin
}
}
await client.rest.repos.createOrUpdateEnvironment({
owner: owner,
repo: repo,
environment_name: name,
deployment_branch_policy: deploymentBranchPolicy ?? undefined,
wait_timer: waitTimer ?? undefined,
prevent_self_review: preventSelfReview ?? undefined,
reviewers: githubReviewers.length ? githubReviewers : undefined,
await ctx.checkpoint({
key: `create.or.update.environment.${owner}.${repo}.${name}`,
fn: async () => {
await client.rest.repos.createOrUpdateEnvironment({
owner: owner,
repo: repo,
environment_name: name,
deployment_branch_policy: deploymentBranchPolicy ?? undefined,
wait_timer: waitTimer ?? undefined,
prevent_self_review: preventSelfReview ?? undefined,
reviewers: githubReviewers.length ? githubReviewers : undefined,
});
},
});
if (customBranchPolicyNames) {
for (const item of customBranchPolicyNames) {
await client.rest.repos.createDeploymentBranchPolicy({
owner: owner,
repo: repo,
type: 'branch',
environment_name: name,
name: item,
await ctx.checkpoint({
key: `create.deployment.branch.policy.branch.${owner}.${repo}.${name}.${item}`,
fn: async () => {
await client.rest.repos.createDeploymentBranchPolicy({
owner: owner,
repo: repo,
type: 'branch',
environment_name: name,
name: item,
});
},
});
}
}
if (customTagPolicyNames) {
for (const item of customTagPolicyNames) {
await client.rest.repos.createDeploymentBranchPolicy({
owner: owner,
repo: repo,
type: 'tag',
environment_name: name,
name: item,
await ctx.checkpoint({
key: `create.deployment.branch.policy.tag.${owner}.${repo}.${name}.${item}`,
fn: async () => {
await client.rest.repos.createDeploymentBranchPolicy({
owner: owner,
repo: repo,
type: 'tag',
environment_name: name,
name: item,
});
},
});
}
}
for (const [key, value] of Object.entries(environmentVariables ?? {})) {
await client.rest.actions.createEnvironmentVariable({
repository_id: repository.data.id,
owner: owner,
repo: repo,
environment_name: name,
name: key,
value,
await ctx.checkpoint({
key: `create.env.variable.${owner}.${repo}.${name}.${key}`,
fn: async () => {
await client.rest.actions.createEnvironmentVariable({
repository_id: repositoryId,
owner: owner,
repo: repo,
environment_name: name,
name: key,
value,
});
},
});
}
if (secrets) {
const publicKeyResponse =
await client.rest.actions.getEnvironmentPublicKey({
repository_id: repository.data.id,
owner: owner,
repo: repo,
environment_name: name,
});
const { publicKey, publicKeyId } = await ctx.checkpoint({
key: `get.env.public.key.${owner}.${repo}.${name}`,
fn: async () => {
const publicKeyResponse =
await client.rest.actions.getEnvironmentPublicKey({
repository_id: repositoryId,
owner: owner,
repo: repo,
environment_name: name,
});
return {
publicKey: publicKeyResponse.data.key,
publicKeyId: publicKeyResponse.data.key_id,
};
},
});
await Sodium.ready;
const binaryKey = Sodium.from_base64(
publicKeyResponse.data.key,
publicKey,
Sodium.base64_variants.ORIGINAL,
);
for (const [key, value] of Object.entries(secrets)) {
@@ -316,14 +366,19 @@ Wildcard characters will not match \`/\`. For example, to match tags that begin
Sodium.base64_variants.ORIGINAL,
);
await client.rest.actions.createOrUpdateEnvironmentSecret({
repository_id: repository.data.id,
owner: owner,
repo: repo,
environment_name: name,
secret_name: key,
encrypted_value: encryptedBase64Secret,
key_id: publicKeyResponse.data.key_id,
await ctx.checkpoint({
key: `create.or.update.env.secret.${owner}.${repo}.${name}.${key}`,
fn: async () => {
await client.rest.actions.createOrUpdateEnvironmentSecret({
repository_id: repositoryId,
owner: owner,
repo: repo,
environment_name: name,
secret_name: key,
encrypted_value: encryptedBase64Secret,
key_id: publicKeyId,
});
},
});
}
}