├── LICENSE ├── .gitignore ├── .dockerignore ├── Dockerfile ├── src ├── package.json └── merge-release-run.js ├── action.yaml ├── entrypoint.sh ├── .github └── workflows │ └── mikeals-workflow.yml └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !./LICENSE 4 | !./README.md 5 | !./entrypoint.sh 6 | !./src 7 | !./src/* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim 2 | 3 | LABEL version="1.0.0" 4 | LABEL repository="http://github.com/mikeal/merge-release" 5 | LABEL homepage="http://github.com/merge-release" 6 | LABEL maintainer="Mikeal Rogers " 7 | 8 | LABEL com.github.actions.name="Automated releases for npm packages." 9 | LABEL com.github.actions.description="Release npm package based on commit metadata." 10 | LABEL com.github.actions.icon="package" 11 | LABEL com.github.actions.color="red" 12 | 13 | RUN apt-get update && apt-get -y --no-install-recommends install git jq findutils curl ca-certificates && rm -rf /var/lib/apt/lists/* 14 | 15 | COPY . . 16 | 17 | # Install dependencies here 18 | RUN cd src && npm i 19 | 20 | ENTRYPOINT ["/entrypoint.sh"] 21 | CMD ["help"] 22 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merge-release", 3 | "version": "0.0.0-dev", 4 | "description": "", 5 | "scripts": { 6 | "test": "standard" 7 | }, 8 | "bin": { 9 | "merge-release": "./merge-release-run.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/mikeal/auto-release.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/mikeal/auto-release/issues" 20 | }, 21 | "homepage": "https://github.com/mikeal/auto-release#readme", 22 | "dependencies": { 23 | "bent": "1.5.13", 24 | "simple-git": "^1.113.0", 25 | "yargs": "^12.0.5" 26 | }, 27 | "devDependencies": { 28 | "standard": "^12.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Merge Release' 2 | description: 'Deploy to npm!' 3 | runs: 4 | using: 'docker' 5 | image: 'Dockerfile' 6 | inputs: 7 | GITHUB_TOKEN: 8 | description: 'GITHUB_TOKEN' 9 | required: true 10 | NPM_AUTH_TOKEN: 11 | description: 'NPM_AUTH_TOKEN' 12 | required: true 13 | DEPLOY_DIR: 14 | description: 'DEPLOY_DIR' 15 | required: false 16 | NPM_REGISTRY_URL: 17 | description: 'NPM_REGISTRY_URL' 18 | required: false 19 | GIT_TAG_SUFFIX: 20 | description: 'GIT_TAG_SUFFIX' 21 | required: false 22 | SRC_PACKAGE_DIR: 23 | description: 'SRC_PACKAGE_DIR' 24 | required: false 25 | GITHUB_SHA: 26 | description: 'GITHUB_SHA' 27 | required: false 28 | GITHUB_ACTOR: 29 | description: 'GITHUB_ACTOR' 30 | required: false 31 | GITHUB_REPOSITORY: 32 | description: 'GITHUB_REPOSITORY' 33 | required: false 34 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Respect NPM_CONFIG_USERCONFIG if it is provided, default to $HOME/.npmrc 6 | NPM_CONFIG_USERCONFIG="${NPM_CONFIG_USERCONFIG-"$HOME/.npmrc"}" 7 | 8 | if [ -n "$NPM_CUSTOM_NPMRC" ]; then 9 | # Use a fully-formed npmrc file if provided 10 | echo "$NPM_CUSTOM_NPMRC" > "$NPM_CONFIG_USERCONFIG" 11 | 12 | chmod 0600 "$NPM_CONFIG_USERCONFIG" 13 | elif [ -n "$NPM_AUTH_TOKEN" ]; then 14 | # Respect NPM_CONFIG_USERCONFIG if it is provided, default to $HOME/.npmrc 15 | NPM_CONFIG_USERCONFIG="${NPM_CONFIG_USERCONFIG-"$HOME/.npmrc"}" 16 | NPM_REGISTRY_URL="${NPM_REGISTRY_URL-registry.npmjs.org}" 17 | NPM_STRICT_SSL="${NPM_STRICT_SSL-true}" 18 | NPM_REGISTRY_SCHEME="https" 19 | if ! $NPM_STRICT_SSL; then 20 | NPM_REGISTRY_SCHEME="http" 21 | fi 22 | 23 | # Allow registry.npmjs.org to be overridden with an environment variable 24 | printf "//%s/:_authToken=%s\\nregistry=%s\\nstrict-ssl=%s" "$NPM_REGISTRY_URL" "$NPM_AUTH_TOKEN" "${NPM_REGISTRY_SCHEME}://$NPM_REGISTRY_URL" "${NPM_STRICT_SSL}" > "$NPM_CONFIG_USERCONFIG" 25 | 26 | chmod 0600 "$NPM_CONFIG_USERCONFIG" 27 | fi 28 | 29 | # initialize git 30 | remote_repo="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 31 | git config http.sslVerify false 32 | git config user.name "Merge Release" 33 | git config user.email "actions@users.noreply.github.com" 34 | git remote add merge-release "${remote_repo}" 35 | git remote --verbose 36 | git show-ref # useful for debugging 37 | git branch --verbose 38 | 39 | # Dependencies are installed at build time 40 | node /src/merge-release-run.js "$@" || exit 1 41 | 42 | git push "${remote_repo}" --tags 43 | -------------------------------------------------------------------------------- /.github/workflows/mikeals-workflow.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Build, Test and maybe Publish 3 | jobs: 4 | test: 5 | name: Build & Test 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node-version: [12.x, 14.x] 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - name: Cache node_modules 17 | id: cache-modules 18 | uses: actions/cache@v1 19 | with: 20 | path: node_modules 21 | key: ${{ matrix.node-version }}-${{ runner.OS }}-build-${{ hashFiles('package.json') }} 22 | - name: Build 23 | if: steps.cache-modules.outputs.cache-hit != 'true' 24 | run: npm install 25 | - name: Test 26 | run: npm_config_yes=true npx best-test@latest 27 | publish: 28 | name: Publish 29 | needs: test 30 | runs-on: ubuntu-latest 31 | if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' ) 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Cache node_modules 35 | id: cache-modules 36 | uses: actions/cache@v1 37 | with: 38 | path: node_modules 39 | key: 12.x-${{ runner.OS }}-build-${{ hashFiles('package.json') }} 40 | - name: Build 41 | if: steps.cache-modules.outputs.cache-hit != 'true' 42 | run: npm install 43 | - name: Test 44 | run: npm_config_yes=true npx best-test@latest 45 | 46 | - name: Publish 47 | uses: mikeal/merge-release@master 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## merge-release 2 | 3 | GitHub Action for automated npm publishing. 4 | 5 | This Action publishes a package to npm. It is meant to be used on every successful merge to master but 6 | you'll need to configure that workflow yourself. You can look to the 7 | [`.github/workflows/push.yml`](./.github/workflows/mikeals-workflow.yml) file in this project as an example. 8 | 9 | ### Workflow 10 | 11 | * Check for the latest version number published to npm. 12 | * Lookup all commits between the git commit that triggered the action and the latest publish. 13 | * If the package hasn't been published or the prior publish does not include a git hash, we'll 14 | only pull the commit data that triggered the action. 15 | * Based on the commit messages, increment the version from the lastest release. 16 | * If the string "BREAKING CHANGE" is found anywhere in any of the commit messages or descriptions the major 17 | version will be incremented. 18 | * If a commit message begins with the string "feat" then the minor version will be increased. This works 19 | for most common commit metadata for feature additions: `"feat: new API"` and `"feature: new API"`. 20 | * All other changes will increment the patch version. 21 | * Publish to npm using the configured token. 22 | * Push a tag for the new version to GitHub. 23 | 24 | 25 | ### Configuration 26 | 27 | You can configure some aspects of merge-release action by passing some environmental variables. 28 | 29 | * **GITHUB_TOKEN (required)** 30 | * Github token to allow tagging the version. 31 | * **NPM_AUTH_TOKEN (required)** 32 | * NPM Auth Token to publish to NPM, read [here](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets) how to setup it as a secret. 33 | * **DEPLOY_DIR** 34 | * The path where the dist `package.json` is to run npm publish. Defaults to the root dir. 35 | * **SRC_PACKAGE_DIR** 36 | * The path where the src package.json is found. Defaults to the root dir. 37 | * **NPM_REGISTRY_URL** 38 | * NPM Registry URL to use. defaults to: `https://registry.npmjs.org/` 39 | 40 | `merge-release` will use `npm publish` unless you've defined a `publish` script in your `package.json`. 41 | 42 | ```yaml 43 | - uses: mikeal/merge-release@master 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 47 | DEPLOY_DIR: my/deploy/dir 48 | SRC_PACKAGE_DIR: my/src/package 49 | ``` 50 | -------------------------------------------------------------------------------- /src/merge-release-run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const bent = require('bent') 5 | const git = require('simple-git')() 6 | const { execSync, spawnSync } = require('child_process') 7 | const { promisify } = require('util') 8 | 9 | const exec = (str, cwd) => { 10 | const [cmd, ...args] = str.split(' ') 11 | const ret = spawnSync(cmd, args, { cwd, stdio: 'inherit' }) 12 | if (ret.status) { 13 | console.error(ret) 14 | console.error(`Error: ${str} returned non-zero exit code`) 15 | process.exit(ret.status) 16 | } 17 | return ret 18 | } 19 | 20 | const getlog = promisify(git.log.bind(git)) 21 | 22 | const get = bent('json', process.env.NPM_REGISTRY_URL || 'https://registry.npmjs.org/') 23 | 24 | const event = JSON.parse(fs.readFileSync('/github/workflow/event.json').toString()) 25 | 26 | const deployDir = path.join(process.cwd(), process.env.DEPLOY_DIR || './') 27 | const srcPackageDir = path.join(process.cwd(), process.env.SRC_PACKAGE_DIR || './') 28 | 29 | console.log(' using deploy directory : ' + deployDir) 30 | console.log('using src directory (package.json) : ' + srcPackageDir) 31 | 32 | let pkg = require(path.join(deployDir, 'package.json')) 33 | 34 | const run = async () => { 35 | if (!process.env.NPM_AUTH_TOKEN) throw new Error('Merge-release requires NPM_AUTH_TOKEN') 36 | let latest 37 | try { 38 | latest = await get(pkg.name + '/latest') 39 | } catch (e) { 40 | // unpublished 41 | } 42 | 43 | let messages 44 | 45 | if (latest) { 46 | if (latest.gitHead === process.env.GITHUB_SHA) return console.log('SHA matches latest release, skipping.') 47 | if (latest.gitHead) { 48 | try { 49 | let logs = await getlog({ from: latest.gitHead, to: process.env.GITHUB_SHA }) 50 | messages = logs.all.map(r => r.message + '\n' + r.body) 51 | } catch (e) { 52 | latest = null 53 | } 54 | // g.log({from: 'f0002b6c9710f818b9385aafeb1bde994fe3b370', to: '53a92ca2d1ea3c55977f44d93e48e31e37d0bc69'}, (err, l) => console.log(l.all.map(r => r.message + '\n' + r.body))) 55 | } else { 56 | latest = null 57 | } 58 | } 59 | if (!latest) { 60 | messages = (event.commits || []).map(commit => commit.message + '\n' + commit.body) 61 | } 62 | 63 | let version = 'patch' 64 | if (messages.map(message => message.includes('BREAKING CHANGE') || message.includes('!:')).includes(true)) { 65 | version = 'major' 66 | } else if (messages.map(message => message.toLowerCase().startsWith('feat')).includes(true)) { 67 | version = 'minor' 68 | } 69 | 70 | const setVersion = version => { 71 | const json = execSync(`jq '.version="${version}"' package.json`, { cwd: srcPackageDir }) 72 | fs.writeFileSync(path.join(srcPackageDir, 'package.json'), json) 73 | 74 | if (deployDir !== './') { 75 | const deployJson = execSync(`jq '.version="${version}"' package.json`, { cwd: deployDir }) 76 | fs.writeFileSync(path.join(deployDir, 'package.json'), deployJson) 77 | } 78 | } 79 | 80 | let currentVersion = execSync(`npm view ${pkg.name} version`, { cwd: srcPackageDir }).toString() 81 | setVersion(currentVersion) 82 | console.log('current:', currentVersion, '/', 'version:', version) 83 | let newVersion = execSync(`npm version --git-tag-version=false ${version}`, { cwd: srcPackageDir }).toString() 84 | newVersion = newVersion.replace(/(\r\n|\n|\r)/gm, '') 85 | setVersion(newVersion.slice(1)) 86 | console.log('new version:', newVersion) 87 | 88 | if (pkg.scripts && pkg.scripts.publish) { 89 | exec(`npm run publish`, deployDir) 90 | } else { 91 | exec(`npm publish`, deployDir) 92 | } 93 | exec(`git checkout package.json`) // cleanup 94 | exec(`git tag ${newVersion}`) 95 | exec(`echo "::set-output name=version::${newVersion}"`) // set action event.{STEP_ID}.output.version 96 | 97 | /* 98 | const env = process.env 99 | const remote = `https://${env.GITHUB_ACTOR}:${env.GITHUB_TOKEN}@github.com/${env.GITHUB_REPOSITORY}.git` 100 | exec(`git push ${remote} --tags`) 101 | */ 102 | } 103 | run() 104 | --------------------------------------------------------------------------------