├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── deploy.yml ├── package.json ├── LICENSE ├── action.yml ├── lib ├── npm.js └── main.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | .* 14 | !.gitignore 15 | !.github 16 | _* 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: coreybutler 4 | patreon: #coreybutler 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@author.io/action-publish", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Automatically publish JavaScript modules to a registry (like the npm registry). The code base is scanned for JavaScript modules, each of which is published to the specified registry (default is npm). Respects .npmrc files if present in the module's root directory.", 6 | "main": "lib/main.js", 7 | "type": "module", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/author/action-publish.git" 11 | }, 12 | "keywords": [ 13 | "actions", 14 | "javascript", 15 | "node", 16 | "publish" 17 | ], 18 | "author": "Author.io", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@actions/core": "^1.10.0", 22 | "@actions/github": "^2.1.0", 23 | "globby": "^11.0.0", 24 | "semver": "^7.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Corey Butler and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: MultiPublish 2 | description: Automatically publish JS modules (supports multiple modules, respects .npmrc) 3 | author: Author.io 4 | branding: 5 | icon: box 6 | color: red 7 | inputs: 8 | scan: 9 | description: Optional. Specify which directories (relative to the root) should be scanned for modules. Comma separated, supports glob syntax. 10 | required: false 11 | default: "./" 12 | ignore: 13 | description: Optional. Prevent specific directories. Comma-separated, supports glob syntax. 14 | required: false 15 | force: 16 | description: Optional. Force a private module to be published (see limitations in README) 17 | required: false 18 | dist_tag: 19 | description: Optional. The dist-tag to apply to non-prerelease modules. examples - latest, stable, current 20 | required: false 21 | prerelease_dist_tag: 22 | description: Optional. The name of a dist-tag to use if the module is a prerelease. examples - beta, next, dev, canary 23 | required: false 24 | outputs: 25 | modules: 26 | description: "A comma-delimited list of modules that were updated. Ex: '@myorg/mypkg@1.0.0, @myorg/myotherpkg@1.4.1'" 27 | runs: 28 | using: "node16" 29 | main: "lib/main.js" 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout updated source code 13 | - uses: actions/checkout@v2 14 | 15 | # If the version has changed, create a new git tag for it. 16 | - name: Tag 17 | id: autotagger 18 | uses: butlerlogic/action-autotag@stable 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | # The remaining steps all depend on whether or not 23 | # a new tag was created. There is no need to release/publish 24 | # updates until the code base is in a releaseable state. 25 | 26 | # If the new version/tag is a pre-release (i.e. 1.0.0-beta.1), create 27 | # an environment variable indicating it is a prerelease. 28 | - name: Pre-release 29 | if: steps.autotagger.outputs.tagname != '' 30 | run: | 31 | if [[ "${{ steps.autotagger.output.version }}" == *"-"* ]]; then echo "::set-env IS_PRERELEASE=true";else echo "::set-env IS_PRERELEASE=''";fi 32 | 33 | # Create a github release 34 | # This will create a snapshot of the module, 35 | # available in the "Releases" section on Github. 36 | - name: Release 37 | id: create_release 38 | if: steps.autotagger.outputs.tagname != '' 39 | uses: actions/create-release@v1.0.0 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | tag_name: ${{ steps.autotagger.outputs.tagname }} 44 | release_name: ${{ steps.autotagger.outputs.tagname }} 45 | body: ${{ steps.autotagger.outputs.tagmessage }} 46 | draft: false 47 | prerelease: env.IS_PRERELEASE != '' 48 | 49 | - name: Rollback Release 50 | if: failure() && steps.create_release.outputs.id != '' 51 | uses: author/action-rollback@stable 52 | with: 53 | tag: ${{ steps.autotagger.outputs.tagname }} 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /lib/npm.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { join, resolve } from 'path' 3 | import { execSync } from 'child_process' 4 | 5 | export default class npm { 6 | constructor(token, registry = 'registry.npmjs.org') { 7 | if (!token || token.trim().length === 0) { 8 | throw new Error('Missing REGISTRY_TOKEN.') 9 | } 10 | 11 | this.token = token 12 | this.registry = registry || 'registry.npmjs.org' 13 | } 14 | 15 | latest (module) { 16 | const result = JSON.parse(execSync(`npm info ${module} --json`).toString()) 17 | return result['dist-tags'].latest 18 | } 19 | 20 | publish (dir, forcePublic = true) { 21 | this.run(dir, `npm publish${forcePublic ? ' --access=public' : ''}`) 22 | } 23 | 24 | tag (dir, name, version, alias) { 25 | this.run(dir, `npm dist-tag add ${name}@${version} ${alias}`) 26 | } 27 | 28 | run (dir, cmd) { 29 | this.config(dir) 30 | const response = execSync(cmd, { cwd: dir }).toString() 31 | 32 | if (response.indexOf('npm ERR!') >= 0) { 33 | console.log('DEBUG: ' + response.split('\n').join('\nDEBUG: ')) 34 | 35 | throw new Error(response) 36 | } 37 | } 38 | 39 | config (dir) { 40 | dir = resolve(dir) 41 | 42 | let npmrc = this.npmrc(dir) 43 | let npmrcFile = join(dir, '.npmrc') 44 | 45 | if (fs.existsSync(npmrcFile)) { 46 | fs.unlinkSync(npmrcFile) 47 | } 48 | console.log('Writing to', npmrcFile) 49 | console.log('npmrc content: ', npmrc.replace(this.token, 'TOKEN')) 50 | console.log('------') 51 | fs.writeFileSync(npmrcFile, npmrc) 52 | } 53 | 54 | npmrc (dir) { 55 | const file = join(dir, '.npmrc') 56 | 57 | if (!fs.existsSync(file)) { 58 | return `//${this.registry}/:_authToken=${this.token}` 59 | } 60 | 61 | let content = fs.readFileSync(file).toString() 62 | let hasRegistry = false 63 | 64 | content = content.split(/\n+/).map(line => { 65 | const match = /(\/{2}[\S]+\/:?)/.exec(line) 66 | 67 | if (match !== null) { 68 | hasRegistry = true 69 | line = `${match[1]}:`.replace(/:+$/, ':') + `_authToken=${this.token}` 70 | } 71 | 72 | return line 73 | }).join('\n').trim() 74 | 75 | if (!hasRegistry) { 76 | content += `\n//${this.registry}/:_authToken=${this.token}` 77 | } 78 | 79 | return content.trim() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | // const github = require('@actions/github') 3 | import globby from 'globby' 4 | import fs from 'fs' 5 | import path from 'path' 6 | // const execSync = require('child_process').execSync 7 | import NpmRegistry from './npm.js' 8 | 9 | const SEMVER_PATTERN = /(?
(?[0-9]+)\.(?[0-9]+)\.(?[0-9]+))(-(?.+))?/i 10 | 11 | async function run() { 12 | try { 13 | core.debug( 14 | ` Available environment variables:\n -> ${Object.keys(process.env) 15 | .map(i => i + ' :: ' + process.env[i]) 16 | .join('\n -> ')}` 17 | ) 18 | 19 | if (!process.env.hasOwnProperty('REGISTRY_TOKEN')) { 20 | core.setFailed('Missing REGISTRY_TOKEN.') 21 | return 22 | } 23 | 24 | const token = process.env.REGISTRY_TOKEN 25 | const dist_tag = { 26 | prerelease: (core.getInput('prerelease_dist_tag', { required: true }) || '').split(',').map(i => i.trim()), 27 | latest: (core.getInput('dist_tag', { required: false }) || '').split(',').map(i => i.trim()) 28 | } 29 | const force = (core.getInput('force', { required: false }) || 'false').trim().toLowerCase() === 'true' ? true : false 30 | const scan = (core.getInput('scan', { required: false }) || './').split(',').map(dir => path.join(process.env.GITHUB_WORKSPACE, dir.trim(), '/**/package.json')) 31 | const ignore = new Set() 32 | const ignoreList = core.getInput('ignore', { required: false }).trim().split(',') 33 | 34 | if (ignoreList.length > 0) { 35 | (await globby( 36 | ignoreList 37 | .filter(dir => dir.trim().length > 0) 38 | .map(dir => path.join(process.env.GITHUB_WORKSPACE, dir.trim(), '/**/package.json')) 39 | )) 40 | .forEach(result => ignore.add(result)) 41 | } 42 | 43 | console.log(`Directories to scan:\n\t- ${scan.join('\n - ')}`) 44 | 45 | const npm = new NpmRegistry(token, core.getInput('registry', { required: false })) 46 | 47 | // Scan for modules 48 | // The test has to run first because globby uses an incorrect Regex pattern that requires it be run twice (global flag) 49 | const test = await globby(scan.concat(['!**/node_modules'])) 50 | const paths = new Set(await globby(scan.concat(['!**/node_modules']))) 51 | 52 | // Remove ignored directories 53 | if (ignore.size > 0) { 54 | core.debug('Ignored:', ignore) 55 | ignore.forEach(file => paths.has(file) && paths.delete(file)) 56 | } 57 | 58 | if (paths.size === 0) { 59 | core.debug('Paths:\n' + Array.from(paths).join('\n')) 60 | core.setFailed('No modules detected in the code base (could not find package.json).') 61 | return 62 | } 63 | 64 | const publications = new Set() 65 | const priorPackages = {} 66 | 67 | for (let file of paths.values()) { 68 | file = path.resolve(file) 69 | console.log(`Attempting to publish from "${file}"`) 70 | 71 | const content = JSON.parse(fs.readFileSync(file)) 72 | 73 | // Do not publish private packages unless forced 74 | if (force === true || !content.private) { 75 | try { 76 | const latest = await npm.latest(content.name) 77 | 78 | npm.publish(path.dirname(file), force === true ? true : !content.private) 79 | 80 | publications.add(`${content.name}@${content.version}`) 81 | 82 | const match = SEMVER_PATTERN.exec(content.version) 83 | 84 | if (match?.groups) { 85 | const { prerelease } = match.groups 86 | 87 | if (prerelease && prerelease.trim().length > 0) { 88 | for (const ptag of dist_tag.prerelease) { 89 | npm.tag(path.dirname(file), content.name, content.version, ptag) 90 | // Assure the prior latest tag remains intact 91 | npm.tag(path.dirname(file), content.name, latest, 'latest') 92 | core.info(`tagged ${content.name}@${content.version} as "${ptag}"`) 93 | } 94 | } else if (dist_tag.latest) { 95 | for (const dtag of dist_tag.latest) { 96 | npm.tag(path.dirname(file), content.name, content.version, dtag) 97 | core.info(`tagged ${content.name}@${content.version} as "${dtag}"`) 98 | } 99 | } 100 | } 101 | } catch (e) { 102 | console.log(e) 103 | 104 | if (e.message.indexOf('previously published version') > 0) { 105 | priorPackages[file] = `${content.name}@${content.version}` 106 | } 107 | 108 | core.warning(e.message) 109 | } 110 | } else { 111 | core.notice(`Skipped publishing ${path.dirname(file)} to ${npm.registry} (private module).`) 112 | } 113 | } 114 | 115 | if (publications.size === 0) { 116 | for (const [file, v] of Object.entries(priorPackages)) { 117 | core.notice(`${v} already exists`) 118 | } 119 | 120 | core.setFailed('Failed to publish or update any modules.') 121 | return 122 | } 123 | 124 | core.setOutput('modules', Array.from(publications).join(', ')) 125 | } catch (e) { 126 | console.log(e.message) 127 | console.log(e.stack) 128 | core.setFailed(e.message) 129 | } 130 | } 131 | 132 | run() 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # author/action-publish 2 | 3 | This action will scan a code base and publish any public JavaScript modules it detects. It **supports publishing one _or more_ modules**, custom npm registries, npm dist-tags, and custom `.npmrc` files. 4 | 5 | Modules are detected by the presence of a `package.json` file. Private packages will not be published (unless forced) and `.npmrc` files will be respected if they exist within the module's root directory. 6 | 7 | This action was designed for workflows which require variations of a module to be published under different names. For example, a Node version and a browser version of the same library. 8 | 9 | This action serves as the last step of a multi-phase deployment process: 10 | 11 | 1. [Build & Test](https://github.com/author/template-cross-runtime) for multiple runtimes. 12 | 1. [Autotag](https://github.com/marketplace/actions/autotagger) new versions by updating `package.json`. 13 | 1. Publish multiple modules (i.e., this action). 14 | 15 | ## Usage 16 | 17 | ### Setup 18 | **First, you'll need an npm security token.** 19 | 20 | To get this, [login to your npm account](https://www.npmjs.com/login) and find/create a token: 21 | 22 | 23 | 24 | Additional instructions are available [here](https://docs.npmjs.com/creating-and-viewing-authentication-tokens). 25 | 26 | Once you've created your npm token, you'll need to make your Github repo aware of it. To do this, [create an encrypted secret](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets) (called `REGISTRY_TOKEN`). 27 | 28 | ### Workflow 29 | 30 | The following is an example `.github/publish.yml` that will execute when a `release` occurs. There are other ways to run this action too (described later), but best practice is to publish whenever code is released. 31 | 32 | ```yaml 33 | name: Publish 34 | 35 | on: 36 | release: 37 | types: 38 | - published 39 | # - created 40 | 41 | jobs: 42 | build: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: author/action-publish@stable 47 | with: 48 | # Optionally specify the directories to scan 49 | # for modules. If this is not specified, the 50 | # root directory is scanned. 51 | scan: "./dist/browser, ./dist/node" 52 | # Optionally force publishing as a public 53 | # module. We don't recommend setting this, 54 | # unless you have a very specific use case. 55 | force: true 56 | env: 57 | # Typically an npm token 58 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 59 | 60 | ``` 61 | 62 | To make this work, the workflow must have the checkout action _before_ the publish action. 63 | 64 | This **order** is important! 65 | 66 | ```yaml 67 | - uses: actions/checkout@v2 68 | - uses: author/action-publish@stable 69 | ``` 70 | 71 | > If the repository is not checked out first, the publisher cannot find the `package.json` file(s). 72 | 73 | ### Optional Configurations (Details) 74 | 75 | There are several options to customize how the publisher handles operations. 76 | 77 | 1. `scan` 78 | 79 | The scan attribute tells the publish action to "look for modules in these directories". If this is not specified, the publish action will scan the project root. Multiple directories can be supplied using a comma-separated **string**. Do not use a YAML array (Github actions does not recognized them). 80 | 81 | This supports glob syntax. Any `node_modules` directories are ignored automatically. 82 | 83 | A module is detected when a `package.json` file is recognized. Private packages will not be published. 84 | 85 | ```yaml 86 | - uses: author/action-publish@stable 87 | with: 88 | scan: ".browser_dist, .node_dist" 89 | env: 90 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 91 | ``` 92 | 93 | 1. `ignore` 94 | 95 | The ignore attribute tells the publish action to skip any modules matching the ignored patterns. 96 | 97 | ```yaml 98 | - uses: author/action-publish@stable 99 | with: 100 | scan: "./" 101 | ignore: "**/build, **/test" 102 | env: 103 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 104 | ``` 105 | 106 | 1. `force` 107 | 108 | It's somewhat possible to force publishing, even if the `private: true` attribute is specified in a `package.json` file. Whether this option will be respected or not is dependent on the registry where the module is being published. Generally, it is not a good idea to use this option. It exists to help with edge cases, such as self-hosted private registries. 109 | 110 | ```yaml 111 | - uses: author/action-publish@stable 112 | with: 113 | force: true 114 | env: 115 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 116 | ``` 117 | 118 | 1. `dist_tag` 119 | 120 | Set a [npm dist-tag](https://docs.npmjs.com/cli/v6/commands/npm-dist-tag) by configuring this attribute. This tag will be applied to all _non-prerelease_ versions (i.e. `x.x.x`, not `x.x.x-prerelease`). 121 | 122 | This allows users to install your module via tag name. For example `npm install mymodule@current`. 123 | 124 | To apply multiple tags, separate with commas. 125 | 126 | ```yaml 127 | - uses: author/action-publish@stable 128 | with: 129 | dist_tag: latest, current 130 | env: 131 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 132 | ``` 133 | 134 | **Notice:** npm automatically creates a `latest` tag on every publish (this attribute can override it). 135 | 136 | 1. `prerelease_dist_tag` 137 | 138 | Set a [npm dist-tag](https://docs.npmjs.com/cli/v6/commands/npm-dist-tag) for a prerelease version by configuring this attribute. This tag will be applied to all _prerelease_ versions (i.e. `x.x.x-prerelease`, not `x.x.x`). 139 | 140 | This allows users to install your module via tag name. For example `npm install mymodule@next`. 141 | 142 | To apply multiple tags, separate with commas. 143 | 144 | ```yaml 145 | - uses: author/action-publish@stable 146 | with: 147 | prerelease_dist_tag: next, beta, canary 148 | env: 149 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 150 | ``` 151 | 152 | This differs from `dist_tag` because it only applies to pre-releases. 153 | 154 | A common approach is to set a dist-tag for prereleases so users will not automatically install a pre-release version when they want the latest stable version. In other words, running `npm install mymodule` (which is the equivalent of `npm install mymodule@latest`) should install the latest stable version while `npm install mymodule@canary` would install the latest prerelease/bleeding edge version. 155 | 156 | ## Developer Notes 157 | 158 | This action is best used as part of a complete deployment process. Consider the following workflow: 159 | 160 | ```yaml 161 | name: Tag, Release, & Publish 162 | 163 | on: 164 | push: 165 | branches: 166 | - master 167 | 168 | jobs: 169 | build: 170 | runs-on: ubuntu-latest 171 | steps: 172 | # Checkout your updatd source code 173 | - uses: actions/checkout@v2 174 | 175 | # If the version has changed, create a new git tag for it. 176 | - name: Tag 177 | id: autotagger 178 | uses: butlerlogic/action-autotag@stable 179 | with: 180 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 181 | 182 | # The remaining steps all depend on whether or not 183 | # a new tag was created. There is no need to release/publish 184 | # updates until the code base is in a releaseable state. 185 | 186 | # Create a github release 187 | # This will create a snapshot of the module, 188 | # available in the "Releases" section on Github. 189 | - name: Release 190 | id: create_release 191 | if: steps.autotagger.outputs.tagcreated == 'yes' 192 | uses: actions/create-release@v1.0.0 193 | env: 194 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 195 | with: 196 | tag_name: ${{ steps.autotagger.outputs.tagname }} 197 | release_name: ${{ steps.autotagger.outputs.tagname }} 198 | body: ${{ steps.autotagger.outputs.tagmessage }} 199 | draft: false 200 | prerelease: ${{ steps.autotagger.outputs.prerelease == 'yes' }} 201 | 202 | # Use this action to publish a single module to npm. 203 | - name: Publish 204 | id: publish 205 | if: steps.autotagger.outputs.tagname != '' 206 | uses: author/action-publish@stable 207 | env: 208 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 209 | ``` 210 | 211 | The configuration above will run whenever new code is merged into the master branch. It will check the code out and use the [butlerlogic/action-autotag](https://github.com/butlerlogic/action-autotag) tag to automatically create a new git tag _if a new version is detected_. If there is no new tag, the action exits gracefully and successfully. 212 | 213 | If a new tag exists, the action will create a new Github Release. It is smart enough to determine whether it's a prerelease or not (draft releases are not applicable to this workflow). Once the release/pre-release is created, the code is published to npm. 214 | 215 | ### Multiple Node Modules 216 | 217 | If you're using our [cross-runtime template](https://github.com/author/template-cross-runtime), then you will likely want to publish multiple versions of your module for Node.js and the browser. This requires modified pre-release, release, and publish steps. 218 | 219 | #### Releases 220 | 221 | We like to archive each module in our releases, making it easier for developers to find prior editions they may need to function in older environents. This can be accomplished by adding a build step _after_ the release step. It may seem counterintuitive to do it after, but you'll need to create a release _before_ uploading artifacts to it. 222 | 223 | ```yaml 224 | - name: Build Release Artifacts 225 | id: build 226 | run: | 227 | cd ./build && npm install && cd ../ 228 | npm run build --if-present 229 | for d in .dist/*/*/ ; do tar -cvzf ${d%%/}-x.x.x.tar.gz ${d%%}*; done; 230 | 231 | - name: Upload Release Artifacts 232 | # This is not one of our actions 233 | uses: AButler/upload-release-assets@v2.0 234 | with: 235 | files: './.dist/**/*.tar.gz' 236 | repo-token: ${{ secrets.GITHUB_TOKEN }} 237 | ``` 238 | 239 | The last line of the build step above looks for a directory called `.dist`. By default, the cross runtime template generates bundles in: 240 | 241 | ```sh 242 | .dist 243 | > node 244 | - module 245 | - module 246 | > browsers 247 | - module 248 | - module 249 | ``` 250 | 251 | The `.dist/*/*` finds all of the `module` directories and generates a tarball from them. If you do not care about taking a snapshot of these individual modules for your release, you can remove the last line. 252 | 253 | If you do want a snapshot of your modules, none of this is necessary. 254 | 255 | #### Publishing Multiple Modules 256 | 257 | The final publish step needs to be modified to: 258 | 259 | ```yaml 260 | - name: Publish 261 | id: publish 262 | if: steps.autotagger.outputs.tagname != '' 263 | uses: author/action-publish@stable 264 | with: 265 | scan: './.dist' 266 | env: 267 | REGISTRY_TOKEN: "${{ secrets.NPM_TOKEN }}" 268 | ``` 269 | 270 | The publish action will scan the `.dist` directory (and recursively scan subdirectories) to find all modules and publish them. 271 | 272 | --- 273 | 274 | ## Credits 275 | 276 | This action was written and is primarily maintained by [Corey Butler](https://github.com/coreybutler). 277 | 278 | # Our Ask... 279 | 280 | If you use this or find value in it, please consider contributing in one or more of the following ways: 281 | 282 | 1. Click the "Sponsor" button at the top of the page. 283 | 1. Star it! 284 | 1. [Tweet about it!](https://twitter.com/intent/tweet?hashtags=github,actions&original_referer=http%3A%2F%2F127.0.0.1%3A91%2F&text=I%20am%20automating%20my%20workflow%20with%20the%20Multipublisher%20Github%20action!&tw_p=tweetbutton&url=https%3A%2F%2Fgithub.com%2Fauthor%2Faction%2Fpublish&via=goldglovecb) 285 | 1. Fix an issue. 286 | 1. Add a feature (post a proposal in an issue first!). 287 | 288 | Copyright © 2020 Author.io, Corey Butler, and Contributors. 289 | --------------------------------------------------------------------------------