From 5be06d6ef9c5482bd8b67425d53136f1ebc000e7 Mon Sep 17 00:00:00 2001 From: sualko Date: Sun, 31 Jan 2021 18:40:35 +0100 Subject: [PATCH] chore: refactor build and publish script - use typescript - create changelog during build - include changelog in archive --- package.json | 1 + .../{build-release.js => build-release.ts} | 134 ++++++++--- scripts/imports/changelog.ts | 140 ++++++++++++ ...{publish-release.js => publish-release.ts} | 212 ++++-------------- yarn.lock | 17 +- 5 files changed, 305 insertions(+), 199 deletions(-) rename scripts/{build-release.js => build-release.ts} (65%) create mode 100644 scripts/imports/changelog.ts rename scripts/{publish-release.js => publish-release.ts} (57%) diff --git a/package.json b/package.json index 28fa3a8..b29a847 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@nextcloud/eslint-plugin": "^2.0.0", "@nextcloud/files": "^1.0.1", "@types/bootstrap": "^4.3.2", + "@types/inquirer": "^7.3.1", "@types/jquery": "^3.3.35", "@types/node": "^14.6.2", "@types/react": "^16.9.34", diff --git a/scripts/build-release.js b/scripts/build-release.ts similarity index 65% rename from scripts/build-release.js rename to scripts/build-release.ts index 03e5317..34576ec 100644 --- a/scripts/build-release.js +++ b/scripts/build-release.ts @@ -1,27 +1,45 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -require('colors').setTheme({ +import colors from 'colors'; +import fs from 'fs'; +import path from 'path'; +import inquirer from 'inquirer'; +import simpleGit from 'simple-git/promise'; +import libxml from 'libxmljs'; +import https from 'https'; +import archiver from 'archiver'; +import execa from 'execa'; +import {exec} from 'child_process'; +import { generateChangelog, hasChangeLogEntry } from './imports/changelog'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageInfo = require('../package.json'); + +declare global { + interface String { + error: string + verbose: string + warn: string + green: string + } +} + +colors.setTheme({ verbose: 'cyan', warn: 'yellow', error: 'red', }); -const fs = require('fs'); -const path = require('path'); -const libxml = require('libxmljs'); -const https = require('https'); -const archiver = require('archiver'); -const execa = require('execa'); -const git = require('simple-git/promise')(); -const package = require('../package.json'); - +const git = simpleGit(); const infoXmlPath = './appinfo/info.xml'; const isStableRelease = process.argv.indexOf('--stable') > 1; +const isDryRun = process.argv.indexOf('--dry-run') > 1; async function getVersion() { - return package.version + (!isStableRelease ? '-git.' + (await git.raw(['rev-parse', '--short', 'HEAD'])).trim() : ''); + return packageInfo.version + (!isStableRelease ? '-git.' + (await git.raw(['rev-parse', '--short', 'HEAD'])).trim() : ''); } -run(); +run().catch(err => { + console.log(`✘ ${err.toString()}`.error); +}); async function run() { const appId = await prepareInfoXml(); @@ -55,6 +73,9 @@ async function createRelease(appId) { const version = await getVersion(); console.log(`I'm now building ${appId} in version ${version}.`.verbose); + await isRepoClean(); + console.log('✔ repo is clean'.green); + await execa('yarn', ['composer:install:dev']); console.log('✔ composer dev dependencies installed'.green); @@ -67,6 +88,8 @@ async function createRelease(appId) { await execa('yarn', ['build']); console.log('✔ scripts built'.green); + await updateChangelog(); + const filePath = await createArchive(appId, appId + '-v' + version); await createNextcloudSignature(appId, filePath); await createGPGSignature(filePath); @@ -76,6 +99,64 @@ async function createRelease(appId) { console.log('✔ composer dev dependencies installed'.green); } +async function isRepoClean() { + const status = await git.status(); + + if (status.staged.length > 0) { + throw 'Repo not clean. Found staged files.'; + } + + if (status.modified.length > 2 || !status.modified.includes('package.json') || !status.modified.includes('appinfo/info.xml')) { + throw 'Repo not clean. Found modified files.'; + } + + if (status.not_added.length > 0) { + throw 'Repo not clean. Found not added files.'; + } +} + +async function updateChangelog() { + if (!isStableRelease) { + console.log('Skip changelog for non-stable releases.'.warn); + return; + } + + const changeLog = await generateChangelog(packageInfo.version); + console.log('✔ change log generated'.green); + + console.log(changeLog); + + console.log('Press any key to continue...'); + await keypress(); + + await hasChangeLogEntry(packageInfo.version); + console.log('✔ there is a change log entry for this version'.green); + + await commitChangeLog(); + console.log('✔ change log commited'.green); +} + +async function keypress() { + return inquirer.prompt([{ + type: 'input', + name: 'keypress', + message: 'Press any key to continue... (where is the any key?)', + }]); +} + +async function commitChangeLog(): Promise { + const status = await git.status(); + + if (status.staged.length > 0) { + throw 'Repo not clean. Found staged files.'; + } + + if (!isDryRun) { + await git.add('CHANGELOG.md'); + await git.commit('docs: update change log', ['-n']); + } +} + function createArchive(appId, fileBaseName) { const fileName = `${fileBaseName}.tar.gz`; @@ -135,11 +216,7 @@ function createArchive(appId, fileBaseName) { } function createNextcloudSignature(appId, filePath) { - const { - exec, - } = require('child_process'); - - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const sigPath = filePath + '.ncsig'; exec(`openssl dgst -sha512 -sign ~/.nextcloud/certificates/${appId}.key ${filePath} | openssl base64 > ${sigPath}`, (error, stdout, stderr) => { if (error) { @@ -162,11 +239,7 @@ function createNextcloudSignature(appId, filePath) { } function createGPGSignature(filePath) { - const { - exec, - } = require('child_process'); - - return new Promise((resolve, reject) => { + return new Promise((resolve) => { exec(`gpg --yes --detach-sign "${filePath}"`, (error, stdout, stderr) => { if (error) { throw error; @@ -188,11 +261,7 @@ function createGPGSignature(filePath) { } function createGPGArmorSignature(filePath) { - const { - exec, - } = require('child_process'); - - return new Promise((resolve, reject) => { + return new Promise((resolve) => { exec(`gpg --yes --detach-sign --armor "${filePath}"`, (error, stdout, stderr) => { if (error) { throw error; @@ -220,9 +289,10 @@ async function validateXml(xmlDoc) { throw 'Found no schema location'; } - - let schemaString; + let schemaString: string; try { + console.log('Downloading schema file...'.verbose); + schemaString = await wget(schemaLocation); } catch (err) { console.log('Could not download schema. Skip validation.'.warn); @@ -245,8 +315,8 @@ async function validateXml(xmlDoc) { } } -function wget(url) { - return new Promise((resolve, reject) => { +function wget(url: string) { + return new Promise((resolve, reject) => { https.get(url, (resp) => { let data = ''; diff --git a/scripts/imports/changelog.ts b/scripts/imports/changelog.ts new file mode 100644 index 0000000..d73ad4c --- /dev/null +++ b/scripts/imports/changelog.ts @@ -0,0 +1,140 @@ +import fs from 'fs'; +import path from 'path'; +import inquirer from 'inquirer'; +import simpleGit from 'simple-git/promise'; + +const git = simpleGit(); + +export async function generateChangelog(version: string): Promise { + const latestTag = (await git.tags()).latest; + const title = `v${version}` === latestTag ? '[Unreleased]' : `${version} (${new Date().toISOString().split('T')[0]})`; + + const logs = await git.log({ + from: latestTag, + to: 'HEAD', + }); + + const sections = [{ + type: 'feat', + label: 'Added', + }, { + type: 'fix', + label: 'Fixed', + }]; + + const entries = {}; + + logs.all.forEach(log => { + const match = log.message.match(/^([a-z]+)(?:\((\w+)\))?: (.+)/); + + if (!match) { + return; + } + + const [, type, scope, description] = match; + const entry = { type, scope, description, issues: [] }; + + if(log.body) { + const matches = log.body.match(/(?:fix|fixes|closes?|refs?) #(\d+)/g) || []; + + for (const match of matches) { + const [, number] = match.match(/(\d+)$/); + + entry.issues.push(number); + } + } + + if (!entries[type]) { + entries[type] = []; + } + + entries[type].push(entry); + }); + + let changeLog = `## ${title}\n`; + + function stringifyEntry(entry) { + const issues = entry.issues.map(issue => { + return `[#${issue}](https://github.com/sualko/cloud_bbb/issues/${issue})`; + }).join(''); + + return `- ${issues}${issues.length > 0 ? ' ' : ''}${entry.description}\n`; + } + + sections.forEach(section => { + if (!entries[section.type]) { + return; + } + + changeLog += `### ${section.label}\n`; + + entries[section.type].forEach(entry => { + changeLog += stringifyEntry(entry); + }); + + delete entries[section.type]; + + changeLog += '\n'; + }); + + const miscKeys = Object.keys(entries); + + if (miscKeys && miscKeys.length > 0) { + changeLog += '### Misc\n'; + + miscKeys.forEach(type => { + entries[type].forEach(entry => { + changeLog += stringifyEntry(entry); + }); + }); + } + + return changeLog; +} + +export async function editChangeLog(changeLog: string):Promise { + const answers = await inquirer.prompt([{ + type: 'editor', + name: 'changeLog', + message: 'You have now the possibility to edit the change log', + default: changeLog, + }]); + + return answers.changeLog; +} + +export async function hasChangeLogEntry(version: string): Promise { + const entry = await getChangelogEntry(version); + + return entry.split('\n').filter(line => !!line.trim()).length > 1; +} + +export function getChangelogEntry(version: string): Promise { + return new Promise(resolve => { + fs.readFile(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf8', function (err, data) { + if (err) throw err; + + const releaseHeader = /^\d+\.\d+\.\d+$/.test(version) ? `## ${version}` : '## [Unreleased]'; + const lines = data.split('\n'); + const entry: string[] = []; + + let inEntry = false; + + for(const line of lines) { + if (line.startsWith(releaseHeader)) { + inEntry = true; + } else if (line.startsWith('## ') && entry.length > 0) { + inEntry = false; + + break; + } + + if (inEntry) { + entry.push(line); + } + } + + resolve(entry.join('\n')); + }); + }); +} diff --git a/scripts/publish-release.js b/scripts/publish-release.ts similarity index 57% rename from scripts/publish-release.js rename to scripts/publish-release.ts index 4ac9185..e9a89dd 100644 --- a/scripts/publish-release.js +++ b/scripts/publish-release.ts @@ -1,29 +1,43 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -require('colors').setTheme({ +import colors from 'colors'; +import fs from 'fs'; +import path from 'path'; +import inquirer from 'inquirer'; +import simpleGit from 'simple-git/promise'; +import https from 'https'; +import execa from 'execa'; +import {Octokit} from '@octokit/rest'; +import dotenv from 'dotenv'; +import { getChangelogEntry, hasChangeLogEntry } from './imports/changelog'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageInfo = require('../package.json'); + +declare global { + interface String { + error: string + verbose: string + warn: string + green: string + } +} + +colors.setTheme({ verbose: 'cyan', warn: 'yellow', error: 'red', }); -const fs = require('fs'); -const path = require('path'); -const https = require('https'); -const { Octokit } = require('@octokit/rest'); -const execa = require('execa'); -const inquirer = require('inquirer'); -const git = require('simple-git/promise')(); -const package = require('../package.json'); - -require('dotenv').config(); +dotenv.config(); +const git = simpleGit(); const isDryRun = process.argv.indexOf('--dry-run') > 1; -const commitMessage = `release: ${package.version} :tada:`; -const tagName = `v${package.version}`; +const commitMessage = `release: ${packageInfo.version} :tada:`; +const tagName = `v${packageInfo.version}`; const files = [ - path.join(__dirname, '..', 'archives', `bbb-v${package.version}.tar.gz`), - path.join(__dirname, '..', 'archives', `bbb-v${package.version}.tar.gz.asc`), - path.join(__dirname, '..', 'archives', `bbb-v${package.version}.tar.gz.ncsig`), - path.join(__dirname, '..', 'archives', `bbb-v${package.version}.tar.gz.sig`), + path.join(__dirname, '..', 'archives', `bbb-v${packageInfo.version}.tar.gz`), + path.join(__dirname, '..', 'archives', `bbb-v${packageInfo.version}.tar.gz.asc`), + path.join(__dirname, '..', 'archives', `bbb-v${packageInfo.version}.tar.gz.ncsig`), + path.join(__dirname, '..', 'archives', `bbb-v${packageInfo.version}.tar.gz.sig`), ]; function pull() { @@ -37,136 +51,11 @@ async function notAlreadyTagged() { } async function lastCommitNotBuild() { - return (await git.log(['-1'])).latest.message !== commitMessage; + return (await git.log(['-1'])).latest?.message !== commitMessage; } async function isMasterBranch() { - return (await git.branch()) === 'master'; -} - -async function generateChangelog() { - const latestTag = (await git.tags()).latest; - const title = `v${package.version}` === latestTag ? '[Unreleased]' : `${package.version} (${new Date().toISOString().split('T')[0]})`; - - const logs = await git.log({ - from: latestTag, - to: 'HEAD', - }); - - const sections = [{ - type: 'feat', - label: 'Added', - }, { - type: 'fix', - label: 'Fixed', - }]; - - const entries = {}; - - logs.all.forEach(log => { - const match = log.message.match(/^([a-z]+)(?:\((\w+)\))?: (.+)/); - - if (!match) { - return; - } - - const [, type, scope, description] = match; - const entry = { type, scope, description, issues: [] }; - - if(log.body) { - const matches = log.body.match(/(?:fix|fixes|closes?|refs?) #(\d+)/g) || []; - - for (const match of matches) { - const [, number] = match.match(/(\d+)$/); - - entry.issues.push(number); - } - } - - if (!entries[type]) { - entries[type] = []; - } - - entries[type].push(entry); - }); - - let changeLog = `## ${title}\n`; - - function stringifyEntry(entry) { - const issues = entry.issues.map(issue => { - return `[#${issue}](https://github.com/sualko/cloud_bbb/issues/${issue})`; - }).join(''); - - return `- ${issues}${issues.length > 0 ? ' ' : ''}${entry.description}\n`; - } - - sections.forEach(section => { - if (!entries[section.type]) { - return; - } - - changeLog += `### ${section.label}\n`; - - entries[section.type].forEach(entry => { - changeLog += stringifyEntry(entry); - }); - - delete entries[section.type]; - - changeLog += '\n'; - }); - - const miscKeys = Object.keys(entries); - - if (miscKeys && miscKeys.length > 0) { - changeLog += '### Misc\n'; - - miscKeys.forEach(type => { - entries[type].forEach(entry => { - changeLog += stringifyEntry(entry); - }); - }); - } - - return changeLog; -} - -async function editChangeLog(changeLog) { - const answers = await inquirer.prompt([{ - type: 'editor', - name: 'changeLog', - message: 'You have now the possibility to edit the change log', - default: changeLog, - }]); - - return answers.changeLog; -} - -function hasChangeLogEntry() { - return new Promise(resolve => { - fs.readFile(path.join(__dirname, '..', 'CHANGELOG.md'), function (err, data) { - if (err) throw err; - - if (!data.includes(`## ${package.version}`) && /^\d+\.\d+\.\d+$/.test(package.version)) { - throw `Found no change log entry for ${package.version}`; - } - - resolve(); - }); - }); -} - -async function commitChangeLog() { - const status = await git.status(); - - if (status.staged.length > 0) { - throw 'Repo not clean. Found staged files.'; - } - - if (!isDryRun) { - await git.add('CHANGELOG.md'); - await git.commit('docs: update change log', ['-n']); - } + return (await git.branch()).current === 'master'; } async function hasArchiveAndSignatures() { @@ -180,7 +69,7 @@ async function stageAllFiles() { const gitProcess = execa('git', ['add', '-u']); - gitProcess.stdout.pipe(process.stdout); + gitProcess.stdout?.pipe(process.stdout); return gitProcess; } @@ -188,7 +77,7 @@ async function stageAllFiles() { function showStagedDiff() { const gitProcess = execa('git', ['diff', '--staged']); - gitProcess.stdout.pipe(process.stdout); + gitProcess.stdout?.pipe(process.stdout); return gitProcess; } @@ -240,8 +129,8 @@ async function createGithubRelease(changeLog) { userAgent: 'custom releaser for sualko/cloud_bbb', }); - const origin = (await git.remote(['get-url', 'origin'])).trim(); - const matches = origin.match(/^git@github\.com:(.+)\/(.+)\.git$/); + const origin = (await git.remote(['get-url', 'origin'])) || ''; + const matches = origin.trim().match(/^git@github\.com:(.+)\/(.+)\.git$/); if (!matches) { throw 'Origin is not configured or no ssh url'; @@ -252,11 +141,10 @@ async function createGithubRelease(changeLog) { const releaseOptions = { owner, repo, - // eslint-disable-next-line @typescript-eslint/camelcase tag_name: tagName, name: `BigBlueButton Integration ${tagName}`, body: changeLog.replace(/^## [^\n]+\n/, ''), - prerelease: !/^\d+\.\d+\.\d+$/.test(package.version), + prerelease: !/^\d+\.\d+\.\d+$/.test(packageInfo.version), }; if (isDryRun) { @@ -289,9 +177,8 @@ async function createGithubRelease(changeLog) { const uploadOptions = { owner, repo, - // eslint-disable-next-line @typescript-eslint/camelcase release_id: releaseResponse.data.id, - data: fs.createReadStream(file), + data: fs.createReadStream(file), headers: { 'content-type': getMimeType(filename), 'content-length': fs.statSync(file)['size'], @@ -316,7 +203,7 @@ async function uploadToNextcloudStore(archiveUrl) { const hostname = 'apps.nextcloud.com'; const apiEndpoint = '/api/v1/apps/releases'; - const signatureFile = files.find(file => file.endsWith('.ncsig')); + const signatureFile = files.find(file => file.endsWith('.ncsig')); const data = JSON.stringify({ download: archiveUrl, signature: fs.readFileSync(signatureFile, 'utf-8'), @@ -339,7 +226,7 @@ async function uploadToNextcloudStore(archiveUrl) { return; } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const req = https.request(options, res => { if (res.statusCode === 200) { console.log('App release was updated successfully'.verbose); @@ -383,23 +270,16 @@ async function run() { await isMasterBranch(); console.log('✔ this is the master branch'.green); - let changeLog = await generateChangelog(); - console.log('✔ change log generated'.green); + await hasChangeLogEntry(packageInfo.version); + console.log('✔ there is a change log entry for this version'.green); - changeLog = await editChangeLog(changeLog); - console.log('✔ change log updated'.green); + const changeLog = await getChangelogEntry(packageInfo.version); console.log(changeLog); console.log('Press any key to continue...'); await keypress(); - await hasChangeLogEntry(); - console.log('✔ there is a change log entry for this version'.green); - - await commitChangeLog(); - console.log('✔ change log commited'.green); - await hasArchiveAndSignatures(); console.log('✔ found archive and signatures'.green); @@ -430,7 +310,7 @@ async function run() { await uploadToNextcloudStore(archiveAssetUrl); console.log('✔ released in Nextcloud app store'.green); -}; +} run().catch(err => { console.log(`✘ ${err.toString()}`.error); diff --git a/yarn.lock b/yarn.lock index b6a8e13..4dd7471 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1348,6 +1348,14 @@ "@types/jquery" "*" popper.js "^1.14.1" +"@types/inquirer@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" + integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/jquery@*", "@types/jquery@^3.3.35": version "3.5.5" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903" @@ -1430,6 +1438,13 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/uglify-js@*": version "3.11.1" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.11.1.tgz#97ff30e61a0aa6876c270b5f538737e2d6ab8ceb" @@ -7100,7 +7115,7 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@^6.6.0: +rxjs@^6.4.0, rxjs@^6.6.0: version "6.6.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==