Skip to content

Commit a72d828

Browse files
committed
npc release use shared release functions
1 parent a36599e commit a72d828

File tree

3 files changed

+348
-196
lines changed

3 files changed

+348
-196
lines changed

scripts/releases.js

Lines changed: 5 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ const path = require('path')
4242
const colors = require('chalk')
4343
const Messenger = require('@codedungeon/messenger')
4444

45+
// Import shared release management utilities
4546
const { program } = require('commander')
47+
const releaseManagement = require('../src/commands/support/release-management')
48+
4649
const { getFolderFromCommandLine, runShellCommand, getPluginFileContents, fileExists, getCopyTargetPath } = require('./shared')
4750

4851
// Command line options
@@ -141,137 +144,8 @@ function getRelativeTime(publishedAt) {
141144
}
142145
}
143146

144-
/**
145-
* Check if a version is a pre-release version (alpha, beta, rc, etc.)
146-
* @param {string} version - Version string
147-
* @returns {boolean} - True if pre-release
148-
*/
149-
function isPreRelease(version) {
150-
return /-(alpha|beta|rc|pre|dev|snapshot)/i.test(version)
151-
}
152-
153-
/**
154-
* Get the base version from a pre-release version (removes pre-release identifier)
155-
* @param {string} version - Version string (e.g., "2.1.0-alpha.1")
156-
* @returns {string} - Base version (e.g., "2.1.0")
157-
*/
158-
function getBaseVersion(version) {
159-
return version.replace(/-.*$/, '')
160-
}
161-
162-
/**
163-
* Check if a pre-release version has an obsolete stable counterpart
164-
* @param {string} preReleaseVersion - The pre-release version to check
165-
* @param {Array<{name: string, tag: string, version: string, publishedAt: string}>} allReleases - All releases
166-
* @param {number} monthsThreshold - Number of months after which stable version makes pre-release obsolete
167-
* @returns {boolean} - True if the pre-release is obsolete
168-
*/
169-
function isPreReleaseObsolete(preReleaseVersion, allReleases, monthsThreshold = 3) {
170-
const baseVersion = getBaseVersion(preReleaseVersion)
171-
const now = new Date()
172-
const thresholdDate = new Date(now.getTime() - monthsThreshold * 30 * 24 * 60 * 60 * 1000)
173-
174-
// Find the stable version of the same base version
175-
const stableVersion = allReleases.find((release) => !isPreRelease(release.version) && getBaseVersion(release.version) === baseVersion)
176-
177-
if (!stableVersion) {
178-
return false // No stable version found, keep the pre-release
179-
}
180-
181-
// Check if the stable version was published more than the threshold ago
182-
const stablePublishedDate = new Date(stableVersion.publishedAt)
183-
return stablePublishedDate < thresholdDate
184-
}
185-
186-
/**
187-
* Compare two version strings for sorting (semantic versioning)
188-
* @param {string} a - First version
189-
* @param {string} b - Second version
190-
* @returns {number} - Comparison result
191-
*/
192-
function compareVersions(a, b) {
193-
// Remove pre-release identifiers for comparison
194-
const cleanA = a.replace(/-.*$/, '')
195-
const cleanB = b.replace(/-.*$/, '')
196-
197-
const partsA = cleanA.split('.').map(Number)
198-
const partsB = cleanB.split('.').map(Number)
199-
200-
const maxLength = Math.max(partsA.length, partsB.length)
201-
202-
for (let i = 0; i < maxLength; i++) {
203-
const partA = partsA[i] || 0
204-
const partB = partsB[i] || 0
205-
206-
if (partA !== partB) {
207-
return partB - partA // Descending order (newest first)
208-
}
209-
}
210-
211-
// If versions are equal, put pre-release after stable
212-
const aIsPre = isPreRelease(a)
213-
const bIsPre = isPreRelease(b)
214-
215-
if (aIsPre && !bIsPre) return 1
216-
if (!aIsPre && bIsPre) return -1
217-
218-
return 0
219-
}
220-
221-
/**
222-
* Identify releases that should be pruned based on heuristics
223-
* @param {Array<{name: string, tag: string, version: string, publishedAt: string}>} releases - Array of releases
224-
* @returns {Array<{name: string, tag: string, version: string, publishedAt: string}>} - Releases to prune
225-
*/
226-
function identifyReleasesToPrune(releases) {
227-
if (releases.length <= 3) {
228-
return [] // Keep at least 3 releases minimum
229-
}
230-
231-
const now = new Date()
232-
const sixMonthsAgo = new Date(now.getTime() - 6 * 30 * 24 * 60 * 60 * 1000)
233-
const twoYearsAgo = new Date(now.getTime() - 2 * 365 * 24 * 60 * 60 * 1000)
234-
235-
// Sort releases by version (newest first)
236-
const sortedReleases = [...releases].sort((a, b) => compareVersions(a.version, b.version))
237-
238-
const toPrune = []
239-
const stableReleases = sortedReleases.filter((r) => !isPreRelease(r.version))
240-
const preReleaseReleases = sortedReleases.filter((r) => isPreRelease(r.version))
241-
242-
// Keep the latest stable release
243-
const latestStable = stableReleases[0]
244-
245-
// Keep the latest 2-3 pre-release versions if they're recent
246-
const recentPreReleases = preReleaseReleases.filter((r) => new Date(r.publishedAt) >= sixMonthsAgo).slice(0, 3)
247-
248-
// Identify releases to prune
249-
for (const release of releases) {
250-
const isRecent = new Date(release.publishedAt) >= sixMonthsAgo
251-
const isOld = new Date(release.publishedAt) < twoYearsAgo
252-
const isLatestStable = release === latestStable
253-
const isRecentPreRelease = recentPreReleases.includes(release)
254-
const isObsoletePreRelease = isPreRelease(release.version) && isPreReleaseObsolete(release.version, releases)
255-
256-
// Prune if:
257-
// 1. It's old (2+ years) AND not the latest stable
258-
// 2. It's a pre-release that's obsolete (stable version published 3+ months ago)
259-
// 3. It's a pre-release that's not recent and we have more than 5 pre-releases
260-
// 4. It's not recent and not the latest stable and we have more than 5 total releases
261-
262-
if (isOld && !isLatestStable) {
263-
toPrune.push(release)
264-
} else if (isObsoletePreRelease) {
265-
toPrune.push(release)
266-
} else if (isPreRelease(release.version) && !isRecentPreRelease && preReleaseReleases.length > 5) {
267-
toPrune.push(release)
268-
} else if (!isRecent && !isLatestStable && releases.length > 5) {
269-
toPrune.push(release)
270-
}
271-
}
272-
273-
return toPrune
274-
}
147+
// Use shared utility functions
148+
const { isPreRelease, getBaseVersion, isPreReleaseObsolete, compareVersions, identifyReleasesToPrune, generatePruneCommands } = releaseManagement
275149

276150
/**
277151
* Identify releases to prune for duplicate version scenario (more lenient)
@@ -309,25 +183,6 @@ function identifyReleasesToPruneForDuplicate(releases) {
309183
return toPrune
310184
}
311185

312-
/**
313-
* Generate prune commands for identified releases
314-
* @param {Array<{name: string, tag: string, version: string, publishedAt: string}>} releasesToPrune - Releases to prune
315-
* @returns {string} - Commands to run for pruning
316-
*/
317-
function generatePruneCommands(releasesToPrune) {
318-
if (releasesToPrune.length === 0) {
319-
return 'No releases recommended for pruning.'
320-
}
321-
322-
const commands = releasesToPrune.map((release) => `gh release delete "${release.tag}" -y`)
323-
324-
if (releasesToPrune.length === 1) {
325-
return `Recommended prune command:\n${commands[0]}`
326-
}
327-
328-
return `Recommended prune commands:\n${commands.join('\n')}\n\nTo prune all at once:\n${commands.join(' && ')}`
329-
}
330-
331186
/**
332187
* Get all existing releases for a specific plugin
333188
* @param {string} pluginName - The plugin name to search for

src/commands/support/plugin-release.js

Lines changed: 77 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { catchError, filter } = require('rxjs/operators')
1414
const streamToObservable = require('@samverschueren/stream-to-observable')
1515
const pluginUtils = require('./plugin-utils')
1616
const github = require('./github')
17+
const releaseManagement = require('./release-management')
1718

1819
const prerequisiteTasks = require('./plugin-release/prerequisite-tasks')
1920
const gitTasks = require('./plugin-release/git-tasks')
@@ -28,27 +29,7 @@ const exec = (cmd, args) => {
2829
return merge(streamToObservable(cp.stdout.pipe(split())), streamToObservable(cp.stderr.pipe(split())), cp).pipe(filter(Boolean))
2930
}
3031

31-
const buildDeleteCommands = async (pluginId = '', currentVersion = '') => {
32-
const deleteCommands = []
33-
34-
const api = http.create({
35-
baseURL: 'https://api.github.com',
36-
headers: { Accept: 'application/vnd.github.v3+json' },
37-
})
38-
39-
const releases = await api.get('repos/NotePlan/plugins/releases')
40-
if (releases.data.length > 0) {
41-
releases.data.forEach((release) => {
42-
const tag = release.tag_name
43-
if (tag.includes(pluginId) && !tag.includes(currentVersion)) {
44-
const version = tag.replace(pluginId, '').replace('-v', '')
45-
deleteCommands.push(`gh release delete "${pluginId}-v${version}" -y`)
46-
}
47-
})
48-
}
49-
50-
return deleteCommands
51-
}
32+
// Removed buildDeleteCommands - no longer automatically deleting releases
5233

5334
module.exports = {
5435
run: async (pluginId = '', pluginName = '', pluginVersion = '', args = {}) => {
@@ -68,25 +49,31 @@ module.exports = {
6849
const tasks = new Listr(
6950
[
7051
{
71-
title: 'Prerequisite check',
52+
title: 'Validating plugin files and structure',
7253
skip: () => {
7354
if (preview) {
74-
return `[Preview] all validation`
55+
return `[Preview] validate plugin files and structure`
7556
}
7657
},
7758
task: () => prerequisiteTasks(pluginId, args),
7859
},
7960
{
80-
title: 'Github check',
61+
title: 'Checking GitHub authentication and repository status',
8162
skip: () => {
8263
if (preview) {
83-
return `[Preview] github tasks`
64+
return `[Preview] check GitHub auth and repo status`
8465
}
8566
},
8667
task: () => gitTasks(pluginId, args),
8768
},
8869
],
89-
{ showSubtaks: true },
70+
{
71+
renderer: 'default',
72+
nonTTYRenderer: 'verbose',
73+
collapse: false,
74+
clearOutput: true,
75+
showSubtasks: false,
76+
},
9077
)
9178

9279
tasks.add([
@@ -166,27 +153,23 @@ module.exports = {
166153
])
167154
}
168155

169-
if (deletePrevious) {
170-
tasks.add([
171-
{
172-
title: 'Deleting previous releases',
173-
skip: async () => {
174-
const cmds = await buildDeleteCommands(pluginId, pluginVersion)
175-
if (args.preview) {
176-
return `[Preview] ${(await cmds).join(', ')}`
177-
}
178-
},
179-
task: async () => {
180-
const cmds = await buildDeleteCommands(pluginId, pluginVersion)
181-
if (cmds.length > 0) {
182-
cmds.forEach(async (cmd) => {
183-
const result = await system.run(cmd, true)
184-
})
185-
}
186-
},
156+
// Always check release history and show pruning recommendations
157+
tasks.add([
158+
{
159+
title: 'Checking release history',
160+
skip: () => {
161+
if (args.preview) {
162+
return `[Preview] Show pruning recommendations`
163+
}
187164
},
188-
])
189-
}
165+
task: async () => {
166+
// Collect release information but don't display it yet (to avoid interfering with Listr)
167+
const releases = await releaseManagement.getExistingReleases(pluginId)
168+
// Store the release info for display after Listr completes
169+
return { releases, pluginId }
170+
},
171+
},
172+
])
190173

191174
tasks.add([
192175
{
@@ -208,7 +191,55 @@ module.exports = {
208191
},
209192
])
210193

194+
// Store release info to display after Listr completes
195+
let releaseInfo = null
196+
197+
// Override the release history task to capture the data
198+
const originalTasks = tasks._tasks
199+
const releaseHistoryTask = originalTasks.find((task) => task.title === 'Checking release history')
200+
if (releaseHistoryTask) {
201+
const originalTask = releaseHistoryTask.task
202+
releaseHistoryTask.task = async () => {
203+
const releases = await releaseManagement.getExistingReleases(pluginId)
204+
releaseInfo = { releases, pluginId }
205+
return { releases, pluginId }
206+
}
207+
}
208+
211209
const result = await tasks.run()
210+
211+
// Now display the release information after Listr has completed
212+
if (releaseInfo) {
213+
const { releases, pluginId: resultPluginId } = releaseInfo
214+
if (releases && releases.length > 0) {
215+
console.log('')
216+
print.note(`Found ${releases.length} existing release(s) for plugin "${resultPluginId}":`)
217+
218+
// Get pruning recommendations
219+
const releasesToPrune = releases.length > 3 ? releaseManagement.identifyReleasesToPrune(releases) : []
220+
const pruneTags = new Set(releasesToPrune.map((r) => r.tag))
221+
222+
releases.forEach((release, index) => {
223+
const publishedDate = new Date(release.publishedAt).toLocaleDateString()
224+
const relativeTime = releaseManagement.getRelativeTime(release.publishedAt)
225+
const shouldPrune = pruneTags.has(release.tag)
226+
const pruneIndicator = shouldPrune ? ` ${colors.red('--PRUNE?')}` : ''
227+
228+
print.log(` ${index + 1}. ${colors.cyan(release.tag)} (version ${release.version}, published ${publishedDate} -- ${relativeTime})${pruneIndicator}`)
229+
})
230+
231+
if (releasesToPrune.length > 0) {
232+
console.log('')
233+
print.log(colors.cyan(releaseManagement.generatePruneCommands(releasesToPrune)))
234+
console.log('')
235+
} else if (releases.length <= 3) {
236+
print.note(`Plugin has ${releases.length} releases (≤3), no pruning recommendations`)
237+
}
238+
} else {
239+
print.note('No existing releases found for this plugin')
240+
}
241+
}
242+
212243
console.log('')
213244
if (preview) {
214245
print.note(`${pluginId} v${pluginVersion} Released Successfully [PREVIEW]`, 'PREVIEW')

0 commit comments

Comments
 (0)