├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── auto-merge.yml │ ├── codeql-analysis.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── prepare-commit-msg ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── definitions │ ├── constants.ts │ └── errors.ts ├── get-ca-config.ts ├── get-error.ts ├── index.ts ├── resolve-config.ts ├── types │ ├── code-artifact.ts │ ├── context.ts │ ├── errors.ts │ ├── global.d.ts │ ├── index.ts │ ├── semantic-release-error.d.ts │ └── util.ts ├── util │ ├── npmrc.ts │ ├── string.ts │ ├── type-guards.ts │ └── url.ts ├── verify-ca-auth.ts ├── verify-config.ts ├── verify-npm.ts └── verify-plugins.ts ├── test ├── fixtures │ └── files │ │ ├── .npmrc │ │ ├── envvars.npmrc │ │ ├── multiregistry.npmrc │ │ ├── package-with-matching-registry.json │ │ ├── package-with-mismatch-registry.json │ │ ├── package-without-registry.json │ │ └── scoped.npmrc ├── get-ca-config.spec.ts ├── helpers │ └── dummies.ts ├── index.spec.ts ├── mocks │ └── mock-context.ts ├── resolve-config.spec.ts ├── util │ ├── npmrc.spec.ts │ └── string.spec.ts ├── verify-ca-auth.spec.ts ├── verify-config.spec.ts └── verify-npm.spec.ts └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "ryansonshine", 10 | "name": "Ryan Sonshine", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/9534477?v=4", 12 | "profile": "https://ryansonshine.com", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "jaredmcateer", 19 | "name": "Jared McAteer", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/781540?v=4", 21 | "profile": "http://jaredmcateer.com", 22 | "contributions": [ 23 | "bug" 24 | ] 25 | }, 26 | { 27 | "login": "doronpr", 28 | "name": "Doron Pearl", 29 | "avatar_url": "https://avatars.githubusercontent.com/u/3373954?v=4", 30 | "profile": "https://github.com/doronpr", 31 | "contributions": [ 32 | "bug" 33 | ] 34 | } 35 | ], 36 | "contributorsPerLine": 7, 37 | "projectName": "semantic-release-codeartifact", 38 | "projectOwner": "ryansonshine", 39 | "repoType": "github", 40 | "repoHost": "https://github.com", 41 | "skipCi": true, 42 | "commitType": "docs", 43 | "commitConvention": "angular" 44 | } 45 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/types/global.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'node', 'prettier'], 5 | parserOptions: { 6 | tsconfigRootDir: __dirname, 7 | project: ['./tsconfig.json'], 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:node/recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 15 | 'plugin:prettier/recommended', 16 | ], 17 | rules: { 18 | 'prettier/prettier': 'warn', 19 | 'node/no-missing-import': 'off', 20 | 'node/no-empty-function': 'off', 21 | 'node/no-unsupported-features/es-syntax': 'off', 22 | 'node/no-missing-require': 'off', 23 | 'node/shebang': 'off', 24 | '@typescript-eslint/no-use-before-define': 'off', 25 | quotes: ['warn', 'single', { avoidEscape: true }], 26 | 'node/no-unpublished-import': 'off', 27 | '@typescript-eslint/no-unsafe-assignment': 'off', 28 | '@typescript-eslint/no-var-requires': 'off', 29 | '@typescript-eslint/ban-ts-comment': 'off', 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the repository to show as TypeScript rather than JS in GitHub 2 | *.js linguist-detectable=false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: Report a reproducible bug or regression. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Current Behavior 11 | 12 | 13 | 14 | ## Expected Behavior 15 | 16 | 17 | 18 | ## Steps to Reproduce the Problem 19 | 20 | 1. 21 | 1. 22 | 1. 23 | 24 | ## Environment 25 | 26 | - Version: 27 | - Platform: 28 | - Node.js Version: 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🌈 Feature request 3 | about: Suggest an amazing new idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Request 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | 14 | 15 | **Describe the solution you'd like** 16 | 17 | 18 | **Describe alternatives you've considered** 19 | 20 | 21 | ## Are you willing to resolve this issue by submitting a Pull Request? 22 | 23 | 26 | 27 | - [ ] Yes, I have the time, and I know how to start. 28 | - [ ] Yes, I have the time, but I don't know how to start. I would need guidance. 29 | - [ ] No, I don't have the time, although I believe I could do it if I had the time... 30 | - [ ] No, I don't have the time and I wouldn't even know how to start. 31 | 32 | 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ### Description of change 9 | 10 | 23 | 24 | ### Pull-Request Checklist 25 | 26 | 31 | 32 | - [ ] Code is up-to-date with the `main` branch 33 | - [ ] `npm run lint` passes with this change 34 | - [ ] `npm run test` passes with this change 35 | - [ ] This pull request links relevant issues as `Fixes #0000` 36 | - [ ] There are new or updated unit tests validating the change 37 | - [ ] Documentation has been updated to reflect this change 38 | - [ ] The new commits follow conventions outlined in the [conventional commit spec](https://www.conventionalcommits.org/en/v1.0.0/) 39 | 40 | 43 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | dependabot: 11 | uses: ryansonshine/ryansonshine/.github/workflows/automerge.yml@main 12 | secrets: 13 | PAT_TOKEN: ${{ secrets.PAT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # Reusable workflow for code analysis; to eject, you can replace this file with 2 | # https://github.com/ryansonshine/ryansonshine/blob/main/.github/workflows/codeql-analysis.yml 3 | name: "CodeQL" 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | # The branches below must be a subset of the branches above 10 | branches: [main] 11 | schedule: 12 | - cron: "36 7 * * 6" 13 | 14 | jobs: 15 | analyze: 16 | uses: ryansonshine/ryansonshine/.github/workflows/codeql-analysis.yml@main 17 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | # Reusable workflow for PRs; to eject, you can replace this file with 2 | # https://github.com/ryansonshine/ryansonshine/blob/main/.github/workflows/pr.yml 3 | name: Pull Request 4 | 5 | on: [pull_request] 6 | 7 | jobs: 8 | build: 9 | uses: ryansonshine/ryansonshine/.github/workflows/pr.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Reusable workflow for releases; to eject, you can replace this file with 2 | # https://github.com/ryansonshine/ryansonshine/blob/main/.github/workflows/release.yml 3 | name: Release 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write 14 | issues: write 15 | pull-requests: write 16 | uses: ryansonshine/ryansonshine/.github/workflows/release.yml@main 17 | secrets: 18 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # Compiled code 119 | lib/ 120 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && node_modules/.bin/cz --hook || true 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "arrowParens": "avoid", 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Current TS File", 9 | "type": "node", 10 | "request": "launch", 11 | "args": ["${relativeFile}"], 12 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], 13 | "envFile": "${workspaceFolder}/.env", 14 | "sourceMaps": true, 15 | "cwd": "${workspaceRoot}", 16 | "protocol": "inspector" 17 | }, 18 | { 19 | "name": "Debug Jest Tests", 20 | "type": "node", 21 | "request": "launch", 22 | "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand"], 23 | "envFile": "${workspaceFolder}/.env", 24 | "console": "integratedTerminal", 25 | "internalConsoleOptions": "neverOpen", 26 | "port": 9229, 27 | "disableOptimisticBPs": true, 28 | "windows": { 29 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 30 | } 31 | }, 32 | { 33 | "type": "node", 34 | "request": "launch", 35 | "name": "Debug Jest Current File", 36 | "program": "${workspaceFolder}/node_modules/.bin/jest", 37 | "args": ["${relativeFile}", "--config", "jest.config.js"], 38 | "console": "integratedTerminal", 39 | "internalConsoleOptions": "neverOpen", 40 | "port": 9229, 41 | "disableOptimisticBPs": true, 42 | "windows": { 43 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Semantic Release CodeArtifact 3 | 4 | [![npm package][npm-img]][npm-url] 5 | [![Build Status][build-img]][build-url] 6 | [![Downloads][downloads-img]][downloads-url] 7 | [![Issues][issues-img]][issues-url] 8 | [![Code Coverage][codecov-img]][codecov-url] 9 | [![Commitizen Friendly][commitizen-img]][commitizen-url] 10 | [![Semantic Release][semantic-release-img]][semantic-release-url] 11 | 12 | >A [semantic-release](https://github.com/semantic-release/semantic-release) plugin 13 | >for publishing packages to [AWS CodeArtifact](https://aws.amazon.com/codeartifact/). 14 | 15 | Automate your entire package release workflow including: determining the next 16 | version number, generating release notes, and publishing packages to 17 | CodeArtifact using this plugin with semantic-release. 18 | 19 | 20 | ## Table of Contents 21 | 22 | - [Install](#install) 23 | - [Usage](#usage) 24 | - [Demo](#demo) 25 | - [Requirements](#requirements) 26 | - [IAM Policy for Publishing](#iam-policy-for-publishing) 27 | - [Configuration](#configuration) 28 | - [AWS Environment variables](#aws-environment-variables) 29 | - [Plugin environment variables](#plugin-environment-variables) 30 | - [Options](#options) 31 | - [Lifecycle Hooks](#lifecycle-hooks) 32 | - [Recipes](#recipes) 33 | - [CI Configurations](#ci-configurations) 34 | - [Additional Usage](#additional-usage) 35 | - [JavaScript - npm](#javascript---npm) 36 | - [Plugin Configuration with npm](#plugin-configuration-with-npm) 37 | - [Python - pip](#python---pip) 38 | - [Java - Maven](#java---maven) 39 | - [Java - Gradle](#java---gradle) 40 | - [Contributors ✨](#contributors-) 41 | 42 | ## Install 43 | 44 | ```bash 45 | npm install -D semantic-release semantic-release-codeartifact 46 | ``` 47 | 48 | ## Usage 49 | 50 | The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration): 51 | 52 | ```json 53 | { 54 | "plugins": [ 55 | "@semantic-release/commit-analyzer", 56 | "@semantic-release/release-notes-generator", 57 | ["semantic-release-codeartifact", { 58 | "tool": "npm", 59 | "domain": "", 60 | "repository": "" 61 | }], 62 | "@semantic-release/npm", 63 | "@semantic-release/github" 64 | ] 65 | } 66 | ``` 67 | 68 | See [Additional Usage](#additional-usage) for details on using other tools with this plugin. 69 | 70 | ## Demo 71 | 72 | Check out [this example repo](https://github.com/aws-samples/aws-codeartifact-semantic-release-example) to see it in action. 73 | 74 | ## Requirements 75 | 76 | In order to use **semantic-release** you need: 77 | 78 | - To host your code in a [Git repository](https://git-scm.com) 79 | - Use a Continuous Integration service that allows you to [securely set up credentials](docs/usage/ci-configuration.md#authentication) 80 | - A Git CLI version that meets [semantic-release's version requirement][sr-git-version] installed in your Continuous Integration environment 81 | - A [Node.js](https://nodejs.org) version that meets [semantic-release's version requirement][sr-node-version] installed in your Continuous Integration environment 82 | 83 | In order to use **semantic-release-codeartifact** you need: 84 | 85 | - An [AWS Account](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) 86 | - An [AWS CodeArtifact](https://aws.amazon.com/codeartifact/) repository 87 | - A role your CI environment can assume with sufficient [IAM permissions to publish packages](#iam-policy-for-publishing) with CodeArtifact 88 | 89 | ### IAM Policy for Publishing 90 | 91 | The IAM role used by your CI environment will need the following permissions: 92 | 93 | ```json 94 | { 95 | "Version": "2012-10-17", 96 | "Statement": [ 97 | { 98 | "Effect": "Allow", 99 | "Action": [ 100 | "codeartifact:GetAuthorizationToken", 101 | "codeartifact:GetRepositoryEndpoint", 102 | "codeartifact:PublishPackageVersion" 103 | ], 104 | "Resource": "*" 105 | }, 106 | { 107 | "Effect": "Allow", 108 | "Action": "sts:GetServiceBearerToken", 109 | "Resource": "*", 110 | "Condition": { 111 | "StringEquals": { 112 | "sts:AWSServiceName": "codeartifact.amazonaws.com" 113 | } 114 | } 115 | } 116 | ] 117 | } 118 | ``` 119 | 120 | ## Configuration 121 | 122 | ### AWS Environment variables 123 | 124 | The AWS configuration is **required** for the AWS SDK which is used for getting 125 | an auth token for CodeArtifact. 126 | 127 | | Variable | Description | 128 | | ----------------------- | -------------------------------------------------------- | 129 | | `AWS_REGION` | **Required.** The AWS region to be used with the AWS SDK | 130 | | `AWS_ACCESS_KEY_ID` | **Required.** Your AWS Access Key | 131 | | `AWS_SECRET_ACCESS_KEY` | **Required.** Your AWS Secret Access Key | 132 | | `AWS_SESSION_TOKEN` | Session token if you have/need it | 133 | 134 | **Note:** Proxy configurations are supported and will be used if HTTP_PROXY or HTTPS_PROXY 135 | is found on the environment using [aws-sdk-v3-proxy](https://github.com/awslabs/aws-sdk-v3-js-proxy). 136 | 137 | ### Plugin environment variables 138 | 139 | The following environment variables can be set to configure the plugin. [Options](#options) 140 | specified by plugin config will take precedence over these environment variables. 141 | 142 | | Variable | Description | 143 | | -------------------- | ---------------------------------------------------------------------- | 144 | | `SR_CA_TOOL` | Tool to connect with the CodeArtifact repository | 145 | | `SR_CA_DOMAIN` | Your CodeArtifact domain name | 146 | | `SR_CA_REPOSITORY` | Your CodeArtifact repository name | 147 | | `SR_CA_DOMAIN_OWNER` | The AWS Account ID that owns your CodeArtifact domain | 148 | | `SR_CA_DURATION_SEC` | The time, in seconds, that login information for CodeArtifact is valid | 149 | 150 | ### Options 151 | 152 | | Option | Description | Default | 153 | | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | 154 | | `tool` | **Required.** Tool to connect with the CodeArtifact repository | `SR_CA_TOOL` environment variable. | 155 | | `domain` | **Required.** Your CodeArtifact domain name | `SR_CA_DOMAIN` environment variable. | 156 | | `repository` | **Required.** Your CodeArtifact repository name | `SR_CA_REPOSITORY` environment variable. | 157 | | `domainOwner` | The AWS Account ID that owns your CodeArtifact domain | `SR_CA_DOMAIN_OWNER` environment variable. | 158 | | `durationSections` | The time, in seconds, that login information for CodeArtifact is valid | `7200` (2 hours) | 159 | | `skipPluginCheck` | Skips the check for required plugins, this can be used if you are using your own custom plugins for your specified tool | `false` | 160 | 161 | ## Lifecycle Hooks 162 | 163 | | Step | Description | 164 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 165 | | `verifyConditions` | Verify the presence and the validity of the authentication (set via [configuration](#configuration)), and provide authentication values to the semantic-release plugin related to the CodeArtifact tool being used | 166 | 167 | ## Recipes 168 | 169 | ### CI Configurations 170 | 171 | - [GitHub Actions](https://github.com/aws-samples/aws-codeartifact-semantic-release-example/blob/main/.github/workflows/release.yml) 172 | - GitLab (coming soon - PRs welcome) 173 | - CircleCI (coming soon - PRs welcome) 174 | 175 | ## Additional Usage 176 | 177 | CodeArtifact supports multiple tools including npm (JavaScript), Maven and Gradle 178 | (Java), and pip (Python). Each contain different dependencies and are listed below. 179 | 180 | ### JavaScript - npm 181 | 182 | Required dependencies: 183 | 184 | - [`@semantic-release/npm`](https://www.npmjs.com/package/@semantic-release/npm) 185 | 186 | ```bash 187 | npm install --save-dev semantic-release semantic-release-codeartifact 188 | ``` 189 | 190 | #### Plugin Configuration with npm 191 | 192 | *semantic-release includes the other plugins listed below:* 193 | 194 | ```json 195 | { 196 | "plugins": [ 197 | "@semantic-release/commit-analyzer", 198 | "@semantic-release/release-notes-generator", 199 | ["semantic-release-codeartifact", { 200 | "tool": "npm", 201 | "domain": "", 202 | "repository": "" 203 | }], 204 | "@semantic-release/npm", 205 | "@semantic-release/github" 206 | ] 207 | } 208 | ``` 209 | 210 | **Note:** `semantic-release-codeartifact` must be listed before `@semantic-release/npm` 211 | 212 | ### Python - pip 213 | 214 | Support for pip coming soon 215 | 216 | ### Java - Maven 217 | 218 | Support for Maven coming soon 219 | 220 | ### Java - Gradle 221 | 222 | Support for Gradle coming soon 223 | 224 | ## Contributors ✨ 225 | 226 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
Ryan Sonshine
Ryan Sonshine

💻
Jared McAteer
Jared McAteer

🐛
Doron Pearl
Doron Pearl

🐛
240 | 241 | 242 | 243 | 244 | 245 | 246 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 247 | 248 | [build-img]:https://github.com/ryansonshine/semantic-release-codeartifact/actions/workflows/release.yml/badge.svg 249 | [build-url]:https://github.com/ryansonshine/semantic-release-codeartifact/actions/workflows/release.yml 250 | [downloads-img]:https://img.shields.io/npm/dt/semantic-release-codeartifact 251 | [downloads-url]:https://www.npmtrends.com/semantic-release-codeartifact 252 | [npm-img]:https://img.shields.io/npm/v/semantic-release-codeartifact 253 | [npm-url]:https://www.npmjs.com/package/semantic-release-codeartifact 254 | [issues-img]:https://img.shields.io/github/issues/ryansonshine/semantic-release-codeartifact 255 | [issues-url]:https://github.com/ryansonshine/semantic-release-codeartifact/issues 256 | [codecov-img]:https://codecov.io/gh/ryansonshine/semantic-release-codeartifact/branch/main/graph/badge.svg 257 | [codecov-url]:https://codecov.io/gh/ryansonshine/semantic-release-codeartifact 258 | [semantic-release-img]:https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 259 | [semantic-release-url]:https://github.com/semantic-release/semantic-release 260 | [commitizen-img]:https://img.shields.io/badge/commitizen-friendly-brightgreen.svg 261 | [commitizen-url]:http://commitizen.github.io/cz-cli/ 262 | [sr-node-version]:https://github.com/semantic-release/semantic-release/blob/master/docs/support/node-version.md 263 | [sr-git-version]:https://github.com/semantic-release/semantic-release/blob/master/docs/support/git-version.md 264 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/test/**/*.spec.ts'], 5 | collectCoverageFrom: [ 6 | '/src/**/*.ts', 7 | '!/src/types/**/*.ts', 8 | ], 9 | globals: { 10 | 'ts-jest': { 11 | diagnostics: false, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-release-codeartifact", 3 | "version": "0.0.0-development", 4 | "description": "semantic-release plugin for AWS CodeArtifact", 5 | "main": "./lib/src/index.js", 6 | "files": [ 7 | "lib/**/*" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ryansonshine/semantic-release-codeartifact.git" 12 | }, 13 | "homepage": "https://github.com/ryansonshine/semantic-release-codeartifact#readme", 14 | "bugs": { 15 | "url": "https://github.com/ryansonshine/semantic-release-codeartifact/issues" 16 | }, 17 | "contributors": [ 18 | { 19 | "name": "Ryan Sonshine", 20 | "url": "https://github.com/ryansonshine" 21 | } 22 | ], 23 | "engines": { 24 | "node": ">=14.17" 25 | }, 26 | "keywords": [ 27 | "aws", 28 | "codeartifact", 29 | "semver", 30 | "package", 31 | "publish", 32 | "automation", 33 | "semantic versioning", 34 | "semantic-release" 35 | ], 36 | "license": "MIT", 37 | "scripts": { 38 | "build": "tsc", 39 | "clean": "rm -rf ./lib/", 40 | "cm": "cz", 41 | "lint": "eslint ./src/ --fix", 42 | "prepare": "husky install", 43 | "semantic-release": "semantic-release", 44 | "test:watch": "jest --watch", 45 | "test": "jest --coverage", 46 | "typecheck": "tsc --noEmit" 47 | }, 48 | "dependencies": { 49 | "@aws-sdk/client-codeartifact": "^3.13.1", 50 | "@semantic-release/error": "^2.2.0", 51 | "aggregate-error": "^3.1.0", 52 | "aws-sdk-v3-proxy": "^2.0.11", 53 | "fs-extra": "^10.0.0", 54 | "read-pkg": "5.2.0", 55 | "save": "^2.4.0" 56 | }, 57 | "devDependencies": { 58 | "@ryansonshine/commitizen": "^4.2.8", 59 | "@ryansonshine/cz-conventional-changelog": "^3.3.4", 60 | "@types/fs-extra": "^9.0.11", 61 | "@types/jest": "^27.5.2", 62 | "@types/node": "^12.20.11", 63 | "@types/signale": "^1.4.1", 64 | "@typescript-eslint/eslint-plugin": "^4.22.0", 65 | "@typescript-eslint/parser": "^4.22.0", 66 | "aws-sdk-client-mock": "^0.4.0", 67 | "conventional-changelog-conventionalcommits": "^5.0.0", 68 | "eslint": "^7.25.0", 69 | "eslint-config-prettier": "^8.3.0", 70 | "eslint-plugin-node": "^11.1.0", 71 | "eslint-plugin-prettier": "^3.4.0", 72 | "husky": "^6.0.0", 73 | "jest": "^27.2.0", 74 | "lint-staged": "^13.2.1", 75 | "prettier": "^2.2.1", 76 | "semantic-release": "^21.0.1", 77 | "signale": "^1.4.0", 78 | "tempy": "^1.0.1", 79 | "ts-jest": "^27.0.5", 80 | "ts-node": "^10.2.1", 81 | "typescript": "^4.2.4" 82 | }, 83 | "config": { 84 | "commitizen": { 85 | "path": "./node_modules/@ryansonshine/cz-conventional-changelog" 86 | } 87 | }, 88 | "lint-staged": { 89 | "*.ts": "eslint --cache --cache-location .eslintcache --fix" 90 | }, 91 | "release": { 92 | "branches": [ 93 | "main" 94 | ], 95 | "plugins": [ 96 | [ 97 | "@semantic-release/commit-analyzer", 98 | { 99 | "preset": "conventionalcommits", 100 | "releaseRules": [ 101 | { 102 | "type": "build", 103 | "scope": "deps", 104 | "release": "patch" 105 | } 106 | ] 107 | } 108 | ], 109 | [ 110 | "@semantic-release/release-notes-generator", 111 | { 112 | "preset": "conventionalcommits", 113 | "presetConfig": { 114 | "types": [ 115 | { 116 | "type": "feat", 117 | "section": "Features" 118 | }, 119 | { 120 | "type": "fix", 121 | "section": "Bug Fixes" 122 | }, 123 | { 124 | "type": "build", 125 | "section": "Dependencies and Other Build Updates", 126 | "hidden": false 127 | } 128 | ] 129 | } 130 | } 131 | ], 132 | "@semantic-release/npm", 133 | "@semantic-release/github" 134 | ] 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/definitions/constants.ts: -------------------------------------------------------------------------------- 1 | /** The default duration seconds to use for CodeArtifact auth (2 hours) */ 2 | export const DEFAULT_DURATION_SECONDS = 60 * 120; 3 | 4 | export const SUPPORTED_TOOL_LIST = ['npm']; 5 | -------------------------------------------------------------------------------- /src/definitions/errors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/restrict-template-expressions */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | import { ErrorDefinitions } from '../types'; 5 | import { SUPPORTED_TOOL_LIST } from './constants'; 6 | 7 | const homepage = 8 | 'https://github.com/ryansonshine/semantic-release-codeartifact'; 9 | 10 | const linkify = (file: string): string => `${homepage}/blob/main/${file}`; 11 | 12 | export const ERROR_DEFINITIONS: ErrorDefinitions = { 13 | EINVALIDTOOL: ({ tool = '' }) => ({ 14 | message: 'Invalid CodeArtifact tool.', 15 | details: `The [CodeArtifact tool](${linkify( 16 | 'README.md#codeartifact-tool' 17 | )}) option, if defined, must be one of '${SUPPORTED_TOOL_LIST.join("','")}'. 18 | 19 | Your configuration for the \`tool\` option is \`${tool}\`.`, 20 | }), 21 | ENODOMAINSET: () => ({ 22 | message: 'No CodeArtifact domain specified.', 23 | details: `A [CodeArtifact domain](${linkify( 24 | 'README.md#options' 25 | )}) must be set in the plugin options.`, 26 | }), 27 | ENOREPOSET: () => ({ 28 | message: 'No CodeArtifact repository specified.', 29 | details: `A [CodeArtifact repository](${linkify( 30 | 'README.md#options' 31 | )}) must be set in the plugin options.`, 32 | }), 33 | ENOAWSACCESSKEY: () => ({ 34 | message: 'No AWS access key specified.', 35 | details: `An [AWS Access Key ID](https://docs.aws.amazon.com/general/latest/gr/glos-chap.html#accesskeyID) must be specified to get an auth token from CodeArtifact. 36 | 37 | Please make sure to set the \`AWS_ACCESS_KEY_ID\` environment variable in your CI environment. 38 | See [AWS Environment variables](${linkify( 39 | 'README.md#aws-environment-variables' 40 | )}) for more details.`, 41 | }), 42 | ENOAWSREGION: () => ({ 43 | message: 'No AWS region specified.', 44 | details: `An [AWS Region](https://docs.aws.amazon.com/general/latest/gr/glos-chap.html#region) must be specified to get an auth token from CodeArtifact. 45 | 46 | Please make sure to set the \`AWS_REGION\` environment variable in your CI environment. 47 | See [AWS Environment variables](${linkify( 48 | 'README.md#aws-environment-variables' 49 | )}) for more details.`, 50 | }), 51 | ENOAWSSECRETKEY: () => ({ 52 | message: 'No AWS secret access key specified.', 53 | details: `An [AWS Secret Access Key](https://docs.aws.amazon.com/general/latest/gr/glos-chap.html#SecretAccessKey) must be specified to get an auth token from CodeArtifact. 54 | 55 | Please make sure to set the \`AWS_SECRET_ACCESS_KEY\` environment variable in your CI environment. 56 | See [AWS Environment variables](${linkify( 57 | 'README.md#aws-environment-variables' 58 | )}) for more details.`, 59 | }), 60 | EMISSINGPLUGIN: ({ plugin, tool, requiredPlugins }) => ({ 61 | message: 'Missing plugin.', 62 | details: `The plugin configuration is missing plugin '${plugin}' and is required for '${tool}'. 63 | 64 | The required plugins for are: ['${requiredPlugins.join("','")}'].`, 65 | }), 66 | EPUBLISHCONFIGMISMATCH: ({ repositoryEndpoint, publishConfig }) => ({ 67 | message: 'Mismatch on CodeArtifact repository and publishConfig.', 68 | details: `The registry set in the \`publishConfig\` of your package.json does not match the CodeArtifact endpoint. 69 | 70 | The package.json \`publishConfig\` registry is '${publishConfig.registry}.' 71 | The CodeArtifact endpoint is '${repositoryEndpoint}'.`, 72 | }), 73 | ENPMRCCONFIGMISMATCH: ({ repositoryEndpoint, registry }) => ({ 74 | message: 'Mismatch on CodeArtifact repository and npmrc registry', 75 | details: `The registry set in the \`.npmrc\` of your project root does not match the CodeArtifact endpoint. 76 | 77 | The \`.npmrc\` registry is '${registry}.' 78 | The CodeArtifact endpoint is '${repositoryEndpoint}'.`, 79 | }), 80 | ENPMRCMULTIPLEREGISTRY: ({ registries }) => ({ 81 | message: 'Multiple registries found in npmrc', 82 | details: `Your \`.npmrc\` contains multiple registries but should only contain one. 83 | 84 | Please remove extraneous registries from your \`.npmrc\`. 85 | Registries found: ['${registries.join("','")}'].`, 86 | }), 87 | ENOAUTHTOKEN: () => ({ 88 | message: 'No auth token returned from CodeArtifact client', 89 | details: `The CodeArtifact client returned and empty value for \`authToken\`. 90 | 91 | Please check your AWS configuration and try again.`, 92 | }), 93 | ENOREPOENDPOINT: () => ({ 94 | message: 'No endpoint returned from CodeArtifact client', 95 | details: `The CodeArtifact client returned and empty value for \`repositoryEndpoint\`. 96 | 97 | Please check your AWS configuration and try again.`, 98 | }), 99 | EAWSSDK: ({ message, name }) => ({ 100 | message: 'AWS SDK Error', 101 | details: `The AWS SDK threw an error while using the CodeArtifact client. 102 | 103 | Name: '${name}' 104 | Message: '${message}'`, 105 | }), 106 | }; 107 | -------------------------------------------------------------------------------- /src/get-ca-config.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CodeArtifactConfig, 3 | PluginConfig, 4 | VerifyConditionsContext, 5 | } from './types'; 6 | import { 7 | CodeartifactClient, 8 | GetAuthorizationTokenCommand, 9 | GetRepositoryEndpointCommand, 10 | } from '@aws-sdk/client-codeartifact'; 11 | import { getError } from './get-error'; 12 | import AggregateError from 'aggregate-error'; 13 | import { isAWSError } from './util/type-guards'; 14 | import { addProxyToClient } from 'aws-sdk-v3-proxy'; 15 | 16 | export const getCodeArtifactConfig = async ( 17 | pluginConfig: PluginConfig, 18 | context: VerifyConditionsContext 19 | ): Promise => { 20 | const errors = []; 21 | const { env } = context; 22 | const { domain, domainOwner, tool, repository } = pluginConfig; 23 | try { 24 | const client = addProxyToClient( 25 | new CodeartifactClient({ 26 | credentials: { 27 | accessKeyId: env.AWS_ACCESS_KEY_ID as string, 28 | secretAccessKey: env.AWS_SECRET_ACCESS_KEY as string, 29 | sessionToken: env.AWS_SESSION_TOKEN, 30 | }, 31 | region: env.AWS_REGION, 32 | }), 33 | { throwOnNoProxy: false } 34 | ); 35 | 36 | const getTokenCmd = new GetAuthorizationTokenCommand({ 37 | domain, 38 | domainOwner, 39 | }); 40 | const { authorizationToken = '' } = await client.send(getTokenCmd); 41 | 42 | const getEndpointCmd = new GetRepositoryEndpointCommand({ 43 | domain, 44 | format: tool, 45 | repository, 46 | domainOwner, 47 | }); 48 | const { repositoryEndpoint = '' } = await client.send(getEndpointCmd); 49 | 50 | if (!authorizationToken) { 51 | errors.push(getError('ENOAUTHTOKEN')); 52 | } 53 | if (!repositoryEndpoint) { 54 | errors.push(getError('ENOREPOENDPOINT')); 55 | } 56 | 57 | if (errors.length > 0) { 58 | throw new AggregateError(errors); 59 | } 60 | 61 | return { authorizationToken, repositoryEndpoint }; 62 | } catch (e) { 63 | if (e instanceof AggregateError) throw e; 64 | 65 | if (isAWSError(e)) { 66 | console.error(e); 67 | errors.push(getError('EAWSSDK', { message: e.message, name: e.name })); 68 | } else { 69 | console.error(e); 70 | errors.push( 71 | getError('EAWSSDK', { 72 | name: 'UnknownException', 73 | message: 74 | 'An unknown error has occured, check the logs for more details.', 75 | }) 76 | ); 77 | } 78 | 79 | throw new AggregateError(errors); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/get-error.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorDefinitions, PluginConfig } from './types'; 2 | import SemanticReleaseError from '@semantic-release/error'; 3 | import { ERROR_DEFINITIONS } from './definitions/errors'; 4 | 5 | export const getError = ( 6 | code: keyof ErrorDefinitions, 7 | args: PluginConfig | Record = {} 8 | ): SemanticReleaseError => { 9 | const { message, details } = ERROR_DEFINITIONS[code](args); 10 | return new SemanticReleaseError(message, code, details); 11 | }; 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginConfig, VerifyConditionsContext } from './types'; 2 | import { verifyCodeArtifact } from './verify-ca-auth'; 3 | import AggregateError from 'aggregate-error'; 4 | 5 | export async function verifyConditions( 6 | pluginConfig: PluginConfig, 7 | context: VerifyConditionsContext 8 | ): Promise { 9 | const errors = await verifyCodeArtifact(pluginConfig, context); 10 | 11 | if (errors.length > 0) { 12 | throw new AggregateError(errors); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/resolve-config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginConfig, VerifyConditionsContext } from './types'; 2 | import { DEFAULT_DURATION_SECONDS } from './definitions/constants'; 3 | 4 | export const resolveConfig = ( 5 | pluginConfig: PluginConfig, 6 | { env }: VerifyConditionsContext 7 | ): PluginConfig => ({ 8 | ...pluginConfig, 9 | tool: pluginConfig.tool || env.SR_CA_TOOL || '', 10 | domain: pluginConfig.domain || env.SR_CA_DOMAIN || '', 11 | domainOwner: pluginConfig.domainOwner || env.SR_CA_DOMAIN_OWNER, 12 | durationSeconds: +( 13 | pluginConfig.durationSeconds || 14 | env.SR_CA_DURATION_SEC || 15 | DEFAULT_DURATION_SECONDS 16 | ), 17 | repository: pluginConfig.repository || env.SR_CA_REPOSITORY || '', 18 | skipPluginCheck: pluginConfig.skipPluginCheck ?? false, 19 | }); 20 | -------------------------------------------------------------------------------- /src/types/code-artifact.ts: -------------------------------------------------------------------------------- 1 | export interface CodeArtifactConfig { 2 | authorizationToken: string; 3 | repositoryEndpoint: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/context.ts: -------------------------------------------------------------------------------- 1 | import type SemanticReleaseError from '@semantic-release/error'; 2 | import type { WriteStream } from 'fs'; 3 | import type { Signale } from 'signale'; 4 | import type { KnownKeys } from './util'; 5 | 6 | /** Context keys shared across all lifecycle methods */ 7 | export interface CommonContext { 8 | stdout: WriteStream; 9 | stderr: WriteStream; 10 | logger: Signale; 11 | } 12 | 13 | /** Context provided to the `verifyConditions` lifecycle method */ 14 | export interface VerifyConditionsContext extends CommonContext { 15 | /** Current working directory */ 16 | cwd: string; 17 | /** Environment variables */ 18 | env: NodeJS.ProcessEnv; 19 | /** Information about CI environment */ 20 | envCi: EnvCi; 21 | /** Options passed to `semantic-release` via CLI, configuration files, etc. */ 22 | options: OptionsBase; 23 | /** Information on the current branch */ 24 | branch: Branch; 25 | /** Information on branches */ 26 | branches: Branch[]; 27 | } 28 | 29 | /** Context provided to the `analyzeCommits` lifecycle method */ 30 | export interface AnalyzeCommitsContext extends VerifyConditionsContext { 31 | /** List of commits taken into account when determining new version */ 32 | commits: CommitDetail[]; 33 | /** List of releases */ 34 | releases: Release[]; 35 | /** Most recent release */ 36 | lastRelease: Release; 37 | } 38 | 39 | /** Context provided to the `verifyRelease` lifecycle method */ 40 | export interface VerifyReleaseContext extends AnalyzeCommitsContext { 41 | /** Next release */ 42 | nextRelease: Release; 43 | } 44 | 45 | /** Context provided to the `generateNotes` lifecycle method */ 46 | export type GenerateNotesContext = VerifyReleaseContext; 47 | 48 | /** Context provided to the `addChannel` lifecycle method */ 49 | export type AddChannelContext = VerifyReleaseContext; 50 | 51 | /** Context provided to the `prepareContext` lifecycle method */ 52 | export type PrepareContext = VerifyReleaseContext; 53 | 54 | /** Context provided to the `publish` lifecycle method */ 55 | export type PublishContext = VerifyReleaseContext; 56 | 57 | /** Context provided to the `success` lifecycle method */ 58 | export type SuccessContext = VerifyReleaseContext; 59 | 60 | /** Context provided to the `fail` lifecycle method */ 61 | export interface FailContext extends VerifyReleaseContext { 62 | errors: SemanticReleaseError[]; 63 | } 64 | 65 | export interface EnvCi { 66 | /** True if the environment is a CI environment */ 67 | isCi: boolean; 68 | /** Commit hash */ 69 | commit: string; 70 | /** Current branch */ 71 | branch: string; 72 | } 73 | 74 | export interface Branch { 75 | tags: Tag[]; 76 | type: string; 77 | name: string; 78 | range?: string; 79 | accept?: string[]; 80 | main?: boolean; 81 | channel?: string; 82 | prerelease?: boolean; 83 | } 84 | 85 | export interface Tag { 86 | gitTag: string; 87 | version: string; 88 | channels: Channel[]; 89 | } 90 | 91 | export interface OptionsBase { 92 | branches: (Branch | string)[]; 93 | repositoryUrl: string; 94 | tagFormat: string; 95 | plugins: string[]; 96 | dryRun: boolean; 97 | [key: string]: any; 98 | } 99 | 100 | // export type CodeArtifactTool = 'npm' | 'nuget' | 'dotnet' | 'pip' | 'twine'; 101 | 102 | export interface PluginConfig extends OptionsBase { 103 | /** Tool to connect with the CodeArtifact repository */ 104 | tool: string; 105 | /** Your CodeArtifact domain name */ 106 | domain: string; 107 | /** The AWS Account ID that owns your CodeArtifact domain */ 108 | domainOwner?: string; 109 | /** The time, in seconds, that the login information is valid */ 110 | durationSeconds?: number; 111 | /** Your CodeArtifact repository name */ 112 | repository: string; 113 | /** Skips the check for required plugins based on CodeArtifact tool */ 114 | skipPluginCheck?: boolean; 115 | } 116 | 117 | export type PluginConfigKey = KnownKeys< 118 | Exclude 119 | >; 120 | 121 | export type Channel = null | string; 122 | 123 | export interface Release { 124 | version: string; 125 | gitTag: string; 126 | channels: (Channel | null)[]; 127 | gitHead: string; 128 | name: string; 129 | } 130 | 131 | export interface CommitDetail { 132 | commit: Commit; 133 | tree: Commit; 134 | author: Author; 135 | committer: Author; 136 | subject: string; 137 | body: string; 138 | hash: string; 139 | committerDate: string; 140 | message: string; 141 | gitTags: string; 142 | } 143 | 144 | export interface Author { 145 | name: string; 146 | email: string; 147 | date: string; 148 | } 149 | 150 | export interface Commit { 151 | long: string; 152 | short: string; 153 | } 154 | -------------------------------------------------------------------------------- /src/types/errors.ts: -------------------------------------------------------------------------------- 1 | import { PluginConfig } from './context'; 2 | 3 | export interface ErrorDetails { 4 | message: string; 5 | details: string; 6 | } 7 | 8 | export interface ErrorDefinitions { 9 | EINVALIDTOOL: (pluginConfig: Partial) => ErrorDetails; 10 | ENODOMAINSET: (...args: unknown[]) => ErrorDetails; 11 | ENOREPOSET: () => ErrorDetails; 12 | ENOAWSREGION: () => ErrorDetails; 13 | ENOAWSACCESSKEY: () => ErrorDetails; 14 | ENOAWSSECRETKEY: () => ErrorDetails; 15 | EMISSINGPLUGIN: (pluginConfig: Partial) => ErrorDetails; 16 | EPUBLISHCONFIGMISMATCH: (pluginConfig: Partial) => ErrorDetails; 17 | ENPMRCCONFIGMISMATCH: (pluginConfig: Partial) => ErrorDetails; 18 | ENPMRCMULTIPLEREGISTRY: (pluginConfig: Partial) => ErrorDetails; 19 | ENOAUTHTOKEN: () => ErrorDetails; 20 | ENOREPOENDPOINT: () => ErrorDetails; 21 | EAWSSDK: (pluginConfig: Partial) => ErrorDetails; 22 | } 23 | 24 | export type AWSError = Error & { 25 | $metadata: Record; 26 | code: string; 27 | errno: string; 28 | hostname: string; 29 | syscall: string; 30 | }; 31 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | /** Tool to connect with the CodeArtifact repository */ 5 | SR_CA_TOOL?: string; 6 | /** Your CodeArtifact domain name */ 7 | SR_CA_DOMAIN?: string; 8 | /** The AWS Account ID that owns your CodeArtifact domain */ 9 | SR_CA_DOMAIN_OWNER?: string; 10 | /** The time, in seconds, that the login information is valid */ 11 | SR_CA_DURATION_SEC?: string; 12 | /** Your CodeArtifact repository name */ 13 | SR_CA_REPOSITORY?: string; 14 | AWS_REGION?: string; 15 | AWS_ACCESS_KEY_ID?: string; 16 | AWS_SECRET_ACCESS_KEY?: string; 17 | } 18 | } 19 | } 20 | 21 | export {}; 22 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './errors'; 3 | export * from './code-artifact'; 4 | export * from './util'; 5 | -------------------------------------------------------------------------------- /src/types/semantic-release-error.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@semantic-release/error' { 2 | declare class SemanticReleaseError extends Error { 3 | constructor(message: string, code: string, details: string); 4 | name: 'SemanticReleaseError'; 5 | code: string; 6 | details: string; 7 | semanticRelease: true; 8 | } 9 | 10 | export = SemanticReleaseError; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/util.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/Microsoft/TypeScript/issues/25987#issuecomment-408339599 2 | export type KnownKeys = keyof { 3 | [K in keyof T as string extends K 4 | ? never 5 | : number extends K 6 | ? never 7 | : K]: never; 8 | }; 9 | -------------------------------------------------------------------------------- /src/util/npmrc.ts: -------------------------------------------------------------------------------- 1 | export const getRegistryFromNpmrc = (npmrc = ''): string[] => { 2 | return npmrc 3 | .split('\n') 4 | .filter(config => config.includes('registry=')) 5 | .map(registry => { 6 | const [, url] = registry.split('registry='); 7 | return url; 8 | }); 9 | }; 10 | 11 | export const replaceEnvVarsInNpmrc = (npmrc = ''): string => { 12 | const re = /(\$\{)([^}]+)(})/g; 13 | const matches = npmrc.matchAll(re); 14 | for (const match of matches) { 15 | npmrc = npmrc.replace(match[0], process.env[match[2]] || ''); 16 | } 17 | 18 | return npmrc; 19 | }; 20 | -------------------------------------------------------------------------------- /src/util/string.ts: -------------------------------------------------------------------------------- 1 | export const removeTrailingSlash = (str: unknown): string => 2 | typeof str === 'string' ? str.replace(/\/$/, '') : ''; 3 | -------------------------------------------------------------------------------- /src/util/type-guards.ts: -------------------------------------------------------------------------------- 1 | import { AWSError } from '../types/errors'; 2 | 3 | export const isAWSError = (e: unknown): e is AWSError => 4 | Boolean(e && e instanceof Error && (e as AWSError).$metadata !== undefined); 5 | -------------------------------------------------------------------------------- /src/util/url.ts: -------------------------------------------------------------------------------- 1 | import { removeTrailingSlash } from './string'; 2 | 3 | export const isUrlMatch = (url1: unknown, url2: unknown): boolean => 4 | typeof url1 === 'string' && 5 | typeof url2 === 'string' && 6 | removeTrailingSlash(url1) === removeTrailingSlash(url2); 7 | -------------------------------------------------------------------------------- /src/verify-ca-auth.ts: -------------------------------------------------------------------------------- 1 | import type { PluginConfig, VerifyConditionsContext } from './types'; 2 | import type SemanticReleaseError from '@semantic-release/error'; 3 | import AggregateError from 'aggregate-error'; 4 | import { resolveConfig } from './resolve-config'; 5 | import { verifyNpm } from './verify-npm'; 6 | import { verifyConfig } from './verify-config'; 7 | import { getCodeArtifactConfig } from './get-ca-config'; 8 | 9 | export const verifyCodeArtifact = async ( 10 | pluginConfig: PluginConfig, 11 | context: VerifyConditionsContext 12 | ): Promise => { 13 | const resolvedConfig = resolveConfig(pluginConfig, context); 14 | const errors = verifyConfig(resolvedConfig, context); 15 | 16 | if (errors.length > 0) { 17 | throw new AggregateError(errors); 18 | } 19 | 20 | const codeArtifactConfig = await getCodeArtifactConfig( 21 | resolvedConfig, 22 | context 23 | ); 24 | 25 | return await verifyNpm(resolvedConfig, context, codeArtifactConfig, errors); 26 | }; 27 | -------------------------------------------------------------------------------- /src/verify-config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginConfig, VerifyConditionsContext } from './types'; 2 | import type SemanticReleaseError from '@semantic-release/error'; 3 | import { SUPPORTED_TOOL_LIST } from './definitions/constants'; 4 | import { getError } from './get-error'; 5 | 6 | export const verifyConfig = ( 7 | pluginConfig: PluginConfig, 8 | context: VerifyConditionsContext 9 | ): SemanticReleaseError[] => { 10 | const errors = []; 11 | const { domain, tool, repository } = pluginConfig; 12 | const { env } = context; 13 | 14 | if (!domain) { 15 | errors.push(getError('ENODOMAINSET')); 16 | } 17 | 18 | if (!repository) { 19 | errors.push(getError('ENOREPOSET')); 20 | } 21 | 22 | if (!SUPPORTED_TOOL_LIST.includes(tool)) { 23 | errors.push(getError('EINVALIDTOOL', pluginConfig)); 24 | } 25 | 26 | if (!env.AWS_REGION) { 27 | errors.push(getError('ENOAWSREGION')); 28 | } 29 | 30 | if (!env.AWS_ACCESS_KEY_ID) { 31 | errors.push(getError('ENOAWSACCESSKEY')); 32 | } 33 | 34 | if (!env.AWS_SECRET_ACCESS_KEY) { 35 | errors.push(getError('ENOAWSSECRETKEY')); 36 | } 37 | 38 | return errors; 39 | }; 40 | -------------------------------------------------------------------------------- /src/verify-npm.ts: -------------------------------------------------------------------------------- 1 | import type SemanticReleaseError from '@semantic-release/error'; 2 | import type { 3 | CodeArtifactConfig, 4 | PluginConfig, 5 | VerifyConditionsContext, 6 | } from './types'; 7 | import { resolve } from 'path'; 8 | import { appendFile, pathExists, readFile, writeFile } from 'fs-extra'; 9 | import readPkg from 'read-pkg'; 10 | import { getError } from './get-error'; 11 | import { isUrlMatch } from './util/url'; 12 | import { getRegistryFromNpmrc, replaceEnvVarsInNpmrc } from './util/npmrc'; 13 | import { verifyPlugins } from './verify-plugins'; 14 | 15 | const REQUIRED_PLUGINS = ['@semantic-release/npm']; 16 | 17 | export const verifyNpm = async ( 18 | pluginConfig: PluginConfig, 19 | context: VerifyConditionsContext, 20 | { authorizationToken, repositoryEndpoint }: CodeArtifactConfig, 21 | errors: SemanticReleaseError[] 22 | ): Promise => { 23 | const { cwd, logger } = context; 24 | 25 | const { publishConfig = {} } = await readPkg({ cwd }); 26 | const npmrcPath = resolve(cwd, '.npmrc'); 27 | 28 | verifyPlugins(pluginConfig, context, REQUIRED_PLUGINS, errors); 29 | 30 | if (publishConfig.registry) { 31 | logger.log( 32 | 'Validating `publishConfig.registry` from `package.json` matches CodeArtifact endpoint' 33 | ); 34 | 35 | if (!isUrlMatch(publishConfig.registry, repositoryEndpoint)) { 36 | errors.push( 37 | getError('EPUBLISHCONFIGMISMATCH', { 38 | repositoryEndpoint, 39 | publishConfig, 40 | }) 41 | ); 42 | return errors; 43 | } 44 | 45 | logger.log( 46 | 'Validated `publishConfig.registry` matches CodeArtifact endpoint %s', 47 | repositoryEndpoint 48 | ); 49 | } 50 | 51 | if (await pathExists(npmrcPath)) { 52 | logger.log('Validating `.npmrc` matches CodeArtifact endpoint'); 53 | const npmrc = await readFile(npmrcPath, 'utf8'); 54 | const formattedNpmrc = replaceEnvVarsInNpmrc(npmrc); 55 | const [registry, ...otherRegistries] = getRegistryFromNpmrc(formattedNpmrc); 56 | 57 | // npmrc exists but no registries are listed 58 | if (registry) { 59 | if (otherRegistries.length) { 60 | errors.push( 61 | getError('ENPMRCMULTIPLEREGISTRY', { 62 | registries: [registry, ...otherRegistries], 63 | }) 64 | ); 65 | } 66 | 67 | if (!isUrlMatch(registry, repositoryEndpoint)) { 68 | errors.push( 69 | getError('ENPMRCCONFIGMISMATCH', { repositoryEndpoint, registry }) 70 | ); 71 | } 72 | } else { 73 | // append to existing npmrc 74 | logger.log( 75 | 'No registry found in existing `.npmrc`, appending CodeArtifact registry' 76 | ); 77 | await appendFile( 78 | npmrcPath, 79 | `\nregistry=${repositoryEndpoint}\n//${repositoryEndpoint.replace( 80 | /(^\w+:|^)\/\//, 81 | '' 82 | )}:always-auth=true\n` 83 | ); 84 | } 85 | } else { 86 | // If no .npmrc exists, create one 87 | await writeFile( 88 | npmrcPath, 89 | `registry=${repositoryEndpoint}\n//${repositoryEndpoint.replace( 90 | /(^\w+:|^)\/\//, 91 | '' 92 | )}:always-auth=true\n` 93 | ); 94 | } 95 | 96 | logger.info( 97 | 'Setting process.env.NPM_TOKEN with the token retrieved from CodeArtifact' 98 | ); 99 | process.env.NPM_TOKEN = authorizationToken; 100 | 101 | return errors; 102 | }; 103 | -------------------------------------------------------------------------------- /src/verify-plugins.ts: -------------------------------------------------------------------------------- 1 | import type SemanticReleaseError from '@semantic-release/error'; 2 | import { getError } from './get-error'; 3 | import { PluginConfig, VerifyConditionsContext } from './types'; 4 | 5 | export const verifyPlugins = ( 6 | pluginConfig: PluginConfig, 7 | { logger, options: { plugins } }: VerifyConditionsContext, 8 | requiredPlugins: string[], 9 | errors: SemanticReleaseError[] 10 | ): SemanticReleaseError[] => { 11 | const { skipPluginCheck, tool } = pluginConfig; 12 | 13 | if (skipPluginCheck) { 14 | logger.info('Skipping plugin check: skipPluginCheck is true'); 15 | return errors; 16 | } 17 | 18 | for (const plugin of requiredPlugins) { 19 | logger.log('Verifying plugin "%s" exists in config', plugin); 20 | if (!plugins.includes(plugin)) { 21 | logger.error('Missing plugin %s', plugin); 22 | errors.push( 23 | getError('EMISSINGPLUGIN', { plugin, tool, plugins, requiredPlugins }) 24 | ); 25 | } 26 | } 27 | 28 | return errors; 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/files/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/ 2 | //my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/:always-auth=true 3 | -------------------------------------------------------------------------------- /test/fixtures/files/envvars.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://my-domain-${AWS_ACCOUNT_ID}.d.codeartifact.${AWS_REGION}.amazonaws.com/npm/my-repo/ 2 | //my-domain-${AWS_ACCOUNT_ID}.d.codeartifact.${AWS_REGION}.amazonaws.com/npm/my-repo/:always-auth=true 3 | -------------------------------------------------------------------------------- /test/fixtures/files/multiregistry.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/ 2 | //my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/:always-auth=true 3 | registry=https://my-domain-000000001.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/ 4 | //my-domain-000000001.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/:always-auth=true 5 | registry=https://my-domain-000000002.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/ 6 | //my-domain-000000002.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/:always-auth=true 7 | -------------------------------------------------------------------------------- /test/fixtures/files/package-with-matching-registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-release-codeartifact", 3 | "version": "0.0.0-development", 4 | "description": "semantic-release plugin for AWS CodeArtifact", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ryansonshine/semantic-release-codeartifact.git" 8 | }, 9 | "homepage": "https://github.com/ryansonshine/semantic-release-codeartifact#readme", 10 | "contributors": [ 11 | { 12 | "name": "Ryan Sonshine", 13 | "url": "https://github.com/ryansonshine" 14 | } 15 | ], 16 | "publishConfig": { 17 | "registry": "https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/" 18 | }, 19 | "scripts": {}, 20 | "dependencies": {}, 21 | "devDependencies": {}, 22 | "release": { 23 | "branches": ["main"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/files/package-with-mismatch-registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-release-codeartifact", 3 | "version": "0.0.0-development", 4 | "description": "semantic-release plugin for AWS CodeArtifact", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ryansonshine/semantic-release-codeartifact.git" 8 | }, 9 | "homepage": "https://github.com/ryansonshine/semantic-release-codeartifact#readme", 10 | "contributors": [ 11 | { 12 | "name": "Ryan Sonshine", 13 | "url": "https://github.com/ryansonshine" 14 | } 15 | ], 16 | "publishConfig": { 17 | "registry": "http://publish-config-registry.local" 18 | }, 19 | "scripts": {}, 20 | "dependencies": {}, 21 | "devDependencies": {}, 22 | "release": { 23 | "branches": ["main"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/files/package-without-registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-release-codeartifact", 3 | "version": "0.0.0-development", 4 | "description": "semantic-release plugin for AWS CodeArtifact", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ryansonshine/semantic-release-codeartifact.git" 8 | }, 9 | "homepage": "https://github.com/ryansonshine/semantic-release-codeartifact#readme", 10 | "contributors": [ 11 | { 12 | "name": "Ryan Sonshine", 13 | "url": "https://github.com/ryansonshine" 14 | } 15 | ], 16 | "scripts": {}, 17 | "dependencies": {}, 18 | "devDependencies": {}, 19 | "release": { 20 | "branches": ["main"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/files/scoped.npmrc: -------------------------------------------------------------------------------- 1 | @my-org:registry=https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/ 2 | //my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/:always-auth=true 3 | -------------------------------------------------------------------------------- /test/get-ca-config.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | import type { ErrorDefinitions } from '../src/types'; 3 | import { mockClient } from 'aws-sdk-client-mock'; 4 | import { 5 | CodeartifactClient, 6 | GetAuthorizationTokenCommand, 7 | GetRepositoryEndpointCommand, 8 | } from '@aws-sdk/client-codeartifact'; 9 | import { makePluginConfig } from './helpers/dummies'; 10 | import { getMockContext } from './mocks/mock-context'; 11 | import { getCodeArtifactConfig } from '../src/get-ca-config'; 12 | 13 | const mockCaClient = mockClient(CodeartifactClient); 14 | 15 | const config = makePluginConfig(); 16 | const context = getMockContext(); 17 | 18 | const authorizationToken = 19 | '16b1690f-4a51-4e2f-a9d6-ff5b0ec1189f-test-auth-token'; 20 | const repositoryEndpoint = 21 | 'https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/'; 22 | 23 | describe('get-ca-config', () => { 24 | describe('getCodeArtifactConfig', () => { 25 | beforeEach(() => { 26 | mockCaClient.reset(); 27 | mockCaClient 28 | .on(GetAuthorizationTokenCommand) 29 | .resolves({ 30 | authorizationToken, 31 | }) 32 | .on(GetRepositoryEndpointCommand) 33 | .resolves({ 34 | repositoryEndpoint, 35 | }); 36 | }); 37 | 38 | it('should return an object containing the auth token and repository endpoint', async () => { 39 | const result = await getCodeArtifactConfig(config, context); 40 | 41 | expect(result).toEqual({ authorizationToken, repositoryEndpoint }); 42 | }); 43 | 44 | it('should throw an AggregateError containing a SemanticRelease error if no auth token is returned', async () => { 45 | expect.assertions(3); 46 | mockCaClient.on(GetAuthorizationTokenCommand).resolves({}); 47 | 48 | try { 49 | await getCodeArtifactConfig(config, context); 50 | } catch (errors) { 51 | const [error, ...otherErrors] = errors as any[]; 52 | expect(error?.code).toEqual('ENOAUTHTOKEN'); 53 | expect(error?.name).toEqual('SemanticReleaseError'); 54 | expect(otherErrors).toHaveLength(0); 55 | } 56 | }); 57 | 58 | it('should throw an AggregateError containing a SemanticRelease error if no repository is returned', async () => { 59 | expect.assertions(3); 60 | mockCaClient.on(GetRepositoryEndpointCommand).resolves({}); 61 | 62 | try { 63 | await getCodeArtifactConfig(config, context); 64 | } catch (errors) { 65 | const [error, ...otherErrors] = errors as any[]; 66 | expect(error?.code).toEqual('ENOREPOENDPOINT'); 67 | expect(error?.name).toEqual('SemanticReleaseError'); 68 | expect(otherErrors).toHaveLength(0); 69 | } 70 | }); 71 | 72 | it('should throw an AggregateError containing a SemanticRelease error when an aws error is thrown', async () => { 73 | expect.hasAssertions(); 74 | mockCaClient.on(GetAuthorizationTokenCommand).rejects({ 75 | $metadata: {}, 76 | message: 'Failed to get auth token', 77 | name: 'TestAWSError', 78 | }); 79 | 80 | try { 81 | await getCodeArtifactConfig(config, context); 82 | } catch (errors) { 83 | const [error, ...otherErrors] = errors as any[]; 84 | expect(error?.code).toEqual('EAWSSDK'); 85 | expect(error?.name).toEqual('SemanticReleaseError'); 86 | expect(error?.details).toMatch('TestAWSError'); 87 | expect(otherErrors).toHaveLength(0); 88 | } 89 | }); 90 | 91 | it('should throw an AggregateError containing a SemanticRelease error when a generic error is thrown', async () => { 92 | expect.hasAssertions(); 93 | mockCaClient 94 | .on(GetAuthorizationTokenCommand) 95 | .rejects(new Error('Generic error')); 96 | 97 | try { 98 | await getCodeArtifactConfig(config, context); 99 | } catch (errors) { 100 | const [error, ...otherErrors] = errors as any[]; 101 | expect(error?.code).toEqual('EAWSSDK'); 102 | expect(error?.name).toEqual('SemanticReleaseError'); 103 | expect(error?.details).toMatch('UnknownException'); 104 | expect(otherErrors).toHaveLength(0); 105 | } 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/helpers/dummies.ts: -------------------------------------------------------------------------------- 1 | import { CodeArtifactConfig, PluginConfig } from '../../src/types'; 2 | 3 | export const makePluginConfig = ( 4 | overrides: Partial = {} 5 | ): PluginConfig => ({ 6 | branches: ['+([0-9])?(.{+([0-9]),x}).x', 'master', 'next', 'next-major'], 7 | domain: 'test-domain', 8 | dryRun: true, 9 | plugins: [], 10 | repository: 'test-repository', 11 | repositoryUrl: 12 | 'https://github.com/ryansonshine/semantic-release-codeartifact.git', 13 | domainOwner: '012345678912', 14 | durationSeconds: 600, 15 | tagFormat: 'v${version}', 16 | tool: 'npm', 17 | skipPluginCheck: false, 18 | ...overrides, 19 | }); 20 | 21 | export const makeCodeArtifactConfig = ( 22 | overrides: Partial = {} 23 | ): CodeArtifactConfig => ({ 24 | authorizationToken: '16b1690f-4a51-4e2f-a9d6-ff5b0ec1189f-test-auth-token', 25 | repositoryEndpoint: 26 | 'https://my-domain-123456789012.d.codeartifact.us-east-1.amazonaws.com/npm/my-repo/', 27 | ...overrides, 28 | }); 29 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { verifyCodeArtifact } from '../src/verify-ca-auth'; 2 | import { mocked } from 'ts-jest/utils'; 3 | import SemanticReleaseError from '@semantic-release/error'; 4 | import { verifyConditions } from '../src'; 5 | import { makePluginConfig } from './helpers/dummies'; 6 | import { getMockContext } from './mocks/mock-context'; 7 | 8 | jest.mock('../src/verify-ca-auth'); 9 | 10 | const mockVerifyCodeArtifact = mocked(verifyCodeArtifact); 11 | 12 | describe('index', () => { 13 | describe('verifyConditions', () => { 14 | const pluginConfig = makePluginConfig(); 15 | const mockContext = getMockContext(); 16 | 17 | beforeEach(() => { 18 | jest.resetAllMocks(); 19 | }); 20 | 21 | it('should throw when errors are returned from verifyCodeArtifact', async () => { 22 | mockVerifyCodeArtifact.mockResolvedValue([ 23 | new SemanticReleaseError('error msg', 'code', 'details'), 24 | ]); 25 | 26 | const fn = () => verifyConditions(pluginConfig, mockContext); 27 | 28 | await expect(fn).rejects.toThrowError(); 29 | }); 30 | 31 | it('should resolve when no errors are returned from verifyCodeArtifact', async () => { 32 | mockVerifyCodeArtifact.mockResolvedValue([]); 33 | 34 | const result = await verifyConditions(pluginConfig, mockContext); 35 | 36 | expect(result).toBeUndefined(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/mocks/mock-context.ts: -------------------------------------------------------------------------------- 1 | import { VerifyConditionsContext } from '../../src/types'; 2 | 3 | export const getMockContext = (): VerifyConditionsContext => ({ 4 | branch: { 5 | name: 'main', 6 | type: 'release', 7 | tags: [{ gitTag: 'v1.0.0', version: '1.0.0', channels: [null] }], 8 | }, 9 | branches: [ 10 | { 11 | name: 'main', 12 | type: 'release', 13 | tags: [{ gitTag: 'v1.0.0', version: '1.0.0', channels: [null] }], 14 | }, 15 | ], 16 | cwd: '.', 17 | env: { 18 | AWS_ACCESS_KEY_ID: 'test-access-key-id', 19 | AWS_REGION: 'test-region', 20 | AWS_SECRET_ACCESS_KEY: 'test-secret-access-key', 21 | SR_CA_TOOL: 'npm', 22 | SR_CA_DOMAIN: 'my-domain', 23 | SR_CA_DOMAIN_OWNER: '123456789', 24 | SR_CA_DURATION_SEC: '120', 25 | SR_CA_REPOSITORY: 'my-repository', 26 | }, 27 | envCi: { 28 | branch: 'main', 29 | commit: '123abc', 30 | isCi: true, 31 | }, 32 | // TODO: Appropriate mocking 33 | logger: { 34 | log: jest.fn(), 35 | error: jest.fn(), 36 | info: jest.fn(), 37 | } as any, 38 | options: { 39 | branches: ['main'], 40 | dryRun: false, 41 | plugins: [ 42 | '@semantic-release/commit-analyzer', 43 | '@semantic-release/release-notes-generator', 44 | '@semantic-release/npm', 45 | '@semantic-release/github', 46 | ], 47 | repositoryUrl: 48 | 'https://github.com/ryansonshine/semantic-release-codeartifact.git', 49 | tagFormat: 'v${version}', 50 | }, 51 | // TODO: Appropriate mocking 52 | stderr: jest.fn() as any, 53 | stdout: jest.fn() as any, 54 | }); 55 | -------------------------------------------------------------------------------- /test/resolve-config.spec.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_DURATION_SECONDS } from '../src/definitions/constants'; 2 | import { resolveConfig } from '../src/resolve-config'; 3 | import { 4 | PluginConfig, 5 | PluginConfigKey, 6 | VerifyConditionsContext, 7 | } from '../src/types'; 8 | import { makePluginConfig } from './helpers/dummies'; 9 | import { getMockContext } from './mocks/mock-context'; 10 | 11 | const config = makePluginConfig(); 12 | const context = getMockContext(); 13 | const pluginConfigKeys: PluginConfigKey[] = [ 14 | 'domain', 15 | 'domainOwner', 16 | 'durationSeconds', 17 | 'repository', 18 | 'tool', 19 | 'skipPluginCheck', 20 | ]; 21 | 22 | describe('resolve-config', () => { 23 | describe('resolveConfig', () => { 24 | it('should preserve existing, non-related plugin configurations', () => { 25 | const existingConfigs = { a: 'a', b: 'b', c: [] }; 26 | 27 | const resolvedConfig = resolveConfig( 28 | { ...config, ...existingConfigs }, 29 | context 30 | ); 31 | 32 | expect(resolvedConfig).toEqual(expect.objectContaining(existingConfigs)); 33 | }); 34 | 35 | it('should resolve plugin configurations over environment variables', () => { 36 | expect.assertions(pluginConfigKeys.length); 37 | const contextWithEnv: VerifyConditionsContext = { 38 | ...getMockContext(), 39 | env: { 40 | SR_CA_DOMAIN: 'env-domain', 41 | SR_CA_DOMAIN_OWNER: 'env-domain_owner', 42 | SR_CA_DURATION_SEC: 'env-duration_sec', 43 | SR_CA_REPOSITORY: 'env-repository', 44 | SR_CA_TOOL: 'env-tool', 45 | }, 46 | }; 47 | 48 | const resolvedConfig = resolveConfig(config, contextWithEnv); 49 | 50 | for (const key of pluginConfigKeys) { 51 | expect(resolvedConfig[key]).toEqual(config[key]); 52 | } 53 | }); 54 | 55 | it('should resolve enviornment variables when plugin configurations do not exist', () => { 56 | const configWithoutOptions: PluginConfig = { 57 | ...makePluginConfig(), 58 | // @ts-expect-error 59 | domain: undefined, 60 | domainOwner: undefined, 61 | durationSeconds: undefined, 62 | // @ts-expect-error 63 | repository: undefined, 64 | // @ts-expect-error 65 | tool: undefined, 66 | }; 67 | const env = { 68 | SR_CA_DOMAIN: 'env-domain', 69 | SR_CA_DOMAIN_OWNER: 'env-domain_owner', 70 | SR_CA_DURATION_SEC: '99999999', 71 | SR_CA_REPOSITORY: 'env-repository', 72 | SR_CA_TOOL: 'env-tool', 73 | }; 74 | const contextWithEnv: VerifyConditionsContext = { 75 | ...getMockContext(), 76 | env, 77 | }; 78 | 79 | const resolvedConfig = resolveConfig( 80 | configWithoutOptions, 81 | contextWithEnv 82 | ); 83 | 84 | expect(resolvedConfig.domain).toEqual(env.SR_CA_DOMAIN); 85 | expect(resolvedConfig.domainOwner).toEqual(env.SR_CA_DOMAIN_OWNER); 86 | expect(resolvedConfig.durationSeconds).toEqual(+env.SR_CA_DURATION_SEC); 87 | expect(resolvedConfig.repository).toEqual(env.SR_CA_REPOSITORY); 88 | expect(resolvedConfig.tool).toEqual(env.SR_CA_TOOL); 89 | }); 90 | 91 | it('should resolve default values when no config options or env vars are set', () => { 92 | const configWithoutOptions: PluginConfig = { 93 | ...makePluginConfig(), 94 | // @ts-expect-error 95 | domain: undefined, 96 | domainOwner: undefined, 97 | durationSeconds: undefined, 98 | // @ts-expect-error 99 | repository: undefined, 100 | // @ts-expect-error 101 | tool: undefined, 102 | }; 103 | const contextWithoutEnv: VerifyConditionsContext = { 104 | ...getMockContext(), 105 | env: {}, 106 | }; 107 | 108 | const resolvedConfig = resolveConfig( 109 | configWithoutOptions, 110 | contextWithoutEnv 111 | ); 112 | 113 | expect(resolvedConfig.tool).toEqual(''); 114 | expect(resolvedConfig.domain).toEqual(''); 115 | expect(resolvedConfig.domainOwner).toBeUndefined(); 116 | expect(resolvedConfig.durationSeconds).toEqual(DEFAULT_DURATION_SECONDS); 117 | expect(resolvedConfig.repository).toEqual(''); 118 | }); 119 | 120 | it('should resolve skipPluginCheck to false when value is undefined', () => { 121 | const configWithPluginSkip: PluginConfig = { 122 | ...makePluginConfig(), 123 | skipPluginCheck: undefined, 124 | }; 125 | 126 | const resolvedConfig = resolveConfig(configWithPluginSkip, context); 127 | 128 | expect(resolvedConfig.skipPluginCheck).toEqual(false); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/util/npmrc.spec.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs-extra'; 2 | import { 3 | getRegistryFromNpmrc, 4 | replaceEnvVarsInNpmrc, 5 | } from '../../src/util/npmrc'; 6 | 7 | const fixtures = 'test/fixtures/files'; 8 | const RE_URL = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/; 9 | 10 | describe('npmrc', () => { 11 | const PREV_ENV = process.env; 12 | 13 | beforeEach(() => { 14 | process.env = { ...PREV_ENV }; 15 | }); 16 | 17 | afterEach(() => { 18 | process.env = PREV_ENV; 19 | }); 20 | 21 | describe('getRegistryFromNpmrc', () => { 22 | it('should handle a single registry npmrc file', async () => { 23 | const singleRegistry = await readFile(`${fixtures}/.npmrc`, 'utf8'); 24 | 25 | const [registry, ...otherRegistries] = getRegistryFromNpmrc( 26 | singleRegistry 27 | ); 28 | 29 | expect(registry).toMatch(RE_URL); 30 | expect(otherRegistries).toHaveLength(0); 31 | }); 32 | 33 | it('should handle a scoped registry npmrc file', async () => { 34 | const scopedRegistry = await readFile(`${fixtures}/scoped.npmrc`, 'utf8'); 35 | 36 | const [registry, ...otherRegistries] = getRegistryFromNpmrc( 37 | scopedRegistry 38 | ); 39 | 40 | expect(registry).toMatch(RE_URL); 41 | expect(otherRegistries).toHaveLength(0); 42 | }); 43 | 44 | it('should not throw when an undefined value is passed in', () => { 45 | const registry = getRegistryFromNpmrc(); 46 | 47 | expect(registry).toEqual([]); 48 | }); 49 | }); 50 | 51 | describe('replaceEnvVarsInNpmrc', () => { 52 | it('should substitute an environment variables with the value on process.env', async () => { 53 | const accountId = '123456789012'; 54 | const region = 'us-east-1'; 55 | const expectedUrl = `https://my-domain-${accountId}.d.codeartifact.${region}.amazonaws.com/npm/my-repo/`; 56 | process.env.AWS_ACCOUNT_ID = accountId; 57 | process.env.AWS_REGION = region; 58 | const npmrc = await readFile(`${fixtures}/envvars.npmrc`, 'utf8'); 59 | 60 | const formattedNpmrc = replaceEnvVarsInNpmrc(npmrc); 61 | 62 | expect(formattedNpmrc).toEqual(expect.stringContaining(expectedUrl)); 63 | }); 64 | 65 | it('should return the same string if no environment variables are present', async () => { 66 | const npmrc = await readFile(`${fixtures}/.npmrc`, 'utf8'); 67 | 68 | const formattedNpmrc = replaceEnvVarsInNpmrc(npmrc); 69 | 70 | expect(formattedNpmrc).toEqual(npmrc); 71 | }); 72 | 73 | it('should not throw when an undefined value is passed in', () => { 74 | const formattedNpmrc = replaceEnvVarsInNpmrc(); 75 | 76 | expect(formattedNpmrc).toEqual(''); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/util/string.spec.ts: -------------------------------------------------------------------------------- 1 | import { removeTrailingSlash } from '../../src/util/string'; 2 | 3 | describe('string', () => { 4 | describe('removeTrailingSlash', () => { 5 | it('should remove trailing slashes with expected inputs', () => { 6 | expect(removeTrailingSlash('https://google.com/')).toEqual( 7 | 'https://google.com' 8 | ); 9 | 10 | expect(removeTrailingSlash(0)).toEqual(''); 11 | 12 | expect( 13 | removeTrailingSlash('http://local/url/with/trailing/slash/') 14 | ).toEqual('http://local/url/with/trailing/slash'); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/verify-ca-auth.spec.ts: -------------------------------------------------------------------------------- 1 | import SemanticReleaseError from '@semantic-release/error'; 2 | import AggregateError from 'aggregate-error'; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { getCodeArtifactConfig } from '../src/get-ca-config'; 5 | import { resolveConfig } from '../src/resolve-config'; 6 | import { PluginConfig } from '../src/types'; 7 | import { verifyCodeArtifact } from '../src/verify-ca-auth'; 8 | import { verifyConfig } from '../src/verify-config'; 9 | import { verifyNpm } from '../src/verify-npm'; 10 | import { makeCodeArtifactConfig, makePluginConfig } from './helpers/dummies'; 11 | import { getMockContext } from './mocks/mock-context'; 12 | jest.mock('../src/resolve-config'); 13 | jest.mock('../src/verify-config'); 14 | jest.mock('../src/get-ca-config'); 15 | jest.mock('../src/verify-npm'); 16 | 17 | const mockResolveConfig = mocked(resolveConfig, true); 18 | const mockVerifyConfig = mocked(verifyConfig, true); 19 | const mockGetCodeArtifactConfig = mocked(getCodeArtifactConfig, true); 20 | const mockVerifyNpm = mocked(verifyNpm, true); 21 | 22 | describe('verify-ca-auth', () => { 23 | describe('verifyCodeArtifact', () => { 24 | const config = makePluginConfig(); 25 | const context = getMockContext(); 26 | const caConfig = makeCodeArtifactConfig(); 27 | 28 | beforeEach(() => { 29 | jest.resetAllMocks(); 30 | mockResolveConfig.mockReturnValue(config); 31 | mockVerifyConfig.mockReturnValue([]); 32 | mockGetCodeArtifactConfig.mockResolvedValue(caConfig); 33 | }); 34 | 35 | it('should throw an AggregateError if configuration errors exist', async () => { 36 | const semanticReleaseError = new SemanticReleaseError( 37 | 'message', 38 | 'code', 39 | 'details' 40 | ); 41 | const aggregateError = new AggregateError([semanticReleaseError]); 42 | 43 | mockVerifyConfig.mockReturnValue([semanticReleaseError]); 44 | 45 | const fn = () => verifyCodeArtifact(config, context); 46 | 47 | await expect(fn).rejects.toThrowError(aggregateError); 48 | }); 49 | 50 | it('should pass the resolved configuration to getCodeArtifactConfig', async () => { 51 | const resolvedConfig: PluginConfig = { 52 | ...config, 53 | repository: 'test-resolved-conf', 54 | }; 55 | mockResolveConfig.mockReturnValue(resolvedConfig); 56 | 57 | await verifyCodeArtifact(resolvedConfig, context); 58 | 59 | expect(getCodeArtifactConfig).toHaveBeenCalledWith( 60 | resolvedConfig, 61 | context 62 | ); 63 | }); 64 | 65 | it('should return any errors returned from verifyNpm', async () => { 66 | const errors: SemanticReleaseError[] = [ 67 | new SemanticReleaseError('message', 'code', 'details'), 68 | ]; 69 | mockVerifyNpm.mockResolvedValue(errors); 70 | 71 | const result = await verifyCodeArtifact(config, context); 72 | 73 | expect(result).toEqual(errors); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/verify-config.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorDefinitions, 3 | PluginConfig, 4 | VerifyConditionsContext, 5 | } from '../src/types'; 6 | import { verifyConfig } from '../src/verify-config'; 7 | import { makePluginConfig } from './helpers/dummies'; 8 | import { getMockContext } from './mocks/mock-context'; 9 | 10 | describe('verify-config', () => { 11 | describe('verifyConfig', () => { 12 | let config: PluginConfig; 13 | let context: VerifyConditionsContext; 14 | beforeEach(() => { 15 | config = makePluginConfig(); 16 | context = getMockContext(); 17 | }); 18 | 19 | it('should return a SemanticRelease error if domain is not set', () => { 20 | // @ts-expect-error 21 | config.domain = undefined; 22 | 23 | const [error, ...otherErrors] = verifyConfig(config, context); 24 | 25 | expect(error?.code).toEqual('ENODOMAINSET'); 26 | expect(error?.name).toEqual('SemanticReleaseError'); 27 | expect(otherErrors).toHaveLength(0); 28 | }); 29 | 30 | it('should return a SemanticRelease error if repository is not set', () => { 31 | // @ts-expect-error 32 | config.repository = undefined; 33 | 34 | const [error, ...otherErrors] = verifyConfig(config, context); 35 | 36 | expect(error?.code).toEqual('ENOREPOSET'); 37 | expect(error?.name).toEqual('SemanticReleaseError'); 38 | expect(otherErrors).toHaveLength(0); 39 | }); 40 | 41 | it('should return a SemanticRelease error if an invalid tool is provided', () => { 42 | config.tool = 'not-supported'; 43 | 44 | const [error, ...otherErrors] = verifyConfig(config, context); 45 | 46 | expect(error?.code).toEqual('EINVALIDTOOL'); 47 | expect(error?.name).toEqual('SemanticReleaseError'); 48 | expect(otherErrors).toHaveLength(0); 49 | }); 50 | 51 | it('should return a SemanticRelease error if tool is not set', () => { 52 | // @ts-expect-error 53 | config.tool = undefined; 54 | 55 | const [error, ...otherErrors] = verifyConfig(config, context); 56 | 57 | expect(error?.code).toEqual('EINVALIDTOOL'); 58 | expect(error?.name).toEqual('SemanticReleaseError'); 59 | expect(otherErrors).toHaveLength(0); 60 | }); 61 | 62 | it('should return a SemanticRelease error if AWS_REGION is not set', () => { 63 | context.env.AWS_REGION = undefined; 64 | 65 | const [error, ...otherErrors] = verifyConfig(config, context); 66 | 67 | expect(error?.code).toEqual('ENOAWSREGION'); 68 | expect(error?.name).toEqual('SemanticReleaseError'); 69 | expect(otherErrors).toHaveLength(0); 70 | }); 71 | 72 | it('should return a SemanticRelease error if AWS_ACCESS_KEY_ID is not set', () => { 73 | context.env.AWS_ACCESS_KEY_ID = undefined; 74 | 75 | const [error, ...otherErrors] = verifyConfig(config, context); 76 | 77 | expect(error?.code).toEqual('ENOAWSACCESSKEY'); 78 | expect(error?.name).toEqual('SemanticReleaseError'); 79 | expect(otherErrors).toHaveLength(0); 80 | }); 81 | 82 | it('should return a SemanticRelease error if AWS_SECRET_ACCESS_KEY is not set', () => { 83 | context.env.AWS_SECRET_ACCESS_KEY = undefined; 84 | 85 | const [error, ...otherErrors] = verifyConfig(config, context); 86 | 87 | expect(error?.code).toEqual('ENOAWSSECRETKEY'); 88 | expect(error?.name).toEqual('SemanticReleaseError'); 89 | expect(otherErrors).toHaveLength(0); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/verify-npm.spec.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ErrorDefinitions, 3 | PluginConfig, 4 | VerifyConditionsContext, 5 | } from '../src/types'; 6 | import { verifyNpm } from '../src/verify-npm'; 7 | import { makeCodeArtifactConfig, makePluginConfig } from './helpers/dummies'; 8 | import { getMockContext } from './mocks/mock-context'; 9 | import tempy from 'tempy'; 10 | import { copy, readFile, writeFile } from 'fs-extra'; 11 | 12 | const fixtures = 'test/fixtures/files'; 13 | 14 | const copyDefaultPackage = async (cwd: string) => 15 | await copy( 16 | `${fixtures}/package-without-registry.json`, 17 | `${cwd}/package.json` 18 | ); 19 | 20 | describe('verify-npm', () => { 21 | describe('verifyNpm', () => { 22 | const pluginConfig = makePluginConfig(); 23 | const caConfig = makeCodeArtifactConfig(); 24 | const mockContext = getMockContext(); 25 | 26 | const PREV_ENV = process.env; 27 | 28 | beforeEach(() => { 29 | process.env = { ...PREV_ENV }; 30 | jest.resetAllMocks(); 31 | }); 32 | 33 | afterEach(() => { 34 | process.env = PREV_ENV; 35 | }); 36 | 37 | it('should add an error when missing a plugin from the list of required plugins', async () => { 38 | await tempy.directory.task(async cwd => { 39 | const contextWithoutPlugins: VerifyConditionsContext = { 40 | ...mockContext, 41 | cwd, 42 | options: { 43 | ...mockContext.options, 44 | plugins: [], 45 | }, 46 | }; 47 | const missingPluginCode: keyof ErrorDefinitions = 'EMISSINGPLUGIN'; 48 | await copy(fixtures, cwd); 49 | await copyDefaultPackage(cwd); 50 | 51 | const [error] = await verifyNpm( 52 | pluginConfig, 53 | contextWithoutPlugins, 54 | caConfig, 55 | [] 56 | ); 57 | 58 | expect(error?.code).toEqual(missingPluginCode); 59 | expect(error?.name).toEqual('SemanticReleaseError'); 60 | }); 61 | }); 62 | 63 | it('should NOT add an error when missing a plugin from the list of required plugins while skipPluginCheck is true', async () => { 64 | await tempy.directory.task(async cwd => { 65 | const contextWithoutPlugins: VerifyConditionsContext = { 66 | ...mockContext, 67 | cwd, 68 | options: { 69 | ...mockContext.options, 70 | plugins: [], 71 | }, 72 | }; 73 | const configWithSkip: PluginConfig = { 74 | ...pluginConfig, 75 | skipPluginCheck: true, 76 | }; 77 | await copy(fixtures, cwd); 78 | await copyDefaultPackage(cwd); 79 | 80 | const [error] = await verifyNpm( 81 | configWithSkip, 82 | contextWithoutPlugins, 83 | caConfig, 84 | [] 85 | ); 86 | 87 | expect(error).toBeUndefined(); 88 | }); 89 | }); 90 | 91 | it('should add an error if the publishConfig registry exists but does not match the CA endpoint', async () => { 92 | await tempy.directory.task(async cwd => { 93 | const mismatchPublishCode: keyof ErrorDefinitions = 94 | 'EPUBLISHCONFIGMISMATCH'; 95 | await copy( 96 | `${fixtures}/package-with-mismatch-registry.json`, 97 | `${cwd}/package.json` 98 | ); 99 | 100 | const [error, ...otherErrors] = await verifyNpm( 101 | pluginConfig, 102 | { ...mockContext, cwd }, 103 | caConfig, 104 | [] 105 | ); 106 | 107 | expect(error?.code).toEqual(mismatchPublishCode); 108 | expect(error?.name).toEqual('SemanticReleaseError'); 109 | expect(otherErrors).toHaveLength(0); 110 | }); 111 | }); 112 | 113 | it('should resolve with no errors if the publishConfig registry matches the CA endpoint', async () => { 114 | await tempy.directory.task(async cwd => { 115 | await copy( 116 | `${fixtures}/package-with-matching-registry.json`, 117 | `${cwd}/package.json` 118 | ); 119 | 120 | const errors = await verifyNpm( 121 | pluginConfig, 122 | { ...mockContext, cwd }, 123 | caConfig, 124 | [] 125 | ); 126 | 127 | expect(errors).toHaveLength(0); 128 | }); 129 | }); 130 | 131 | it('should add an error if multiple registries exist in the existing npmrc', async () => { 132 | await tempy.directory.task(async cwd => { 133 | const multipleRegistryCode: keyof ErrorDefinitions = 134 | 'ENPMRCMULTIPLEREGISTRY'; 135 | await copy(`${fixtures}/multiregistry.npmrc`, `${cwd}/.npmrc`); 136 | await copyDefaultPackage(cwd); 137 | 138 | const [error, ...otherErrors] = await verifyNpm( 139 | pluginConfig, 140 | { ...mockContext, cwd }, 141 | caConfig, 142 | [] 143 | ); 144 | 145 | expect(error.code).toEqual(multipleRegistryCode); 146 | expect(error.name).toEqual('SemanticReleaseError'); 147 | expect(otherErrors).toHaveLength(0); 148 | }); 149 | }); 150 | 151 | it('should add an error if npmrc exists and the registry does not match the CA endpoint', async () => { 152 | await tempy.directory.task(async cwd => { 153 | const mismatchNpmrcCode: keyof ErrorDefinitions = 154 | 'ENPMRCCONFIGMISMATCH'; 155 | const caMismatchConfig = makeCodeArtifactConfig({ 156 | repositoryEndpoint: 'https://not-a-match.local/', 157 | }); 158 | await copy(fixtures, cwd); 159 | await copyDefaultPackage(cwd); 160 | 161 | const [error, ...otherErrors] = await verifyNpm( 162 | pluginConfig, 163 | { ...mockContext, cwd }, 164 | caMismatchConfig, 165 | [] 166 | ); 167 | 168 | expect(error?.code).toEqual(mismatchNpmrcCode); 169 | expect(error?.name).toEqual('SemanticReleaseError'); 170 | expect(otherErrors).toHaveLength(0); 171 | }); 172 | }); 173 | 174 | it('should NOT add an error if npmrc has environment variables that match with substitution', async () => { 175 | process.env.AWS_ACCOUNT_ID = '123456789012'; 176 | process.env.AWS_REGION = 'us-east-1'; 177 | await tempy.directory.task(async cwd => { 178 | await copy(`${fixtures}/envvars.npmrc`, `${cwd}/.npmrc`); 179 | await copyDefaultPackage(cwd); 180 | 181 | const errors = await verifyNpm( 182 | pluginConfig, 183 | { ...mockContext, cwd }, 184 | caConfig, 185 | [] 186 | ); 187 | 188 | expect(errors).toHaveLength(0); 189 | }); 190 | }); 191 | 192 | it('should write the registry to npmrc when no publishConfig exists and an npmrc exists but contains no registry', async () => { 193 | await tempy.directory.task(async cwd => { 194 | const npmrc = `${cwd}/.npmrc`; 195 | const npmrcContents = 196 | '//my-unrelated-registry.local/:always-auth=true\n'; 197 | const expected = await readFile(`${fixtures}/.npmrc`, 'utf8'); 198 | await copyDefaultPackage(cwd); 199 | await writeFile(npmrc, npmrcContents); 200 | 201 | const errors = await verifyNpm( 202 | pluginConfig, 203 | { ...mockContext, cwd }, 204 | caConfig, 205 | [] 206 | ); 207 | const result = await readFile(npmrc, 'utf8'); 208 | 209 | expect(result.split('\n')).toContain(expected.split('\n')[0]); 210 | expect(errors).toHaveLength(0); 211 | }); 212 | }); 213 | 214 | it('should not write to npmrc if npmrc exists and has a matching registry', async () => { 215 | await tempy.directory.task(async cwd => { 216 | const npmrc = `${cwd}/.npmrc`; 217 | const npmrcContents = `registry=${caConfig.repositoryEndpoint}`; 218 | await copyDefaultPackage(cwd); 219 | await writeFile(npmrc, npmrcContents); 220 | 221 | const errors = await verifyNpm( 222 | pluginConfig, 223 | { ...mockContext, cwd }, 224 | caConfig, 225 | [] 226 | ); 227 | const result = await readFile(npmrc, 'utf8'); 228 | 229 | expect(result).toEqual(npmrcContents); 230 | expect(errors).toHaveLength(0); 231 | }); 232 | }); 233 | 234 | it('should write the registry to npmrc when no publishConfig exists and no npmrc exists', async () => { 235 | await tempy.directory.task(async cwd => { 236 | const expectedNpmrc = await readFile(`${fixtures}/.npmrc`, 'utf8'); 237 | await copyDefaultPackage(cwd); 238 | 239 | await verifyNpm(pluginConfig, { ...mockContext, cwd }, caConfig, []); 240 | const result = await readFile(`${cwd}/.npmrc`, 'utf8'); 241 | 242 | expect(result).toEqual(expectedNpmrc); 243 | }); 244 | }); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "outDir": "lib/", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "include": ["src/**/*.ts", "test/**/*.ts"] 13 | } 14 | --------------------------------------------------------------------------------