diff --git a/packages/shipjs/package.json b/packages/shipjs/package.json index da69e1ee..647a78b5 100644 --- a/packages/shipjs/package.json +++ b/packages/shipjs/package.json @@ -45,6 +45,7 @@ "esm": "3.2.25", "globby": "^10.0.1", "inquirer": "7.0.0", + "mime-types": "^2.1.25", "mkdirp": "^0.5.1", "open": "^7.0.0", "prettier": "^1.18.2", diff --git a/packages/shipjs/src/helper/getChangelog.js b/packages/shipjs/src/helper/getChangelog.js index 50e0907a..cae98719 100644 --- a/packages/shipjs/src/helper/getChangelog.js +++ b/packages/shipjs/src/helper/getChangelog.js @@ -5,8 +5,8 @@ import { extractSpecificChangelog } from './'; export default function getChangelog({ version, dir }) { const changelogPath = path.resolve(dir, 'CHANGELOG.md'); try { - const changelogFile = fs.readFileSync(changelogPath, 'utf-8').toString(); - return extractSpecificChangelog({ changelogFile, version }); + const changelog = fs.readFileSync(changelogPath, 'utf-8').toString(); + return extractSpecificChangelog({ changelog, version }); } catch (err) { if (err.code === 'ENOENT') { return null; diff --git a/packages/shipjs/src/step/prepare/__tests__/createPullRequest.spec.js b/packages/shipjs/src/step/prepare/__tests__/createPullRequest.spec.js index 8da82036..f1cad6a4 100644 --- a/packages/shipjs/src/step/prepare/__tests__/createPullRequest.spec.js +++ b/packages/shipjs/src/step/prepare/__tests__/createPullRequest.spec.js @@ -1,10 +1,8 @@ import { getRepoInfo } from 'shipjs-lib'; -import tempWrite from 'temp-write'; import Octokit from '@octokit/rest'; import createPullRequest from '../createPullRequest'; import { run } from '../../../util'; import { getDestinationBranchName } from '../../../helper'; -jest.mock('temp-write'); jest.mock('@octokit/rest'); const getDefaultParams = ({ @@ -43,7 +41,6 @@ describe('createPullRequest', () => { branch: 'master', url: 'https://github.com/my/repo', })); - tempWrite.sync.mockImplementationOnce(() => '/temp/path'); getDestinationBranchName.mockImplementation(() => 'master'); }); diff --git a/packages/shipjs/src/step/prepare/createPullRequest.js b/packages/shipjs/src/step/prepare/createPullRequest.js index 0e08d502..521303ed 100644 --- a/packages/shipjs/src/step/prepare/createPullRequest.js +++ b/packages/shipjs/src/step/prepare/createPullRequest.js @@ -63,7 +63,7 @@ export default async ({ run({ command: `git remote prune ${remote}`, dir, dryRun }); if (dryRun) { - print('Creates a pull request with the following:'); + print('Creating a pull request with the following:'); print(` - Title: ${title}`); print(` - Message: ${message}`); return {}; diff --git a/packages/shipjs/src/step/release/__tests__/createGitHubRelease.spec.js b/packages/shipjs/src/step/release/__tests__/createGitHubRelease.spec.js index 2f2a9a4c..9fa230a3 100644 --- a/packages/shipjs/src/step/release/__tests__/createGitHubRelease.spec.js +++ b/packages/shipjs/src/step/release/__tests__/createGitHubRelease.spec.js @@ -1,10 +1,16 @@ -import tempWrite from 'temp-write'; import globby from 'globby'; +import fs from 'fs'; +import mime from 'mime-types'; +import { getRepoInfo } from 'shipjs-lib'; +import Octokit from '@octokit/rest'; import createGitHubRelease from '../createGitHubRelease'; -import { run } from '../../../util'; import { hubInstalled, hubConfigured } from '../../../helper'; jest.mock('temp-write'); +jest.mock('@octokit/rest'); jest.mock('globby'); +jest.mock('shipjs-lib'); +jest.mock('fs'); +jest.mock('mime-types'); const getDefaultParams = ({ assetsToUpload, @@ -20,29 +26,48 @@ const getDefaultParams = ({ dryRun: false, }); +const createRelease = jest.fn().mockImplementation(() => ({ + data: { + upload_url: 'https://dummy/upload/url', // eslint-disable-line camelcase + }, +})); +const uploadReleaseAsset = jest.fn(); +Octokit.mockImplementation(function() { + this.repos = { createRelease, uploadReleaseAsset }; +}); + describe('createGitHubRelease', () => { beforeEach(() => { hubInstalled.mockImplementation(() => true); hubConfigured.mockImplementation(() => true); + getRepoInfo.mockImplementation(() => ({ + owner: 'my', + name: 'repo', + })); + fs.readFileSync = jest.fn(); + fs.statSync = jest.fn().mockImplementation(() => ({ size: 1024 })); + mime.lookup.mockImplementation(() => 'application/zip'); + globby.mockImplementation(path => Promise.resolve([path])); }); it('works without assets', async () => { - tempWrite.sync.mockImplementation(() => `/my chan"ge"log/temp/path`); await createGitHubRelease(getDefaultParams()); - expect(run).toHaveBeenCalledTimes(1); - expect(run.mock.calls[0]).toMatchInlineSnapshot(` + expect(createRelease).toHaveBeenCalledTimes(1); + expect(createRelease.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "command": "hub release create -F '/my chan\\"ge\\"log/temp/path' v1.2.3", - "dir": ".", - "dryRun": false, + "body": "", + "name": "v1.2.3", + "owner": "my", + "repo": "repo", + "tag_name": "v1.2.3", }, ] `); + expect(uploadReleaseAsset).toHaveBeenCalledTimes(0); }); it('works with assets (fn)', async () => { - tempWrite.sync.mockImplementation(() => `/temp/path`); await createGitHubRelease( getDefaultParams({ assetsToUpload: () => { @@ -50,61 +75,131 @@ describe('createGitHubRelease', () => { }, }) ); - expect(run).toHaveBeenCalledTimes(1); - expect(run.mock.calls[0]).toMatchInlineSnapshot(` + expect(createRelease).toHaveBeenCalledTimes(1); + expect(createRelease.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "command": "hub release create -F /temp/path -a /path1 -a /path2 v1.2.3", - "dir": ".", - "dryRun": false, + "body": "", + "name": "v1.2.3", + "owner": "my", + "repo": "repo", + "tag_name": "v1.2.3", }, ] `); + expect(uploadReleaseAsset).toHaveBeenCalledTimes(2); + expect(uploadReleaseAsset.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "file": undefined, + "headers": Object { + "content-length": 1024, + "content-type": "application/zip", + }, + "name": "path1", + "url": "https://dummy/upload/url", + }, + ], + Array [ + Object { + "file": undefined, + "headers": Object { + "content-length": 1024, + "content-type": "application/zip", + }, + "name": "path2", + "url": "https://dummy/upload/url", + }, + ], + ] + `); }); it('works with assets (list)', async () => { - tempWrite.sync.mockImplementation(() => `/temp/path`); - globby.mockImplementation(path => Promise.resolve([path])); await createGitHubRelease( getDefaultParams({ assetsToUpload: ['/path1', '/path2'], }) ); - expect(run).toHaveBeenCalledTimes(1); - expect(run.mock.calls[0]).toMatchInlineSnapshot(` + expect(createRelease).toHaveBeenCalledTimes(1); + expect(createRelease.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "command": "hub release create -F /temp/path -a /path1 -a /path2 v1.2.3", - "dir": ".", - "dryRun": false, + "body": "", + "name": "v1.2.3", + "owner": "my", + "repo": "repo", + "tag_name": "v1.2.3", }, ] `); + expect(uploadReleaseAsset).toHaveBeenCalledTimes(2); + expect(uploadReleaseAsset.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "file": undefined, + "headers": Object { + "content-length": 1024, + "content-type": "application/zip", + }, + "name": "path1", + "url": "https://dummy/upload/url", + }, + ], + Array [ + Object { + "file": undefined, + "headers": Object { + "content-length": 1024, + "content-type": "application/zip", + }, + "name": "path2", + "url": "https://dummy/upload/url", + }, + ], + ] + `); }); it('works with assets (string)', async () => { - tempWrite.sync.mockImplementation(() => `/temp/path`); - globby.mockImplementation(path => Promise.resolve([path])); await createGitHubRelease( getDefaultParams({ assetsToUpload: '/path1', }) ); - expect(run).toHaveBeenCalledTimes(1); - expect(run.mock.calls[0]).toMatchInlineSnapshot(` + expect(createRelease).toHaveBeenCalledTimes(1); + expect(createRelease.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "command": "hub release create -F /temp/path -a /path1 v1.2.3", - "dir": ".", - "dryRun": false, + "body": "", + "name": "v1.2.3", + "owner": "my", + "repo": "repo", + "tag_name": "v1.2.3", }, ] `); + expect(uploadReleaseAsset).toHaveBeenCalledTimes(1); + expect(uploadReleaseAsset.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "file": undefined, + "headers": Object { + "content-length": 1024, + "content-type": "application/zip", + }, + "name": "path1", + "url": "https://dummy/upload/url", + }, + ], + ] + `); }); it('works with extractChangelog', async () => { - tempWrite.sync.mockImplementation(() => `/temp/path`); - globby.mockImplementation(path => Promise.resolve([path])); const mockExtractChangelog = jest .fn() .mockImplementation(({ version, dir }) => `# v${version} (${dir})`); @@ -117,13 +212,15 @@ describe('createGitHubRelease', () => { version: '1.2.3', dir: '.', }); - expect(run).toHaveBeenCalledTimes(1); - expect(run.mock.calls[0]).toMatchInlineSnapshot(` + expect(createRelease).toHaveBeenCalledTimes(1); + expect(createRelease.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "command": "hub release create -F /temp/path v1.2.3", - "dir": ".", - "dryRun": false, + "body": "# v1.2.3 (.)", + "name": "v1.2.3", + "owner": "my", + "repo": "repo", + "tag_name": "v1.2.3", }, ] `); diff --git a/packages/shipjs/src/step/release/createGitHubRelease.js b/packages/shipjs/src/step/release/createGitHubRelease.js index 6448f752..72b437ad 100644 --- a/packages/shipjs/src/step/release/createGitHubRelease.js +++ b/packages/shipjs/src/step/release/createGitHubRelease.js @@ -1,10 +1,12 @@ import path from 'path'; +import fs from 'fs'; import globby from 'globby'; -import tempWrite from 'temp-write'; -import { quote } from 'shell-quote'; +import Octokit from '@octokit/rest'; +import mime from 'mime-types'; +import { getRepoInfo } from 'shipjs-lib'; import runStep from '../runStep'; -import { run } from '../../util'; import { getChangelog, hubInstalled, hubConfigured } from '../../helper'; +import { print } from '../../util'; const cannotUseHub = () => !hubInstalled() || !hubConfigured(); @@ -16,56 +18,95 @@ export default async ({ version, config, dir, dryRun }) => }, async () => { const { + remote, getTagName, releases: { assetsToUpload, extractChangelog } = {}, } = config; const tagName = getTagName({ version }); - const args = []; // extract matching changelog const getChangelogFn = extractChangelog || getChangelog; const changelog = getChangelogFn({ version, dir }); - const content = `${tagName}\n\n${changelog || ''}`; - const exportedPath = tempWrite.sync(content); - args.push('-F', quote([exportedPath])); + const content = changelog || ''; // handle assets - if (assetsToUpload) { - const assetPaths = []; + const assetPaths = await getAssetPaths({ + assetsToUpload, + dir, + version, + tagName, + }); - if (typeof assetsToUpload === 'function') { - // function - // assetsToUpload: ({dir, version, tagName}) => [...] - const files = await Promise.resolve( - assetsToUpload({ dir, version, tagName }) - ); - assetPaths.push(...files); - } else if (Array.isArray(assetsToUpload) && assetsToUpload.length > 0) { - // list - // assetsToUpload: ['package.json', 'dist/*.zip'] - for (const asset of assetsToUpload) { - const files = await globby(asset, { cwd: dir }); - if (files) { - assetPaths.push(...files); - } - } - } else if (typeof assetsToUpload === 'string') { - // string - // assetsToUpload: 'archive.zip' - const files = await globby(assetsToUpload, { cwd: dir }); - if (files) { - assetPaths.push(...files); - } + if (dryRun) { + print('Creating a release with the following:'); + print(` - content: ${content}`); + if (assetPaths.length > 0) { + print(` - assets: ${assetPaths.join(' ')}`); } + return; + } + + const { owner, name: repo } = getRepoInfo(remote, dir); + + const octokit = new Octokit({ + auth: `token ${process.env.GITHUB_TOKEN}`, + }); + + const { + data: { upload_url }, // eslint-disable-line camelcase + } = await octokit.repos.createRelease({ + owner, + repo, + tag_name: tagName, // eslint-disable-line camelcase + name: tagName, + body: content, + }); - for (const asset of assetPaths) { - args.push('-a', quote([path.resolve(dir, asset)])); + if (assetPaths.length > 0) { + for (const assetPath of assetPaths) { + const file = path.resolve(dir, assetPath); + octokit.repos.uploadReleaseAsset({ + file: fs.readFileSync(file), + headers: { + 'content-length': fs.statSync(file).size, + 'content-type': mime.lookup(file), + }, + name: path.basename(file), + url: upload_url, // eslint-disable-line camelcase + }); } } - - // create GitHub release - const hubCommand = ['hub', 'release', 'create']; - const command = [...hubCommand, ...args, tagName].join(' '); - run({ command, dir, dryRun }); } ); + +async function getAssetPaths({ assetsToUpload, dir, version, tagName }) { + if (!assetsToUpload) { + return []; + } + const assetPaths = []; + if (typeof assetsToUpload === 'function') { + // function + // assetsToUpload: ({dir, version, tagName}) => [...] + const files = await Promise.resolve( + assetsToUpload({ dir, version, tagName }) + ); + assetPaths.push(...files); + } else if (Array.isArray(assetsToUpload) && assetsToUpload.length > 0) { + // list + // assetsToUpload: ['package.json', 'dist/*.zip'] + for (const asset of assetsToUpload) { + const files = await globby(asset, { cwd: dir }); + if (files) { + assetPaths.push(...files); + } + } + } else if (typeof assetsToUpload === 'string') { + // string + // assetsToUpload: 'archive.zip' + const files = await globby(assetsToUpload, { cwd: dir }); + if (files) { + assetPaths.push(...files); + } + } + return assetPaths; +} diff --git a/yarn.lock b/yarn.lock index 56445a4e..92703f03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5998,6 +5998,11 @@ mime-db@1.40.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +mime-db@1.42.0: + version "1.42.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" + integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== + mime-types@^2.1.12, mime-types@~2.1.19: version "2.1.24" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" @@ -6005,6 +6010,13 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.40.0" +mime-types@^2.1.25: + version "2.1.25" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" + integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== + dependencies: + mime-db "1.42.0" + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"