From 3f45e0fe35b1e8d88da11d791c56ce9bd96ff6d7 Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Mon, 17 Mar 2025 22:32:05 +0100 Subject: [PATCH] Made "github:environment:create" action idempotent Signed-off-by: Bogdan Nechyporenko --- .changeset/fifty-icons-go.md | 5 + .../src/actions/githubEnvironment.ts | 161 ++++++++++++------ 2 files changed, 113 insertions(+), 53 deletions(-) create mode 100644 .changeset/fifty-icons-go.md diff --git a/.changeset/fifty-icons-go.md b/.changeset/fifty-icons-go.md new file mode 100644 index 0000000000..ad1f39436f --- /dev/null +++ b/.changeset/fifty-icons-go.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Made "github:environment:create" action idempotent diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index f525ac8ff9..f93cb49946 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -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, + }); + }, }); } }