From 225c73325806522efe27d54faa6e2bd0191915ce Mon Sep 17 00:00:00 2001 From: Bogdan Nechyporenko Date: Wed, 12 Mar 2025 19:40:46 +0100 Subject: [PATCH] Made gitlab:repo:push action idempotent. Signed-off-by: Bogdan Nechyporenko --- .changeset/good-chairs-visit.md | 5 ++ .../README.md | 63 ++++++++++++------- .../src/actions/gitlabRepoPush.ts | 52 +++++++++------ 3 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 .changeset/good-chairs-visit.md diff --git a/.changeset/good-chairs-visit.md b/.changeset/good-chairs-visit.md new file mode 100644 index 0000000000..15aa04a855 --- /dev/null +++ b/.changeset/good-chairs-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Made gitlab:repo:push action idempotent. diff --git a/beps/0004-scaffolder-task-idempotency/README.md b/beps/0004-scaffolder-task-idempotency/README.md index 208446158e..9c0fc7077f 100644 --- a/beps/0004-scaffolder-task-idempotency/README.md +++ b/beps/0004-scaffolder-task-idempotency/README.md @@ -154,30 +154,36 @@ export function createGithubRepoCreateAction(options: { username: owner, }); - await ctx.checkpoint('repo.creation', async () => { - const repoCreationPromise = - user.data.type === 'Organization' - ? client.rest.repos.createInOrg({ - name: repo, - org: owner, - }) - : client.rest.repos.createForAuthenticatedUser({ - name: repo, - }); - const { repoUrl } = await repoCreationPromise; - return { repoUrl }; + await ctx.checkpoint({ + key: 'repo.creation.v1', + fn: async () => { + const repoCreationPromise = + user.data.type === 'Organization' + ? client.rest.repos.createInOrg({ + name: repo, + org: owner, + }) + : client.rest.repos.createForAuthenticatedUser({ + name: repo, + }); + const { repoUrl } = await repoCreationPromise; + return { repoUrl }; + }, }); if (secrets) { - await ctx.checkpoint('repo.create.variables', async () => { - for (const [key, value] of Object.entries(repoVariables ?? {})) { - await client.rest.actions.createRepoVariable({ - owner, - repo, - name: key, - value: value, - }); - } + await ctx.checkpoint({ + key: 'repo.create.variables', + fn: async () => { + for (const [key, value] of Object.entries(repoVariables ?? {})) { + await client.rest.actions.createRepoVariable({ + owner, + repo, + name: key, + value: value, + }); + } + }, }); } @@ -202,9 +208,12 @@ Checkpoints will allow action authors to create actions where code paths are ign This will be provided on a context object and action of author provide a key and a callback. ```typescript -await ctx.checkpoint('repo.creation', async () => { - const { repoUrl } = await client.rest.Repository.create({}); - return { repoUrl }; +await ctx.checkpoint({ + key: 'repo.creation', + fn: async () => { + const { repoUrl } = await client.rest.Repository.create({}); + return { repoUrl }; + }, }); ``` @@ -259,6 +268,12 @@ Task state will be stored in the extra column `state` in the table `tasks` with } ``` +Whenever you change the return type of the checkpoint, we encourage you to change the ID. +For example, you can embed the versioning or another indicator for that. +If you'll preserve the same key, and you'll try to restart the affected task, it will fail on this checkpoint. +The cached result will not match with the expected updated return type. +By changing the key, you'll invalidate the cache of the checkpoint. + #### Workspace Persistence The workspace will be serialized and stored in the database by default. This serialization should occur at the end of a step, and after each checkpoint. It will be possible to provide additional modules to extend the workspace serialization to other providers, such as GCS or S3 instead of the database. diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts index 6537c42315..f347aaadd0 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabRepoPush.ts @@ -151,19 +151,24 @@ export const createGitlabRepoPushAction = (options: { execute_filemode: file.executable, })); - let branchExists = false; - try { - await api.Branches.show(repoID, branchName); - branchExists = true; - } catch (e: any) { - if (e.cause?.response?.status !== 404) { - throw new InputError( - `Failed to check status of branch '${branchName}'. Please make sure that branch already exists or Backstage has permissions to create one. ${getErrorMessage( - e, - )}`, - ); - } - } + const branchExists = await ctx.checkpoint({ + key: `branch.exists.${repoID}.${branchName}`, + fn: async () => { + try { + await api.Branches.show(repoID, branchName); + return true; + } catch (e: any) { + if (e.cause?.response?.status !== 404) { + throw new InputError( + `Failed to check status of branch '${branchName}'. Please make sure that branch already exists or Backstage has permissions to create one. ${getErrorMessage( + e, + )}`, + ); + } + } + return false; + }, + }); if (!branchExists) { // create a branch using the default branch as ref @@ -181,15 +186,22 @@ export const createGitlabRepoPushAction = (options: { } try { - const commit = await api.Commits.create( - repoID, - branchName, - ctx.input.commitMessage, - actions, - ); + const commitId = await ctx.checkpoint({ + key: `commit.create.${repoID}.${branchName}`, + fn: async () => { + const commit = await api.Commits.create( + repoID, + branchName, + ctx.input.commitMessage, + actions, + ); + return commit.id; + }, + }); + ctx.output('projectid', repoID); ctx.output('projectPath', repoID); - ctx.output('commitHash', commit.id); + ctx.output('commitHash', commitId); } catch (e) { throw new InputError( `Committing the changes to ${branchName} failed. Please check that none of the files created by the template already exists. ${getErrorMessage(