From 6b8fa3fc4562973f69c318d76f0eca973af0a6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kancer=20=28Nilay=29=20G=C3=B6k=C4=B1rmak?= Date: Sun, 6 Sep 2020 20:13:46 +0200 Subject: [PATCH] feat: allow setting editorial workflow PR label prefix (#4181) --- .../netlify-cms-backend-bitbucket/src/API.ts | 11 +++-- .../src/implementation.ts | 4 ++ .../src/implementation.ts | 3 ++ .../netlify-cms-backend-github/src/API.ts | 41 ++++++++++------- .../src/__tests__/API.spec.js | 34 ++++++++++++++ .../src/implementation.tsx | 3 ++ .../netlify-cms-backend-gitlab/src/API.ts | 17 ++++--- .../src/implementation.ts | 3 ++ .../src/implementation.ts | 13 +++++- packages/netlify-cms-core/index.d.ts | 1 + .../src/constants/configSchema.js | 1 + packages/netlify-cms-lib-util/src/APIUtils.ts | 13 ++++-- .../src/__tests__/apiUtils.spec.js | 45 ++++++++++++++++--- .../src/implementation.ts | 1 + .../src/middlewares/joi/index.ts | 3 ++ .../src/middlewares/localGit/index.ts | 17 ++++--- .../src/middlewares/types.ts | 3 ++ website/content/docs/backends-overview.md | 1 + 18 files changed, 170 insertions(+), 44 deletions(-) diff --git a/packages/netlify-cms-backend-bitbucket/src/API.ts b/packages/netlify-cms-backend-bitbucket/src/API.ts index b814384b..02b0f070 100644 --- a/packages/netlify-cms-backend-bitbucket/src/API.ts +++ b/packages/netlify-cms-backend-bitbucket/src/API.ts @@ -41,6 +41,7 @@ interface Config { hasWriteAccess?: () => Promise; squashMerges: boolean; initialWorkflowStatus: string; + cmsLabelPrefix: string; } interface CommitAuthor { @@ -203,6 +204,7 @@ export default class API { commitAuthor?: CommitAuthor; mergeStrategy: string; initialWorkflowStatus: string; + cmsLabelPrefix: string; constructor(config: Config) { this.apiRoot = config.apiRoot || 'https://api.bitbucket.org/2.0'; @@ -214,6 +216,7 @@ export default class API { this.repoURL = this.repo ? `/repositories/${this.repo}` : ''; this.mergeStrategy = config.squashMerges ? 'squash' : 'merge_commit'; this.initialWorkflowStatus = config.initialWorkflowStatus; + this.cmsLabelPrefix = config.cmsLabelPrefix; } buildRequest = (req: ApiRequest) => { @@ -554,7 +557,7 @@ export default class API { }), }); // use comments for status labels - await this.addPullRequestComment(pullRequest, statusToLabel(status)); + await this.addPullRequestComment(pullRequest, statusToLabel(status, this.cmsLabelPrefix)); } async getDifferences(source: string, destination: string = this.branch) { @@ -656,7 +659,7 @@ export default class API { pullRequests.values.map(pr => this.getPullRequestLabel(pr.id)), ); - return pullRequests.values.filter((_, index) => isCMSLabel(labels[index])); + return pullRequests.values.filter((_, index) => isCMSLabel(labels[index], this.cmsLabelPrefix)); } async getBranchPullRequest(branch: string) { @@ -686,7 +689,7 @@ export default class API { const pullRequest = await this.getBranchPullRequest(branch); const diffs = await this.getDifferences(branch); const label = await this.getPullRequestLabel(pullRequest.id); - const status = labelToStatus(label); + const status = labelToStatus(label, this.cmsLabelPrefix); const updatedAt = pullRequest.updated_on; return { collection, @@ -705,7 +708,7 @@ export default class API { const branch = branchFromContentKey(contentKey); const pullRequest = await this.getBranchPullRequest(branch); - await this.addPullRequestComment(pullRequest, statusToLabel(newStatus)); + await this.addPullRequestComment(pullRequest, statusToLabel(newStatus, this.cmsLabelPrefix)); } async mergePullRequest(pullRequest: BitBucketPullRequest) { diff --git a/packages/netlify-cms-backend-bitbucket/src/implementation.ts b/packages/netlify-cms-backend-bitbucket/src/implementation.ts index d6b49f43..5a91f438 100644 --- a/packages/netlify-cms-backend-bitbucket/src/implementation.ts +++ b/packages/netlify-cms-backend-bitbucket/src/implementation.ts @@ -78,6 +78,7 @@ export default class BitbucketBackend implements Implementation { authenticator?: NetlifyAuthenticator; _mediaDisplayURLSem?: Semaphore; squashMerges: boolean; + cmsLabelPrefix: string; previewContext: string; largeMediaURL: string; _largeMediaClientPromise?: Promise; @@ -113,6 +114,7 @@ export default class BitbucketBackend implements Implementation { this.token = ''; this.mediaFolder = config.media_folder; this.squashMerges = config.backend.squash_merges || false; + this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.previewContext = config.backend.preview_context || ''; this.lock = asyncLock(); this.authType = config.backend.auth_type || ''; @@ -166,6 +168,7 @@ export default class BitbucketBackend implements Implementation { branch: this.branch, repo: this.repo, squashMerges: this.squashMerges, + cmsLabelPrefix: this.cmsLabelPrefix, initialWorkflowStatus: this.options.initialWorkflowStatus, }); } @@ -189,6 +192,7 @@ export default class BitbucketBackend implements Implementation { repo: this.repo, apiRoot: this.apiRoot, squashMerges: this.squashMerges, + cmsLabelPrefix: this.cmsLabelPrefix, initialWorkflowStatus: this.options.initialWorkflowStatus, }); diff --git a/packages/netlify-cms-backend-git-gateway/src/implementation.ts b/packages/netlify-cms-backend-git-gateway/src/implementation.ts index d5e82273..9a68ab33 100644 --- a/packages/netlify-cms-backend-git-gateway/src/implementation.ts +++ b/packages/netlify-cms-backend-git-gateway/src/implementation.ts @@ -132,6 +132,7 @@ export default class GitGateway implements Implementation { api?: GitHubAPI | GitLabAPI | BitBucketAPI; branch: string; squashMerges: boolean; + cmsLabelPrefix: string; mediaFolder: string; transformImages: boolean; gatewayUrl: string; @@ -159,6 +160,7 @@ export default class GitGateway implements Implementation { this.config = config; this.branch = config.backend.branch?.trim() || 'master'; this.squashMerges = config.backend.squash_merges || false; + this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.mediaFolder = config.media_folder; this.transformImages = config.backend.use_large_media_transforms_in_media_library || true; @@ -332,6 +334,7 @@ export default class GitGateway implements Implementation { tokenPromise: this.tokenPromise!, commitAuthor: pick(userData, ['name', 'email']), squashMerges: this.squashMerges, + cmsLabelPrefix: this.cmsLabelPrefix, initialWorkflowStatus: this.options.initialWorkflowStatus, }; diff --git a/packages/netlify-cms-backend-github/src/API.ts b/packages/netlify-cms-backend-github/src/API.ts index dc32a0d0..fee5926b 100644 --- a/packages/netlify-cms-backend-github/src/API.ts +++ b/packages/netlify-cms-backend-github/src/API.ts @@ -52,6 +52,7 @@ export interface Config { originRepo?: string; squashMerges: boolean; initialWorkflowStatus: string; + cmsLabelPrefix: string; } interface TreeFile { @@ -132,8 +133,10 @@ type MediaFile = { path: string; }; -const withCmsLabel = (pr: GitHubPull) => pr.labels.some(l => isCMSLabel(l.name)); -const withoutCmsLabel = (pr: GitHubPull) => pr.labels.every(l => !isCMSLabel(l.name)); +const withCmsLabel = (pr: GitHubPull, cmsLabelPrefix: string) => + pr.labels.some(l => isCMSLabel(l.name, cmsLabelPrefix)); +const withoutCmsLabel = (pr: GitHubPull, cmsLabelPrefix: string) => + pr.labels.every(l => !isCMSLabel(l.name, cmsLabelPrefix)); const getTreeFiles = (files: GitHubCompareFiles) => { const treeFiles = files.reduce((arr, file) => { @@ -190,6 +193,7 @@ export default class API { originRepoURL: string; mergeMethod: string; initialWorkflowStatus: string; + cmsLabelPrefix: string; _userPromise?: Promise; _metadataSemaphore?: Semaphore; @@ -215,6 +219,7 @@ export default class API { this.originRepoName = originRepoParts[1]; this.mergeMethod = config.squashMerges ? 'squash' : 'merge'; + this.cmsLabelPrefix = config.cmsLabelPrefix; this.initialWorkflowStatus = config.initialWorkflowStatus; } @@ -527,18 +532,18 @@ export default class API { return { head: { sha: data.commit.sha }, number: MOCK_PULL_REQUEST, - labels: [{ name: statusToLabel(this.initialWorkflowStatus) }], + labels: [{ name: statusToLabel(this.initialWorkflowStatus, this.cmsLabelPrefix) }], state: PullRequestState.Open, } as GitHubPull; } catch (e) { throw new EditorialWorkflowError('content is not under editorial workflow', true); } } else { - pullRequest.labels = pullRequest.labels.filter(l => !isCMSLabel(l.name)); + pullRequest.labels = pullRequest.labels.filter(l => !isCMSLabel(l.name, this.cmsLabelPrefix)); const cmsLabel = pullRequest.state === PullRequestState.Closed - ? { name: statusToLabel(this.initialWorkflowStatus) } - : { name: statusToLabel('pending_review') }; + ? { name: statusToLabel(this.initialWorkflowStatus, this.cmsLabelPrefix) } + : { name: statusToLabel('pending_review', this.cmsLabelPrefix) }; pullRequest.labels.push(cmsLabel as Octokit.PullsGetResponseLabelsItem); return pullRequest; @@ -550,7 +555,9 @@ export default class API { const pullRequests = await this.getPullRequests(branch, PullRequestState.All, () => true); return this.getOpenAuthoringPullRequest(branch, pullRequests); } else { - const pullRequests = await this.getPullRequests(branch, PullRequestState.Open, withCmsLabel); + const pullRequests = await this.getPullRequests(branch, PullRequestState.Open, pr => + withCmsLabel(pr, this.cmsLabelPrefix), + ); if (pullRequests.length <= 0) { throw new EditorialWorkflowError('content is not under editorial workflow', true); } @@ -579,8 +586,10 @@ export default class API { const pullRequest = await this.getBranchPullRequest(branch); const { files } = await this.getDifferences(this.branch, pullRequest.head.sha); const diffs = files.map(diffFromFile); - const label = pullRequest.labels.find(l => isCMSLabel(l.name)) as { name: string }; - const status = labelToStatus(label.name); + const label = pullRequest.labels.find(l => isCMSLabel(l.name, this.cmsLabelPrefix)) as { + name: string; + }; + const status = labelToStatus(label.name, this.cmsLabelPrefix); const updatedAt = pullRequest.updated_at; return { collection, @@ -823,7 +832,7 @@ export default class API { const pullRequests = await this.getPullRequests( undefined, PullRequestState.Open, - pr => !pr.head.repo.fork && withoutCmsLabel(pr), + pr => !pr.head.repo.fork && withoutCmsLabel(pr, this.cmsLabelPrefix), ); let prCount = 0; for (const pr of pullRequests) { @@ -838,10 +847,8 @@ export default class API { prCount = prCount + 1; await this.migratePullRequest(pr, `${prCount} of ${pullRequests.length}`); } - const cmsPullRequests = await this.getPullRequests( - undefined, - PullRequestState.Open, - withCmsLabel, + const cmsPullRequests = await this.getPullRequests(undefined, PullRequestState.Open, pr => + withCmsLabel(pr, this.cmsLabelPrefix), ); branches = cmsPullRequests.map(pr => pr.head.ref); } @@ -1098,8 +1105,10 @@ export default class API { async setPullRequestStatus(pullRequest: GitHubPull, newStatus: string) { const labels = [ - ...pullRequest.labels.filter(label => !isCMSLabel(label.name)).map(l => l.name), - statusToLabel(newStatus), + ...pullRequest.labels + .filter(label => !isCMSLabel(label.name, this.cmsLabelPrefix)) + .map(l => l.name), + statusToLabel(newStatus, this.cmsLabelPrefix), ]; await this.updatePullRequestLabels(pullRequest.number, labels); } diff --git a/packages/netlify-cms-backend-github/src/__tests__/API.spec.js b/packages/netlify-cms-backend-github/src/__tests__/API.spec.js index 25b4bb9b..e6a59d56 100644 --- a/packages/netlify-cms-backend-github/src/__tests__/API.spec.js +++ b/packages/netlify-cms-backend-github/src/__tests__/API.spec.js @@ -51,6 +51,40 @@ describe('github API', () => { })), ).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['netlify-cms/draft'] }); }); + + it('should create PR with correct base branch name with custom prefix when publishing with editorial workflow', () => { + let prBaseBranch = null; + let labels = null; + const api = new API({ + branch: 'gh-pages', + repo: 'owner/my-repo', + initialWorkflowStatus: 'draft', + cmsLabelPrefix: 'other/', + }); + const responses = { + '/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }), + '/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }), + '/repos/owner/my-repo/git/trees': () => ({}), + '/repos/owner/my-repo/git/commits': () => ({}), + '/repos/owner/my-repo/git/refs': () => ({}), + '/repos/owner/my-repo/pulls': req => { + prBaseBranch = JSON.parse(req.body).base; + return { head: { sha: 'cbd' }, labels: [], number: 1 }; + }, + '/repos/owner/my-repo/issues/1/labels': req => { + labels = JSON.parse(req.body).labels; + return {}; + }, + }; + mockAPI(api, responses); + + return expect( + api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({ + prBaseBranch, + labels, + })), + ).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['other/draft'] }); + }); }); describe('updateTree', () => { diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index 7a989a9b..0f1f23d1 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -72,6 +72,7 @@ export default class GitHub implements Implementation { previewContext: string; token: string | null; squashMerges: boolean; + cmsLabelPrefix: string; useGraphql: boolean; _currentUserPromise?: Promise; _userIsOriginMaintainerPromises?: { @@ -111,6 +112,7 @@ export default class GitHub implements Implementation { this.apiRoot = config.backend.api_root || 'https://api.github.com'; this.token = ''; this.squashMerges = config.backend.squash_merges || false; + this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.useGraphql = config.backend.use_graphql || false; this.mediaFolder = config.media_folder; this.previewContext = config.backend.preview_context || ''; @@ -297,6 +299,7 @@ export default class GitHub implements Implementation { originRepo: this.originRepo, apiRoot: this.apiRoot, squashMerges: this.squashMerges, + cmsLabelPrefix: this.cmsLabelPrefix, useOpenAuthoring: this.useOpenAuthoring, initialWorkflowStatus: this.options.initialWorkflowStatus, }); diff --git a/packages/netlify-cms-backend-gitlab/src/API.ts b/packages/netlify-cms-backend-gitlab/src/API.ts index 26ba7771..6d36350b 100644 --- a/packages/netlify-cms-backend-gitlab/src/API.ts +++ b/packages/netlify-cms-backend-gitlab/src/API.ts @@ -41,6 +41,7 @@ export interface Config { repo?: string; squashMerges: boolean; initialWorkflowStatus: string; + cmsLabelPrefix: string; } export interface CommitAuthor { @@ -189,6 +190,7 @@ export default class API { commitAuthor?: CommitAuthor; squashMerges: boolean; initialWorkflowStatus: string; + cmsLabelPrefix: string; constructor(config: Config) { this.apiRoot = config.apiRoot || 'https://gitlab.com/api/v4'; @@ -198,6 +200,7 @@ export default class API { this.repoURL = `/projects/${encodeURIComponent(this.repo)}`; this.squashMerges = config.squashMerges; this.initialWorkflowStatus = config.initialWorkflowStatus; + this.cmsLabelPrefix = config.cmsLabelPrefix; } withAuthorizationHeaders = (req: ApiRequest) => { @@ -557,7 +560,9 @@ export default class API { }); return mergeRequests.filter( - mr => mr.source_branch.startsWith(CMS_BRANCH_PREFIX) && mr.labels.some(isCMSLabel), + mr => + mr.source_branch.startsWith(CMS_BRANCH_PREFIX) && + mr.labels.some(l => isCMSLabel(l, this.cmsLabelPrefix)), ); } @@ -658,8 +663,8 @@ export default class API { return { id, path, newFile }; }), ); - const label = mergeRequest.labels.find(isCMSLabel) as string; - const status = labelToStatus(label); + const label = mergeRequest.labels.find(l => isCMSLabel(l, this.cmsLabelPrefix)) as string; + const status = labelToStatus(label, this.cmsLabelPrefix); const updatedAt = mergeRequest.updated_at; return { collection, @@ -710,7 +715,7 @@ export default class API { target_branch: this.branch, title: commitMessage, description: DEFAULT_PR_BODY, - labels: statusToLabel(status), + labels: statusToLabel(status, this.cmsLabelPrefix), // eslint-disable-next-line @typescript-eslint/camelcase remove_source_branch: true, squash: this.squashMerges, @@ -771,8 +776,8 @@ export default class API { const mergeRequest = await this.getBranchMergeRequest(branch); const labels = [ - ...mergeRequest.labels.filter(label => !isCMSLabel(label)), - statusToLabel(newStatus), + ...mergeRequest.labels.filter(label => !isCMSLabel(label, this.cmsLabelPrefix)), + statusToLabel(newStatus, this.cmsLabelPrefix), ]; await this.updateMergeRequestLabels(mergeRequest, labels); } diff --git a/packages/netlify-cms-backend-gitlab/src/implementation.ts b/packages/netlify-cms-backend-gitlab/src/implementation.ts index ee9d4515..bc61a8af 100644 --- a/packages/netlify-cms-backend-gitlab/src/implementation.ts +++ b/packages/netlify-cms-backend-gitlab/src/implementation.ts @@ -52,6 +52,7 @@ export default class GitLab implements Implementation { apiRoot: string; token: string | null; squashMerges: boolean; + cmsLabelPrefix: string; mediaFolder: string; previewContext: string; @@ -79,6 +80,7 @@ export default class GitLab implements Implementation { this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4'; this.token = ''; this.squashMerges = config.backend.squash_merges || false; + this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.mediaFolder = config.media_folder; this.previewContext = config.backend.preview_context || ''; this.lock = asyncLock(); @@ -117,6 +119,7 @@ export default class GitLab implements Implementation { repo: this.repo, apiRoot: this.apiRoot, squashMerges: this.squashMerges, + cmsLabelPrefix: this.cmsLabelPrefix, initialWorkflowStatus: this.options.initialWorkflowStatus, }); const user = await this.api.user(); diff --git a/packages/netlify-cms-backend-proxy/src/implementation.ts b/packages/netlify-cms-backend-proxy/src/implementation.ts index 1893eb18..b0150427 100644 --- a/packages/netlify-cms-backend-proxy/src/implementation.ts +++ b/packages/netlify-cms-backend-proxy/src/implementation.ts @@ -50,6 +50,7 @@ export default class ProxyBackend implements Implementation { mediaFolder: string; options: { initialWorkflowStatus?: string }; branch: string; + cmsLabelPrefix?: string; constructor(config: Config, options = {}) { if (!config.backend.proxy_url) { @@ -60,6 +61,7 @@ export default class ProxyBackend implements Implementation { this.proxyUrl = config.backend.proxy_url; this.mediaFolder = config.media_folder; this.options = options; + this.cmsLabelPrefix = config.backend.cms_label_prefix; } isGitBackend() { @@ -146,7 +148,7 @@ export default class ProxyBackend implements Implementation { try { const entry: UnpublishedEntry = await this.request({ action: 'unpublishedEntry', - params: { branch: this.branch, id, collection, slug }, + params: { branch: this.branch, id, collection, slug, cmsLabelPrefix: this.cmsLabelPrefix }, }); return entry; @@ -190,6 +192,7 @@ export default class ProxyBackend implements Implementation { entry, assets, options: { ...options, status: options.status || this.options.initialWorkflowStatus }, + cmsLabelPrefix: this.cmsLabelPrefix, }, }); } @@ -197,7 +200,13 @@ export default class ProxyBackend implements Implementation { updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) { return this.request({ action: 'updateUnpublishedEntryStatus', - params: { branch: this.branch, collection, slug, newStatus }, + params: { + branch: this.branch, + collection, + slug, + newStatus, + cmsLabelPrefix: this.cmsLabelPrefix, + }, }); } diff --git a/packages/netlify-cms-core/index.d.ts b/packages/netlify-cms-core/index.d.ts index 0c961d9f..496c5e9a 100644 --- a/packages/netlify-cms-core/index.d.ts +++ b/packages/netlify-cms-core/index.d.ts @@ -213,6 +213,7 @@ declare module 'netlify-cms-core' { site_domain?: string; base_url?: string; auth_endpoint?: string; + cms_label_prefix?: string; } export interface CmsSlug { diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js index 1ac33377..2a15b092 100644 --- a/packages/netlify-cms-core/src/constants/configSchema.js +++ b/packages/netlify-cms-core/src/constants/configSchema.js @@ -78,6 +78,7 @@ const getConfigSchema = () => ({ examples: ['repo', 'public_repo'], enum: ['repo', 'public_repo'], }, + cms_label_prefix: { type: 'string', minLength: 1 }, open_authoring: { type: 'boolean', examples: [true] }, }, required: ['name'], diff --git a/packages/netlify-cms-lib-util/src/APIUtils.ts b/packages/netlify-cms-lib-util/src/APIUtils.ts index d959ea91..d7306e90 100644 --- a/packages/netlify-cms-lib-util/src/APIUtils.ts +++ b/packages/netlify-cms-lib-util/src/APIUtils.ts @@ -2,10 +2,15 @@ export const CMS_BRANCH_PREFIX = 'cms'; export const DEFAULT_PR_BODY = 'Automatically generated by Netlify CMS'; export const MERGE_COMMIT_MESSAGE = 'Automatically generated. Merged on Netlify CMS.'; -const NETLIFY_CMS_LABEL_PREFIX = 'netlify-cms/'; -export const isCMSLabel = (label: string) => label.startsWith(NETLIFY_CMS_LABEL_PREFIX); -export const labelToStatus = (label: string) => label.substr(NETLIFY_CMS_LABEL_PREFIX.length); -export const statusToLabel = (status: string) => `${NETLIFY_CMS_LABEL_PREFIX}${status}`; +const DEFAULT_NETLIFY_CMS_LABEL_PREFIX = 'netlify-cms/'; +const getLabelPrefix = (labelPrefix: string) => labelPrefix || DEFAULT_NETLIFY_CMS_LABEL_PREFIX; + +export const isCMSLabel = (label: string, labelPrefix: string) => + label.startsWith(getLabelPrefix(labelPrefix)); +export const labelToStatus = (label: string, labelPrefix: string) => + label.substr(getLabelPrefix(labelPrefix).length); +export const statusToLabel = (status: string, labelPrefix: string) => + `${getLabelPrefix(labelPrefix)}${status}`; export const generateContentKey = (collectionName: string, slug: string) => `${collectionName}/${slug}`; diff --git a/packages/netlify-cms-lib-util/src/__tests__/apiUtils.spec.js b/packages/netlify-cms-lib-util/src/__tests__/apiUtils.spec.js index b565c916..f5627dcb 100644 --- a/packages/netlify-cms-lib-util/src/__tests__/apiUtils.spec.js +++ b/packages/netlify-cms-lib-util/src/__tests__/apiUtils.spec.js @@ -19,23 +19,56 @@ describe('APIUtils', () => { describe('isCMSLabel', () => { it('should return true for CMS label', () => { - expect(apiUtils.isCMSLabel('netlify-cms/draft')).toBe(true); + expect(apiUtils.isCMSLabel('netlify-cms/draft', 'netlify-cms/')).toBe(true); }); it('should return false for non CMS label', () => { - expect(apiUtils.isCMSLabel('other/label')).toBe(false); + expect(apiUtils.isCMSLabel('other/label', 'netlify-cms/')).toBe(false); + }); + + it('should return true if the prefix not provided for CMS label', () => { + expect(apiUtils.isCMSLabel('netlify-cms/draft', '')).toBe(true); + }); + + it('should return false if a different prefix provided for CMS label', () => { + expect(apiUtils.isCMSLabel('netlify-cms/draft', 'other/')).toBe(false); + }); + + it('should return true for CMS label when undefined prefix is passed', () => { + expect(apiUtils.isCMSLabel('netlify-cms/draft', undefined)).toBe(true); }); }); describe('labelToStatus', () => { - it('should get status from label', () => { - expect(apiUtils.labelToStatus('netlify-cms/draft')).toBe('draft'); + it('should get status from label when default prefix is passed', () => { + expect(apiUtils.labelToStatus('netlify-cms/draft', 'netlify-cms/')).toBe('draft'); + }); + + it('should get status from label when custom prefix is passed', () => { + expect(apiUtils.labelToStatus('other/draft', 'other/')).toBe('draft'); + }); + + it('should get status from label when empty prefix is passed', () => { + expect(apiUtils.labelToStatus('netlify-cms/draft', '')).toBe('draft'); + }); + + it('should get status from label when undefined prefix is passed', () => { + expect(apiUtils.labelToStatus('netlify-cms/draft', undefined)).toBe('draft'); }); }); describe('statusToLabel', () => { - it('should generate label from status', () => { - expect(apiUtils.statusToLabel('draft')).toBe('netlify-cms/draft'); + it('should generate label from status when default prefix is passed', () => { + expect(apiUtils.statusToLabel('draft', 'netlify-cms/')).toBe('netlify-cms/draft'); + }); + it('should generate label from status when custom prefix is passed', () => { + expect(apiUtils.statusToLabel('draft', 'other/')).toBe('other/draft'); + }); + it('should generate label from status when empty prefix is passed', () => { + expect(apiUtils.statusToLabel('draft', '')).toBe('netlify-cms/draft'); + }); + it('should generate label from status when undefined prefix is passed', () => { + expect(apiUtils.statusToLabel('draft', undefined)).toBe('netlify-cms/draft'); }); }); }); diff --git a/packages/netlify-cms-lib-util/src/implementation.ts b/packages/netlify-cms-lib-util/src/implementation.ts index e6db07bf..39e15cf9 100644 --- a/packages/netlify-cms-lib-util/src/implementation.ts +++ b/packages/netlify-cms-lib-util/src/implementation.ts @@ -89,6 +89,7 @@ export type Config = { proxy_url?: string; auth_type?: string; app_id?: string; + cms_label_prefix?: string; }; media_folder: string; base_url?: string; diff --git a/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts b/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts index 6a609430..e23b7cc2 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts @@ -82,6 +82,7 @@ export const defaultSchema = ({ path = requiredString } = {}) => { id: Joi.string().optional(), collection: Joi.string().optional(), slug: Joi.string().optional(), + cmsLabelPrefix: Joi.string().optional(), }) .required(), }, @@ -120,6 +121,7 @@ export const defaultSchema = ({ path = requiredString } = {}) => { is: 'persistEntry', then: defaultParams .keys({ + cmsLabelPrefix: Joi.string().optional(), entry: Joi.object({ slug: requiredString, path, @@ -145,6 +147,7 @@ export const defaultSchema = ({ path = requiredString } = {}) => { collection, slug, newStatus: requiredString, + cmsLabelPrefix: Joi.string().optional(), }) .required(), }, diff --git a/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts b/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts index 58f3658e..6e8a5215 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts @@ -222,7 +222,7 @@ export const localGitMiddleware = ({ repoPath, logger }: GitOptions) => { break; } case 'unpublishedEntry': { - let { id, collection, slug } = body.params as UnpublishedEntryParams; + let { id, collection, slug, cmsLabelPrefix } = body.params as UnpublishedEntryParams; if (id) { ({ collection, slug } = parseContentKey(id)); } @@ -232,7 +232,7 @@ export const localGitMiddleware = ({ repoPath, logger }: GitOptions) => { if (branchExists) { const diffs = await getDiffs(git, branch, cmsBranch); const label = await git.raw(['config', branchDescription(cmsBranch)]); - const status = label && labelToStatus(label.trim()); + const status = label && labelToStatus(label.trim(), cmsLabelPrefix || ''); const unpublishedEntry = { collection, slug, @@ -276,7 +276,7 @@ export const localGitMiddleware = ({ repoPath, logger }: GitOptions) => { break; } case 'persistEntry': { - const { entry, assets, options } = body.params as PersistEntryParams; + const { entry, assets, options, cmsLabelPrefix } = body.params as PersistEntryParams; if (!options.useWorkflow) { await runOnBranch(git, branch, async () => { await commitEntry(git, repoPath, entry, assets, options.commitMessage); @@ -304,7 +304,7 @@ export const localGitMiddleware = ({ repoPath, logger }: GitOptions) => { // add status for new entries if (!branchExists) { - const description = statusToLabel(options.status); + const description = statusToLabel(options.status, cmsLabelPrefix || ''); await git.addConfig(branchDescription(cmsBranch), description); } }); @@ -313,10 +313,15 @@ export const localGitMiddleware = ({ repoPath, logger }: GitOptions) => { break; } case 'updateUnpublishedEntryStatus': { - const { collection, slug, newStatus } = body.params as UpdateUnpublishedEntryStatusParams; + const { + collection, + slug, + newStatus, + cmsLabelPrefix, + } = body.params as UpdateUnpublishedEntryStatusParams; const contentKey = generateContentKey(collection, slug); const cmsBranch = branchFromContentKey(contentKey); - const description = statusToLabel(newStatus); + const description = statusToLabel(newStatus, cmsLabelPrefix || ''); await git.addConfig(branchDescription(cmsBranch), description); res.json({ message: `${branch} description was updated to ${description}` }); break; diff --git a/packages/netlify-cms-proxy-server/src/middlewares/types.ts b/packages/netlify-cms-proxy-server/src/middlewares/types.ts index 3207a5f1..36315174 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/types.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/types.ts @@ -20,6 +20,7 @@ export type UnpublishedEntryParams = { id?: string; collection?: string; slug?: string; + cmsLabelPrefix?: string; }; export type UnpublishedEntryDataFileParams = { @@ -45,6 +46,7 @@ export type UpdateUnpublishedEntryStatusParams = { collection: string; slug: string; newStatus: string; + cmsLabelPrefix?: string; }; export type PublishUnpublishedEntryParams = { @@ -57,6 +59,7 @@ export type Entry = { slug: string; path: string; raw: string; newPath?: string export type Asset = { path: string; content: string; encoding: 'base64' }; export type PersistEntryParams = { + cmsLabelPrefix?: string; entry: Entry; assets: Asset[]; options: { diff --git a/website/content/docs/backends-overview.md b/website/content/docs/backends-overview.md index 28601c29..56f1dfb5 100644 --- a/website/content/docs/backends-overview.md +++ b/website/content/docs/backends-overview.md @@ -18,6 +18,7 @@ Individual backends should provide their own configuration documentation, but th | `site_domain` | `location.hostname` (or `cms.netlify.com` when on `localhost`) | Sets the `site_id` query param sent to the API endpoint. Non-Netlify auth setups will often need to set this for local development to work properly. | | `base_url` | `https://api.netlify.com` (GitHub, Bitbucket) or `https://gitlab.com` (GitLab) | OAuth client hostname (just the base domain, no path). **Required** when using an external OAuth server or self-hosted GitLab. | | `auth_endpoint` | `auth` (GitHub, Bitbucket) or `oauth/authorize` (GitLab) | Path to append to `base_url` for authentication requests. Optional. | +| `cms_label_prefix` | `netlify-cms/` | Pull (or Merge) Requests label prefix when using editorial workflow. Optional. | ## Creating a New Backend