diff --git a/.changeset/grumpy-humans-brush.md b/.changeset/grumpy-humans-brush.md new file mode 100644 index 0000000000..dda0b61b3c --- /dev/null +++ b/.changeset/grumpy-humans-brush.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Made gitlab:issue:edit action idempotent. diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.ts index 34c8b1c5e7..c4b131ab60 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.ts @@ -16,15 +16,18 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { + createTemplateAction, + generateStableHash, +} from '@backstage/plugin-scaffolder-node'; import commonGitlabConfig, { - IssueType, IssueStateEvent, + IssueType, } from '../commonGitlabConfig'; import { examples } from './gitlabIssueEdit.examples'; import { z } from 'zod'; import { checkEpicScope, convertDate, getClient, parseRepoUrl } from '../util'; -import { IssueSchema, EditIssueOptions } from '@gitbeaker/rest'; +import { EditIssueOptions, IssueSchema } from '@gitbeaker/rest'; import { getErrorMessage } from './helpers'; const editIssueInputProperties = z.object({ @@ -181,17 +184,24 @@ export const editGitlabIssueAction = (options: { let isEpicScoped = false; - if (epicId) { - isEpicScoped = await checkEpicScope(api, projectId, epicId); + isEpicScoped = await ctx.checkpoint({ + key: `issue.edit.is.scoped.${projectId}.${epicId}`, + fn: async () => { + if (epicId) { + const scoped = await checkEpicScope(api, projectId, epicId); - if (isEpicScoped) { - ctx.logger.info('Epic is within Project Scope'); - } else { - ctx.logger.warn( - 'Chosen epic is not within the Project Scope. The issue will be created without an associated epic.', - ); - } - } + if (scoped) { + ctx.logger.info('Epic is within Project Scope'); + } else { + ctx.logger.warn( + 'Chosen epic is not within the Project Scope. The issue will be created without an associated epic.', + ); + } + return scoped; + } + return false; + }, + }); const mappedUpdatedAt = convertDate( String(updatedAt), @@ -216,19 +226,36 @@ export const editGitlabIssueAction = (options: { weight, }; - const response = (await api.Issues.edit( - projectId, - issueIid, - editIssueOptions, - )) as IssueSchema; + const editedIssue = await ctx.checkpoint({ + key: `issue.edit.${projectId}.${issueIid}.${generateStableHash( + editIssueOptions, + )}`, + fn: async () => { + const response = (await api.Issues.edit( + projectId, + issueIid, + editIssueOptions, + )) as IssueSchema; - ctx.output('issueId', response.id); - ctx.output('projectId', response.project_id); - ctx.output('issueUrl', response.web_url); - ctx.output('issueIid', response.iid); - ctx.output('title', response.title); - ctx.output('state', response.state); - ctx.output('updatedAt', response.updated_at); + return { + issueId: response.id, + issueUrl: response.web_url, + projectId: response.project_id, + issueIid: response.iid, + title: response.title, + state: response.state, + updatedAt: response.updated_at, + }; + }, + }); + + ctx.output('issueId', editedIssue.issueIid); + ctx.output('projectId', editedIssue.projectId); + ctx.output('issueUrl', editedIssue.issueUrl); + ctx.output('issueIid', editedIssue.issueIid); + ctx.output('title', editedIssue.title); + ctx.output('state', editedIssue.state); + ctx.output('updatedAt', editedIssue.updatedAt); } catch (error: any) { if (error instanceof z.ZodError) { // Handling Zod validation errors diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 80dbe0b431..234a1c6a8a 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -62,6 +62,7 @@ "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/types": "workspace:^", "concat-stream": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^11.2.0", "globby": "^11.0.0", "isomorphic-git": "^1.23.0", diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index 2d3e418efb..c4244bbe37 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -219,6 +219,9 @@ export function fetchFile(options: { token?: string; }): Promise; +// @public +export function generateStableHash(entity: object): string; + // @public (undocumented) export const getRepoSourceDirectory: ( workspacePath: string, diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts index c1c8551c53..126ccc13db 100644 --- a/plugins/scaffolder-node/src/actions/index.ts +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -33,4 +33,8 @@ export { createBranch, cloneRepo, } from './gitHelpers'; -export { parseRepoUrl, getRepoSourceDirectory } from './util'; +export { + parseRepoUrl, + getRepoSourceDirectory, + generateStableHash, +} from './util'; diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index 5e4ce60a1a..607d463a09 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import stableStringify from 'fast-json-stable-stringify'; +import { createHash } from 'crypto'; import { InputError } from '@backstage/errors'; import { isChildPath } from '@backstage/backend-plugin-api'; import { join as joinPath, normalize as normalizePath } from 'path'; @@ -124,3 +126,15 @@ function checkRequiredParams(repoUrl: URL, ...params: string[]) { } } } + +/** + * @public + * + * Intended to be used in checkpoint function. + * If the object has to be part of the checkpoint's key, this function will help you create a hash for it. + */ +export function generateStableHash(entity: object) { + return createHash('sha1') + .update(stableStringify({ ...entity })) + .digest('hex'); +} diff --git a/yarn.lock b/yarn.lock index dc2c9de764..30424dff6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7501,6 +7501,7 @@ __metadata: "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/types": "workspace:^" concat-stream: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 fs-extra: ^11.2.0 globby: ^11.0.0 isomorphic-git: ^1.23.0