@docupace/pipeline 1.0.85 → 1.0.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ #Overview
2
+
3
+ Implement support for converting release notes to canny approach. This requires us to reprocess release notes from a specified version and up and generate via AI using existing functions the yaml files and then using those same yaml files upload to canny. This process is split into to parts convert-prepare, convert-complete
4
+
5
+ # Implementation
6
+
7
+ ## convert-prepare
8
+
9
+ Add to src/changeset/cli.js convert-prepare with input parameter version id which will include major and minor version
10
+ In a new file convert.js add function convertPrepare which will perform the following logic:
11
+ - Process changelog MD to extract list of Jira tickets from the version specified up to the latest version. Please see example changelog /Users/fielm/Documents/Docupace/GraphQL/docupace-graphql-server/CHANGELOG.md
12
+ - For each Jira ticket call const { enhancedNotes, yamlFilePath, mainTicketId, summary } = await getReleaseNotes(jiraTicketId);
13
+ - Replace in CHANGELOG.md file the Jira ticket id [DC-XXXX] with [[${taskTicketId}]](${JIRA_BASE_URL}/browse/${taskTicketId}) [[details]](${relYamlFilePath})`;
14
+ - Note: do not use the summary returned by getReleaseNotes we will leave the existing summary
15
+
16
+ ## convert-complete
17
+ - Create new function similar to updateDetailsLinkInText in src/changeset/changelog.js to support multiple links
18
+ - Call this function passing passing fetchNewLink and contents of the changelog.md file
19
+ - Update the changelog.md file with the results which should have now links to canny
20
+
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@docupace/pipeline",
3
- "version": "1.0.85",
3
+ "version": "1.0.87",
4
+ "description": "",
4
5
  "main": "index.js",
5
6
  "type": "module",
7
+ "exports": {
8
+ "./changelogCanny": "./src/changeset/changelog-module.cjs"
9
+ },
6
10
  "bin": {
7
11
  "dp-changeset-cli": "src/changeset/cli.js",
8
12
  "dp-next-cli": "src/next/cli.js",
@@ -17,9 +21,9 @@
17
21
  "license": "ISC",
18
22
  "dependencies": {
19
23
  "@changesets/cli": "^2.27.10",
20
- "@docupace/core-client": "^1.0.1",
21
- "@docupace/pipeline": "^1.0.12",
24
+ "@docupace/pipeline": "^1.0.86",
22
25
  "@jest/globals": "^30.1.2",
26
+ "@docupace/core-client": "^0.1.59",
23
27
  "auth0-deploy-cli": "^8.8.0",
24
28
  "axios": "^1.7.9",
25
29
  "commander": "^12.1.0",
@@ -29,6 +33,7 @@
29
33
  "inquirer": "^12.6.1",
30
34
  "js-yaml": "^4.1.0",
31
35
  "lodash": "^4.17.21",
36
+ "node-fetch": "^3.3.2",
32
37
  "openai": "^4.76.3",
33
38
  "semver": "^7.7.2",
34
39
  "zod": "^3.24.1",
@@ -52,6 +57,7 @@
52
57
  "test-deploy-from-site": "dp-next-cli deploy-from-site --from-env dev --to-env dev --from-site financialdev --to-site template --deploy-core true",
53
58
  "test-run-pipline": "dp-next-cli run-pipeline -r docupace-next-e2e -b master -p run-tests-qa",
54
59
  "test-validate": "dp-changset-cli validate",
55
- "test-create": "dp-changset-cli create-changeset"
60
+ "test-create": "dp-changset-cli create-changeset",
61
+ "lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx}\""
56
62
  }
57
63
  }
@@ -812,7 +812,7 @@ export async function configureFromSite(
812
812
 
813
813
  const envConfig = environmentConfig[toEnv];
814
814
 
815
- let CORE_URL, SITE_HOST, CORE_API_CONTEXT, CONNECT_URL, DOCUPACE_HOST;
815
+ let CORE_URL, SITE_HOST, CONNECT_URL, DOCUPACE_HOST;
816
816
  const newUrl = envConfig.newUrl(toSite);
817
817
  const ccUrl = envConfig.ccUrl(toSite);
818
818
 
@@ -822,14 +822,12 @@ export async function configureFromSite(
822
822
  const newUrl = envConfig.newUrl(toSite);
823
823
  CORE_URL = `${newUrl}/core`;
824
824
  // SITE_HOST = newUrl;
825
- CORE_API_CONTEXT = 'core';
826
825
  CONNECT_URL = `${newUrl}/connect`;
827
826
  DOCUPACE_HOST = envConfig.docupaceHost();
828
827
  } else {
829
828
  const oldUrl = envConfig.oldUrl(toSite);
830
829
  CORE_URL = oldUrl;
831
830
  // SITE_HOST = oldUrl;
832
- CORE_API_CONTEXT = toSite;
833
831
  CONNECT_URL = `${oldUrl}_ui`;
834
832
  }
835
833
 
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import fetch from 'node-fetch';
3
+
4
+ // Configuration
5
+ const CANNY_API_KEY = process.env.CANNY_API_KEY;
6
+
7
+ /**
8
+ * Upload release notes to Canny
9
+ * @param {Object} releaseNote - The release note data
10
+ * @returns {Promise<Object>} - The full Canny entry object
11
+ */
12
+ export async function uploadToCanny(releaseNote, labelIDs) {
13
+ if (!CANNY_API_KEY) {
14
+ console.warn('Canny API key not provided. Skipping upload.');
15
+ return null;
16
+ }
17
+
18
+ try {
19
+ // Prepare the content for Canny based on the release notes
20
+ const title = releaseNote.title;
21
+ let content = '';
22
+
23
+ // Include user-facing description if available
24
+ if (
25
+ releaseNote.userFacingDescription &&
26
+ releaseNote.userFacingDescription.__line.trim()
27
+ ) {
28
+ content += `${releaseNote.summary}\n\n`;
29
+ content += `${releaseNote.userFacingDescription.__line}\n\n`;
30
+ } else {
31
+ content += `${releaseNote.summary}\n\n`;
32
+ }
33
+
34
+ // Include technical details if available
35
+ if (
36
+ releaseNote.technicalDescription &&
37
+ releaseNote.technicalDescription.__line.trim()
38
+ ) {
39
+ content += `**Technical Details**\n\n${releaseNote.technicalDescription.__line}\n\n`;
40
+ }
41
+
42
+ // Add JIRA reference
43
+ content += `**Reference**: JIRA ticket [${releaseNote.ticketId}]`;
44
+
45
+ // Map our changeType to Canny's type parameter
46
+ let entryType;
47
+ if (releaseNote.changeType === 'New') {
48
+ entryType = 'new';
49
+ } else if (releaseNote.changeType === 'Improved') {
50
+ entryType = 'improved';
51
+ } else if (releaseNote.changeType === 'Fixed') {
52
+ entryType = 'fixed';
53
+ }
54
+
55
+ const body = JSON.stringify({
56
+ apiKey: CANNY_API_KEY,
57
+ title: title,
58
+ details: content,
59
+ type: entryType,
60
+ published: false, // Keep as draft initially
61
+ labelIDs,
62
+ });
63
+
64
+ // Create a new changelog entry in Canny
65
+ const createResponse = await fetch(
66
+ 'https://canny.io/api/v1/entries/create',
67
+ {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ },
72
+ body,
73
+ },
74
+ );
75
+
76
+ if (!createResponse.ok) {
77
+ console.error(`Error from Canny API: ${createResponse.statusText}`);
78
+ return null;
79
+ }
80
+
81
+ const createData = await createResponse.json();
82
+ const entryId = createData.id;
83
+
84
+ // Fetch the complete entry details using the entries/list endpoint
85
+ const listResponse = await fetch('https://canny.io/api/v1/entries/list', {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ body: JSON.stringify({
91
+ apiKey: CANNY_API_KEY,
92
+ }),
93
+ });
94
+
95
+ if (!listResponse.ok) {
96
+ console.error(
97
+ `Error fetching entries list from Canny API: ${listResponse.statusText}`,
98
+ );
99
+ return createData; // Return just the create response as fallback
100
+ }
101
+
102
+ const listData = await listResponse.json();
103
+
104
+ // Find the entry we just created in the list
105
+ const fullEntry = listData.entries.find((entry) => entry.id === entryId);
106
+
107
+ if (!fullEntry) {
108
+ console.warn(
109
+ `Created entry with ID ${entryId} not found in entries list. Returning partial data.`,
110
+ );
111
+ return createData;
112
+ }
113
+
114
+ return fullEntry;
115
+ } catch (error) {
116
+ console.error(`Error uploading to Canny: ${error.message}`);
117
+ return null;
118
+ }
119
+ }
@@ -0,0 +1,37 @@
1
+ async function getReleaseLine(changeset, type, changelogOpts) {
2
+ return (async () => {
3
+ try {
4
+ const { default: changelog } = await import('./changelog.js');
5
+ return await changelog.getReleaseLine(changeset, type, changelogOpts);
6
+ } catch (e) {
7
+ console.error(e);
8
+ process.exit(1);
9
+ }
10
+ // Now you can use `changelog` here
11
+ })();
12
+ }
13
+
14
+ async function getDependencyReleaseLine(
15
+ changesets,
16
+ dependenciesUpdated,
17
+ changelogOpts,
18
+ ) {
19
+ return (async () => {
20
+ try {
21
+ const { default: changelog } = await import('./changelog.js');
22
+ return await changelog.getDependenciesReleaseLine(
23
+ changesets,
24
+ dependenciesUpdated,
25
+ changelogOpts,
26
+ ); // Now you can use `changelog` here
27
+ } catch (e) {
28
+ console.error(e);
29
+ process.exit(1);
30
+ }
31
+ })();
32
+ }
33
+
34
+ module.exports = {
35
+ getReleaseLine,
36
+ getDependencyReleaseLine,
37
+ };
@@ -0,0 +1,210 @@
1
+ import defaultChangeLog from '@changesets/cli/changelog';
2
+ import { loadChangesetOutput } from './version.js';
3
+ import path from 'path';
4
+ import yaml from 'js-yaml';
5
+ import { getJiraReleaseNotes, updateJiraReleaseNotes } from '../next/jira.js';
6
+ import { uploadToCanny } from './canny.js';
7
+ import fs from 'fs';
8
+ import semver from 'semver';
9
+ import { componentConfig, labelIDMap } from './config.js';
10
+ import { findJiraTicketId } from './util.js';
11
+
12
+ async function updateDetailsLinkInText(text, processDetailsLink, params) {
13
+ const detailsLinkRegex = /\[\[details\]\]\((.*?)\)/;
14
+
15
+ const match = text.match(detailsLinkRegex);
16
+ if (!match) {
17
+ return text; // No details link found
18
+ }
19
+
20
+ const originalLink = match[1];
21
+ const newLink = await processDetailsLink(originalLink, params);
22
+
23
+ const updatedText = text.replace(detailsLinkRegex, `[[details]](${newLink})`);
24
+ return updatedText;
25
+ }
26
+
27
+ /**
28
+ * Update multiple details links in text
29
+ * Similar to updateDetailsLinkInText but supports multiple [[details]](...) links
30
+ * @param {string} text - Text containing [[details]](link) patterns
31
+ * @param {Function} processDetailsLink - Function to process each link
32
+ * @param {Object} params - Parameters to pass to processDetailsLink
33
+ * @returns {Promise<string>} - Updated text with processed links
34
+ */
35
+
36
+ function getChanges(params) {
37
+ const changes = params.changeset.releases.map((release) => {
38
+ const matchingRelease = params.output.releases.find(
39
+ (r) => r.name === release.name,
40
+ );
41
+
42
+ const version = matchingRelease ? matchingRelease.newVersion : null;
43
+ const parsed = version ? semver.parse(version) : null;
44
+
45
+ return {
46
+ name: release.name,
47
+ type: release.type,
48
+ version,
49
+ majorMinor: parsed ? `${parsed.major}.${parsed.minor}` : null,
50
+ };
51
+ });
52
+ return changes;
53
+ }
54
+
55
+ function getLabels(changes) {
56
+ const labels = [];
57
+ for (const change of changes) {
58
+ const componentName = componentConfig[change.name];
59
+ if (componentName) {
60
+ const labelName = componentName;
61
+ if (!labels.includes(labelName)) {
62
+ labels.push(labelName);
63
+ }
64
+ }
65
+ }
66
+ return labels;
67
+ }
68
+
69
+ function getJiraVersions(changes) {
70
+ const versions = [];
71
+ for (const change of changes) {
72
+ const componentName = componentConfig[change.name];
73
+ if (componentName) {
74
+ const labelName = `${componentName} - ${change.majorMinor}`;
75
+ if (!versions.includes(labelName)) {
76
+ versions.push(labelName);
77
+ }
78
+ }
79
+ }
80
+ return versions;
81
+ }
82
+
83
+ async function getLabelIDs(labels) {
84
+ let cannyConfig;
85
+ try {
86
+ const cannyConfigPath = path.join(
87
+ process.cwd(),
88
+ '.changeset',
89
+ 'canny.json',
90
+ );
91
+ cannyConfig = JSON.parse(fs.readFileSync(cannyConfigPath, 'utf-8'));
92
+ } catch (e) {
93
+ console.error('Canny read canny.json file', e);
94
+ }
95
+
96
+ const mergedLabelMap = { ...labelIDMap, ...cannyConfig?.labels };
97
+ return labels.map((label) => mergedLabelMap[label]);
98
+ }
99
+
100
+ function extractSummaryDescription(text) {
101
+ const lines = text.split(/\r?\n/);
102
+
103
+ // Start from the 5th line (index 4)
104
+ const summaryLines = [];
105
+
106
+ for (let i = 0; i < lines.length; i++) {
107
+ if (lines[i].startsWith('[[')) {
108
+ break;
109
+ }
110
+ summaryLines.push(lines[i]);
111
+ }
112
+
113
+ return summaryLines.join('\n').trim();
114
+ }
115
+
116
+ async function fetchNewLink(oldLink, params) {
117
+ if (oldLink.includes('.yaml')) {
118
+ const normalizedOldLink = oldLink.replaceAll('\\', '/');
119
+ const yamlPath = path.resolve(
120
+ process.cwd(),
121
+ '.changeset',
122
+ normalizedOldLink,
123
+ );
124
+ const releaseNote = yaml.load(fs.readFileSync(yamlPath, 'utf-8'));
125
+ const ticketId = findJiraTicketId(params.changeset.id);
126
+ const mainTicketId = releaseNote.ticketId;
127
+ const summary = extractSummaryDescription(params.changeset.summary);
128
+ if (!ticketId) {
129
+ console.log('No ticket found in: ', JSON.stringify(params.changeset));
130
+ return;
131
+ }
132
+
133
+ const uploadToCannyInternal = async () => {
134
+ const changes = getChanges(params);
135
+ const jiraVersions = getJiraVersions(changes);
136
+ const labels = getLabels(changes);
137
+ const labelIDs = await getLabelIDs([...labels, ...jiraVersions]);
138
+ const data = await uploadToCanny(releaseNote, labelIDs);
139
+ return data;
140
+ };
141
+
142
+ const uploadToJiraInternal = async (ticketId, url) => {
143
+ const changes = getChanges(params);
144
+ const jiraVersions = getJiraVersions(changes);
145
+ await updateJiraReleaseNotes(ticketId, summary, url, jiraVersions);
146
+ };
147
+
148
+ const jiraReleaseInfo = await getJiraReleaseNotes(mainTicketId);
149
+ if (mainTicketId != ticketId) {
150
+ const subtaskJiraReleaseInfo = await getJiraReleaseNotes(ticketId);
151
+ if (subtaskJiraReleaseInfo?.url) {
152
+ return subtaskJiraReleaseInfo?.url;
153
+ } else {
154
+ if (jiraReleaseInfo?.url) {
155
+ await uploadToJiraInternal(ticketId, jiraReleaseInfo?.url);
156
+ return jiraReleaseInfo?.url;
157
+ } else {
158
+ const data = await uploadToCannyInternal();
159
+ await uploadToJiraInternal(ticketId, data.url);
160
+ await uploadToJiraInternal(mainTicketId, data.url);
161
+ return data.url;
162
+ }
163
+ }
164
+ } else {
165
+ if (jiraReleaseInfo?.url) {
166
+ return jiraReleaseInfo.url;
167
+ } else {
168
+ const data = await uploadToCannyInternal();
169
+ await uploadToJiraInternal(mainTicketId, data.url);
170
+ return data.url;
171
+ }
172
+ }
173
+ } else {
174
+ return oldLink;
175
+ }
176
+ }
177
+
178
+ async function uploadChangeset(changeset) {
179
+ const output = await loadChangesetOutput();
180
+ const summary = await updateDetailsLinkInText(
181
+ changeset.summary,
182
+ fetchNewLink,
183
+ { changeset, output },
184
+ );
185
+ return { ...changeset, summary };
186
+ }
187
+
188
+ async function getReleaseLine(changeset, type, changelogOpts) {
189
+ const newChangeset = await uploadChangeset(changeset);
190
+ return await defaultChangeLog.getReleaseLine(
191
+ newChangeset,
192
+ type,
193
+ changelogOpts,
194
+ );
195
+ }
196
+
197
+ async function getDependenciesReleaseLine(
198
+ changesets,
199
+ dependenciesUpdated,
200
+ changelogOpts,
201
+ ) {
202
+ return await defaultChangeLog.getDependencyReleaseLine(
203
+ changesets,
204
+ dependenciesUpdated,
205
+ changelogOpts,
206
+ );
207
+ }
208
+
209
+ export default { getReleaseLine, getDependenciesReleaseLine };
210
+ export { fetchNewLink };
@@ -10,10 +10,8 @@ import {
10
10
  versionCommand,
11
11
  } from './version.js';
12
12
  import { createChangeset } from './create.js';
13
- import {
14
- updateWikiWithVersion,
15
- upsertReleaseNotesFromFile,
16
- } from './changelog-wiki.js';
13
+ import { updateWikiWithVersion } from './changelog-wiki.js';
14
+ import { convertPrepare, convertComplete } from './convert.js';
17
15
 
18
16
  dotenv.config({ path: ['.env.pipeline.local', 'config/.env.pipeline.local'] });
19
17
 
@@ -93,14 +91,12 @@ async function commitTagMono(releases) {
93
91
  }
94
92
 
95
93
  async function commitTag() {
96
- const newVersion = (
97
- await $`node -p "require('./package.json').version"`
98
- ).stdout.trim();
99
-
100
94
  if (process.env.IS_DEV) {
101
- console.log(`Simulate commit of version ${newVersion}`);
102
95
  return;
103
96
  }
97
+ const newVersion = (
98
+ await $`node -p "require('./package.json').version"`
99
+ ).stdout.trim();
104
100
 
105
101
  const defaultBranch = process.env.BITBUCKET_BRANCH;
106
102
 
@@ -119,7 +115,16 @@ program
119
115
  if (!process.env.BITBUCKET_PR_ID) {
120
116
  return;
121
117
  }
122
- await validateChangesets();
118
+ console.log('🔍 Validating changesets...');
119
+ try {
120
+ await validateChangesets();
121
+ console.log('✅ Changeset validation passed.');
122
+ } catch {
123
+ console.error(
124
+ '❌ No changeset found. Please add a changeset to this PR. Please see more information at https://docupacellc.atlassian.net/wiki/spaces/PM/pages/1350893571/Creating+changesets+for+Docupace+Next',
125
+ );
126
+ process.exit(1);
127
+ }
123
128
  });
124
129
  // Command: version
125
130
  program
@@ -197,40 +202,20 @@ program
197
202
  await createChangeset();
198
203
  });
199
204
 
205
+ // Command: convert-prepare
200
206
  program
201
- .command('upload-wiki')
202
- .description('Upload release notes from markdown file to Confluence')
203
- .requiredOption(
204
- '-v, --version <version>',
205
- 'Version title (e.g., "Version 1.0")',
206
- )
207
- .requiredOption(
208
- '-c, --component <component>',
209
- 'Component title (e.g., "Component A")',
210
- )
211
- .requiredOption('-f, --file <file>', 'Path to markdown file')
212
- .requiredOption('-g, --group <group>', 'Group of release notes')
207
+ .command('convert-prepare <version> [changelogPath]')
208
+ .description('Convert release notes to Canny format - Part 1 (Prepare)')
209
+ .action(async (version, changelogPath) => {
210
+ await convertPrepare(version, changelogPath);
211
+ });
213
212
 
214
- .action(async (options) => {
215
- try {
216
- console.log('📤 Uploading release notes to Confluence...');
217
- const { versionPage, componentPage } = await upsertReleaseNotesFromFile({
218
- version: options.version,
219
- component: options.component,
220
- markdownFile: options.file,
221
- group: options.group,
222
- });
223
-
224
- console.log('✅ Successfully uploaded to Confluence');
225
- console.log('Version page:', versionPage._links?.webui || versionPage.id);
226
- console.log(
227
- 'Component page:',
228
- componentPage._links?.webui || componentPage.id,
229
- );
230
- } catch (error) {
231
- console.error('❌ Error uploading to Confluence:', error.message);
232
- process.exit(1);
233
- }
213
+ // Command: convert-complete
214
+ program
215
+ .command('convert-complete [changelogPath]')
216
+ .description('Convert release notes to Canny format - Part 2 (Complete)')
217
+ .action(async (changelogPath) => {
218
+ await convertComplete(changelogPath);
234
219
  });
235
220
 
236
221
  // Parse arguments
@@ -0,0 +1,20 @@
1
+ export const componentConfig = {
2
+ '@docupace/enterprise-ui-app': 'Enterprise',
3
+ '@docupace/cc-ui-app': 'C & C Application',
4
+ '@docupace/core': 'Enterprise Core',
5
+ '@docupace/graphql-server': 'Enterprise',
6
+ '@docupace/enterprise-template': 'Enterprise',
7
+ };
8
+
9
+ export const labelIDMap = {
10
+ Enterprise: '6836d53ad8622b2abab1fbd9',
11
+ 'Enterprise Core': '6836d532716bbe32f53661fb',
12
+ 'Enterprise Core - 25.0': '68400cea3c20ce44785cca3e',
13
+ 'Enterprise Core - 25.1': '68401502fa2d977cfcd6e102',
14
+ 'Enterprise Core - 25.2': '6840151b0915e310b1766005',
15
+ 'Enterprise Core - 25.3': '684018c7f11b9e5d5d24936f',
16
+ 'Enterprise Core - 25.4': '6914853cd07d92c594ab8c61',
17
+ 'Enterprise Core - 25.5': '691485400eb37f0c0fdf75c5',
18
+ 'Enterprise Core - 25.6': '69148545c0a13575489d609f',
19
+ 'Enterprise Core - 25.7': '691485fcdd3accda32e16288',
20
+ };