diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..c8c08f9bb4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Release +on: + push: + tags: + - '*' +jobs: + automate: + runs-on: ubuntu-latest + env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + FORCE_COLOR: 1 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: true + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: yarn + - run: grunt automated-release + #- run: npm publish + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/Gruntfile.js b/Gruntfile.js index 5248ca24db..3624b020ab 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,6 +17,8 @@ const KnexMigrator = require('knex-migrator'); const knexMigrator = new KnexMigrator({ knexMigratorFilePath: config.get('paths:appRoot') }); +const releaseUtils = require('@tryghost/release-utils'); +const semver = require('semver'); const path = require('path'); const escapeChar = process.platform.match(/^win/) ? '^' : '\\'; @@ -32,6 +34,10 @@ const logBuildingClient = function (grunt) { } }; +const ORGNAME = 'TryGhost'; +const githubRepoPathGhost = `https://github.com/${ORGNAME}/Ghost`; +let gistUrl, githubUploadURL; + // ## Grunt configuration const configureGrunt = function (grunt) { // #### Load all grunt tasks @@ -579,6 +585,143 @@ const configureGrunt = function (grunt) { ['shell:master', 'subgrunt:init'] ); + grunt.registerTask('changelog', 'Generate changelog since version (:)', function (version) { + let ghostPackageInfo = grunt.file.readJSON(path.join(process.cwd(), 'package.json')); + const done = this.async(); + + releaseUtils + .releases + .get({ + userAgent: 'ghost-release', + uri: `https://api.github.com/repos/${ORGNAME}/Ghost/releases` + }) + .then((response) => { + const sameMajorReleaseTags = [], otherReleaseTags = []; + + response.forEach((release) => { + let lastVersion = release.tag_name || release.name; + + // only compare to versions smaller than the new one + if (semver.gt(ghostPackageInfo.version, lastVersion)) { + // check if the majors are the same + if (semver.major(lastVersion) === semver.major(ghostPackageInfo.version)) { + sameMajorReleaseTags.push(lastVersion); + } else { + otherReleaseTags.push(lastVersion); + } + } + }); + + return (sameMajorReleaseTags.length !== 0) ? sameMajorReleaseTags[0] : otherReleaseTags[0]; + }) + .then((previousVersion) => { + let versionToUse = version || previousVersion; + + const changelog = new releaseUtils.Changelog({ + changelogPath: path.join(process.cwd(), '.', 'changelog.md'), + folder: process.cwd() + }); + + changelog + .write({ + githubRepoPath: githubRepoPathGhost, + lastVersion: versionToUse + }) + .write({ + githubRepoPath: `https://github.com/${ORGNAME}/Ghost-Admin`, + lastVersion: versionToUse, + append: true, + folder: path.join(process.cwd(), 'core', 'client') + }) + .sort() + .clean(); + + grunt.log.writeln('changelog.md generated'.cyan); + done(); + }) + .catch(done); + }); + + grunt.registerTask('gist', 'Generate a gist with the changelog', function () { + let ghostPackageInfo = grunt.file.readJSON(path.join(process.cwd(), 'package.json')); + const done = this.async(); + + releaseUtils + .gist + .create({ + userAgent: 'ghost-release', + gistName: 'changelog-' + ghostPackageInfo.version + '.md', + gistDescription: 'Changelog ' + ghostPackageInfo.version, + changelogPath: path.join(process.cwd(), 'changelog.md'), + github: { + token: process.env.RELEASE_TOKEN + }, + isPublic: true + }).then((response) => { + gistUrl = response.gistUrl; + grunt.log.writeln(`Gist generated: ${gistUrl}`.cyan); + done(); + }).catch(done); + }); + + grunt.registerTask('draftRelease', 'Publish a draft release on GitHub', function () { + let ghostPackageInfo = grunt.file.readJSON(path.join(process.cwd(), 'package.json')); + let changelogPaths = [{changelogPath: path.join(process.cwd(), 'changelog.md')}]; + const done = this.async(); + + /*if (hasCasperChanged) { + changelogPaths.push({ + changelogPath: path.join(process.cwd(), releaseDir, casperDir, 'changelog.md'), + content: [`\n\nCasper (the default theme) has been upgraded to ${casperNewVersion}:`] + }); + }*/ + + releaseUtils + .releases + .create({ + draft: true, + preRelease: false, + tagName: ghostPackageInfo.version, + releaseName: ghostPackageInfo.version, + userAgent: 'ghost-release', + uri: `https://api.github.com/repos/${ORGNAME}/Ghost/releases`, + github: { + token: process.env.RELEASE_TOKEN + }, + changelogPath: changelogPaths, + gistUrl: gistUrl + }) + .then((response) => { + githubUploadURL = response.uploadUrl; + grunt.log.writeln(`Release draft generated: ${response.releaseUrl}`.cyan); + done(); + }).catch(done); + }); + + grunt.registerTask('uploadGitHub', 'Upload Ghost .zip to GitHub', function () { + const done = this.async(); + const ghostPackageInfo = grunt.file.readJSON(path.join(process.cwd(), 'package.json')); + const zipName = `Ghost-${ghostPackageInfo.version}.zip`; + + releaseUtils + .releases + .uploadZip({ + github: { + token: process.env.RELEASE_TOKEN + }, + zipPath: path.join(process.cwd(), '.dist', 'release', zipName), + uri: `${githubUploadURL.substring(0, githubUploadURL.indexOf('{'))}?name=${zipName}`, + userAgent: 'ghost-release' + }) + .then(done) + .catch(done); + }); + + grunt.registerTask('automated-release', function () { + grunt.option('skip-tests', true); + grunt.task.run(['release', 'changelog', 'gist', 'draftRelease', 'uploadGitHub']); + }); + // ### Release // Run `grunt release` to create a Ghost release zip file. // Uses the files specified by `.npmignore` to know what should and should not be included. diff --git a/package.json b/package.json index e0e867113e..9d23b7af70 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ }, "devDependencies": { "@lodder/grunt-postcss": "^2.0.1", + "@tryghost/release-utils": "0.5.0", "cssnano": "^4.1.10", "eslint": "6.8.0", "eslint-plugin-ghost": "1.0.1", diff --git a/yarn.lock b/yarn.lock index 9e6c79c008..88166b8f31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,6 +436,18 @@ chalk "^2.4.1" sywac "^1.2.1" +"@tryghost/release-utils@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@tryghost/release-utils/-/release-utils-0.5.0.tgz#39ec5d0095bc26f7aba7fa461047a393a196f3f8" + integrity sha512-HJoXlaTDFfem6ivnHMRhdpdXat9DLl4gQ22/OwseZ0ItrTDb4BNb0wes/hCafktfACPeqB5IOT0tmy6rAystgA== + dependencies: + bluebird "^3.5.3" + emoji-regex "^8.0.0" + execa "^1.0.0" + lodash "^4.17.11" + request "^2.88.0" + request-promise "^4.2.4" + "@tryghost/social-urls@0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@tryghost/social-urls/-/social-urls-0.1.7.tgz#a62b008c16e2e1f6d7519a9f36f3b2966be2bad8" @@ -2022,7 +2034,7 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^6.0.5: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2941,6 +2953,19 @@ execa@^0.8.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -3515,7 +3540,7 @@ get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^4.1.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==