├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── auto-queue.yml │ ├── build.yml │ ├── integ.yml │ ├── pull-request-lint.yml │ ├── release.yml │ ├── upgrade-cdklabs-projen-project-types-main.yml │ ├── upgrade-dev-deps-main.yml │ └── upgrade-main.yml ├── .gitignore ├── .npmignore ├── .projen ├── deps.json ├── files.json ├── jest-snapshot-resolver.js └── tasks.json ├── .projenrc.ts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── jsii-release-shim ├── publib ├── publib-ca ├── publib-golang ├── publib-maven ├── publib-npm ├── publib-nuget └── publib-pypi ├── package.json ├── scripts └── update-package-name.js ├── src ├── bin │ ├── publib-ca.ts │ ├── publib-golang.ts │ └── publib-maven.ts ├── codeartifact │ ├── codeartifact-cli.ts │ ├── codeartifact-repo.ts │ ├── display.ts │ ├── files.ts │ ├── shell.ts │ ├── staging │ │ ├── maven.ts │ │ ├── npm.ts │ │ ├── nuget.ts │ │ ├── parallel-shell.ts │ │ └── pip.ts │ └── usage-dir.ts ├── help │ ├── corking.ts │ ├── git.ts │ ├── os.ts │ ├── shell.ts │ └── sleep.ts ├── index.ts └── targets │ └── go.ts ├── test ├── __fixtures__ │ ├── combined │ │ ├── go.mod │ │ ├── module1 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ ├── module2 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ ├── source.go │ │ └── version │ ├── github-enterprise │ │ ├── go.mod │ │ ├── source.go │ │ └── version │ ├── major-version │ │ ├── go.mod │ │ ├── module1 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ ├── source.go │ │ └── version │ ├── major-versionv3 │ │ ├── go.mod │ │ ├── module1 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ ├── source.go │ │ └── version │ ├── multi-repo │ │ ├── module1 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ └── module2 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ ├── multi-version │ │ ├── module1 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ └── module2 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ ├── no-major-version-suffix │ │ ├── go.mod │ │ ├── source.go │ │ └── version │ ├── no-modules │ │ └── file.txt │ ├── no-version │ │ ├── go.mod │ │ └── source.go │ ├── not-github │ │ ├── go.mod │ │ ├── source.go │ │ └── version │ ├── sub-modules │ │ ├── module1 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ │ └── module2 │ │ │ ├── go.mod │ │ │ ├── source.go │ │ │ └── version │ └── top-level │ │ ├── go.mod │ │ ├── source.go │ │ └── version ├── publib-ca.integ.ts ├── targets │ ├── git-mocked.test.ts │ ├── git.test.ts │ └── go.test.ts └── with-temporary-directory.ts ├── tsconfig.dev.json ├── tsconfig.json ├── version.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import", 11 | "@stylistic" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.dev.json" 18 | }, 19 | "extends": [ 20 | "plugin:import/typescript" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.ts", 44 | "!projenrc/**/*.ts" 45 | ], 46 | "rules": { 47 | "@stylistic/indent": [ 48 | "error", 49 | 2 50 | ], 51 | "@stylistic/quotes": [ 52 | "error", 53 | "single", 54 | { 55 | "avoidEscape": true 56 | } 57 | ], 58 | "@stylistic/comma-dangle": [ 59 | "error", 60 | "always-multiline" 61 | ], 62 | "@stylistic/comma-spacing": [ 63 | "error", 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "@stylistic/no-multi-spaces": [ 70 | "error", 71 | { 72 | "ignoreEOLComments": false 73 | } 74 | ], 75 | "@stylistic/array-bracket-spacing": [ 76 | "error", 77 | "never" 78 | ], 79 | "@stylistic/array-bracket-newline": [ 80 | "error", 81 | "consistent" 82 | ], 83 | "@stylistic/object-curly-spacing": [ 84 | "error", 85 | "always" 86 | ], 87 | "@stylistic/object-curly-newline": [ 88 | "error", 89 | { 90 | "multiline": true, 91 | "consistent": true 92 | } 93 | ], 94 | "@stylistic/object-property-newline": [ 95 | "error", 96 | { 97 | "allowAllPropertiesOnSameLine": true 98 | } 99 | ], 100 | "@stylistic/keyword-spacing": [ 101 | "error" 102 | ], 103 | "@stylistic/brace-style": [ 104 | "error", 105 | "1tbs", 106 | { 107 | "allowSingleLine": true 108 | } 109 | ], 110 | "@stylistic/space-before-blocks": [ 111 | "error" 112 | ], 113 | "@stylistic/member-delimiter-style": [ 114 | "error" 115 | ], 116 | "@stylistic/semi": [ 117 | "error", 118 | "always" 119 | ], 120 | "@stylistic/max-len": [ 121 | "error", 122 | { 123 | "code": 150, 124 | "ignoreUrls": true, 125 | "ignoreStrings": true, 126 | "ignoreTemplateLiterals": true, 127 | "ignoreComments": true, 128 | "ignoreRegExpLiterals": true 129 | } 130 | ], 131 | "@stylistic/quote-props": [ 132 | "error", 133 | "consistent-as-needed" 134 | ], 135 | "@stylistic/key-spacing": [ 136 | "error" 137 | ], 138 | "@stylistic/no-multiple-empty-lines": [ 139 | "error" 140 | ], 141 | "@stylistic/no-trailing-spaces": [ 142 | "error" 143 | ], 144 | "curly": [ 145 | "error", 146 | "multi-line", 147 | "consistent" 148 | ], 149 | "@typescript-eslint/no-require-imports": "error", 150 | "import/no-extraneous-dependencies": [ 151 | "error", 152 | { 153 | "devDependencies": [ 154 | "**/test/**", 155 | "**/build-tools/**", 156 | ".projenrc.ts", 157 | "projenrc/**/*.ts" 158 | ], 159 | "optionalDependencies": false, 160 | "peerDependencies": true 161 | } 162 | ], 163 | "import/no-unresolved": [ 164 | "error" 165 | ], 166 | "import/order": [ 167 | "warn", 168 | { 169 | "groups": [ 170 | "builtin", 171 | "external" 172 | ], 173 | "alphabetize": { 174 | "order": "asc", 175 | "caseInsensitive": true 176 | } 177 | } 178 | ], 179 | "import/no-duplicates": [ 180 | "error" 181 | ], 182 | "no-shadow": [ 183 | "off" 184 | ], 185 | "@typescript-eslint/no-shadow": "error", 186 | "@typescript-eslint/no-floating-promises": "error", 187 | "no-return-await": [ 188 | "off" 189 | ], 190 | "@typescript-eslint/return-await": "error", 191 | "dot-notation": [ 192 | "error" 193 | ], 194 | "no-bitwise": [ 195 | "error" 196 | ], 197 | "@typescript-eslint/member-ordering": [ 198 | "error", 199 | { 200 | "default": [ 201 | "public-static-field", 202 | "public-static-method", 203 | "protected-static-field", 204 | "protected-static-method", 205 | "private-static-field", 206 | "private-static-method", 207 | "field", 208 | "constructor", 209 | "method" 210 | ] 211 | } 212 | ] 213 | }, 214 | "overrides": [ 215 | { 216 | "files": [ 217 | ".projenrc.ts" 218 | ], 219 | "rules": { 220 | "@typescript-eslint/no-require-imports": "off", 221 | "import/no-extraneous-dependencies": "off" 222 | } 223 | } 224 | ] 225 | } 226 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/auto-approve.yml linguist-generated 9 | /.github/workflows/auto-queue.yml linguist-generated 10 | /.github/workflows/build.yml linguist-generated 11 | /.github/workflows/integ.yml linguist-generated 12 | /.github/workflows/pull-request-lint.yml linguist-generated 13 | /.github/workflows/release.yml linguist-generated 14 | /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml linguist-generated 15 | /.github/workflows/upgrade-dev-deps-main.yml linguist-generated 16 | /.github/workflows/upgrade-main.yml linguist-generated 17 | /.gitignore linguist-generated 18 | /.npmignore linguist-generated 19 | /.projen/** linguist-generated 20 | /.projen/deps.json linguist-generated 21 | /.projen/files.json linguist-generated 22 | /.projen/tasks.json linguist-generated 23 | /LICENSE linguist-generated 24 | /package.json linguist-generated 25 | /tsconfig.dev.json linguist-generated 26 | /tsconfig.json linguist-generated 27 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'cdklabs-automation' || github.event.pull_request.user.login == 'dependabot[bot]') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-queue.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-queue 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - reopened 9 | - ready_for_review 10 | jobs: 11 | enableAutoQueue: 12 | name: "Set AutoQueue on PR #${{ github.event.number }}" 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | steps: 18 | - uses: peter-evans/enable-pull-request-automerge@v3 19 | with: 20 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 21 | pull-request-number: ${{ github.event.number }} 22 | merge-method: squash 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | merge_group: {} 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | outputs: 14 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 15 | env: 16 | CI: "true" 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.pull_request.head.ref }} 22 | repository: ${{ github.event.pull_request.head.repo.full_name }} 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: lts/* 27 | - name: Install dependencies 28 | run: yarn install --check-files 29 | - name: build 30 | run: npx projen build 31 | - name: Find mutations 32 | id: self_mutation 33 | run: |- 34 | git add . 35 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 36 | working-directory: ./ 37 | - name: Upload patch 38 | if: steps.self_mutation.outputs.self_mutation_happened 39 | uses: actions/upload-artifact@v4.4.0 40 | with: 41 | name: repo.patch 42 | path: repo.patch 43 | overwrite: true 44 | - name: Fail build on mutation 45 | if: steps.self_mutation.outputs.self_mutation_happened 46 | run: |- 47 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 48 | cat repo.patch 49 | exit 1 50 | self-mutation: 51 | needs: build 52 | runs-on: ubuntu-latest 53 | permissions: 54 | contents: write 55 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v4 59 | with: 60 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 61 | ref: ${{ github.event.pull_request.head.ref }} 62 | repository: ${{ github.event.pull_request.head.repo.full_name }} 63 | - name: Download patch 64 | uses: actions/download-artifact@v4 65 | with: 66 | name: repo.patch 67 | path: ${{ runner.temp }} 68 | - name: Apply patch 69 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 70 | - name: Set git identity 71 | run: |- 72 | git config user.name "github-actions" 73 | git config user.email "github-actions@github.com" 74 | - name: Push changes 75 | env: 76 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 77 | run: |- 78 | git add . 79 | git commit -s -m "chore: self mutation" 80 | git push origin HEAD:$PULL_REQUEST_REF 81 | -------------------------------------------------------------------------------- /.github/workflows/integ.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: integ 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | merge_group: {} 13 | jobs: 14 | determine_env: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | outputs: 19 | env_name: ${{ steps.output.outputs.env_name }} 20 | steps: 21 | - name: Print event output for debugging in case the condition is incorrect 22 | run: cat $GITHUB_EVENT_PATH 23 | - name: Start requiring approval 24 | run: echo IntegTestCredentialsRequireApproval > .envname 25 | - name: Run automatically if in a mergeGroup or PR created from this repo 26 | if: ${{ github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository }} 27 | run: echo IntegTestCredentials > .envname 28 | - name: Output the value 29 | id: output 30 | run: echo "env_name=$(cat .envname)" >> "$GITHUB_OUTPUT" 31 | integ: 32 | needs: determine_env 33 | runs-on: ubuntu-latest 34 | permissions: 35 | contents: read 36 | id-token: write 37 | environment: ${{needs.determine_env.outputs.env_name}} 38 | steps: 39 | - name: Federate into AWS 40 | uses: aws-actions/configure-aws-credentials@v4 41 | with: 42 | aws-region: us-east-1 43 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 44 | role-session-name: publib-integ-test 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | with: 48 | ref: ${{ github.event.pull_request.head.sha }} 49 | repository: ${{ github.event.pull_request.head.repo.full_name }} 50 | - name: Setup Node.js 51 | uses: actions/setup-node@v4 52 | with: 53 | cache: yarn 54 | node-version: "20" 55 | - name: Yarn install 56 | run: yarn install --frozen-lockfile 57 | - name: Run integration tests 58 | run: yarn integ 59 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | runs-on: ubuntu-latest 67 | permissions: 68 | contents: write 69 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 70 | steps: 71 | - uses: actions/setup-node@v4 72 | with: 73 | node-version: lts/* 74 | - name: Download build artifacts 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: build-artifact 78 | path: dist 79 | - name: Restore build artifact permissions 80 | run: cd dist && setfacl --restore=permissions-backup.acl 81 | continue-on-error: true 82 | - name: Release 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_SHA 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 86 | release_npm: 87 | name: Publish to npm 88 | needs: release 89 | runs-on: ubuntu-latest 90 | permissions: 91 | id-token: write 92 | contents: read 93 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 94 | steps: 95 | - uses: actions/setup-node@v4 96 | with: 97 | node-version: lts/* 98 | - name: Download build artifacts 99 | uses: actions/download-artifact@v4 100 | with: 101 | name: build-artifact 102 | path: dist 103 | - name: Restore build artifact permissions 104 | run: cd dist && setfacl --restore=permissions-backup.acl 105 | continue-on-error: true 106 | - name: Release 107 | env: 108 | NPM_DIST_TAG: latest 109 | NPM_REGISTRY: registry.npmjs.org 110 | NPM_CONFIG_PROVENANCE: "true" 111 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 112 | run: npx -p publib@latest publib-npm 113 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-cdklabs-projen-project-types-main 4 | on: 5 | workflow_dispatch: {} 6 | jobs: 7 | upgrade: 8 | name: Upgrade 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | outputs: 13 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | ref: main 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade-cdklabs-projen-project-types 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4.4.0 34 | with: 35 | name: repo.patch 36 | path: repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | ref: main 50 | - name: Download patch 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: repo.patch 54 | path: ${{ runner.temp }} 55 | - name: Apply patch 56 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 57 | - name: Set git identity 58 | run: |- 59 | git config user.name "github-actions" 60 | git config user.email "github-actions@github.com" 61 | - name: Create Pull Request 62 | id: create-pr 63 | uses: peter-evans/create-pull-request@v6 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | commit-message: |- 67 | chore(deps): upgrade cdklabs-projen-project-types 68 | 69 | Upgrades project dependencies. See details in [workflow run]. 70 | 71 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 72 | 73 | ------ 74 | 75 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 76 | branch: github-actions/upgrade-cdklabs-projen-project-types-main 77 | title: "chore(deps): upgrade cdklabs-projen-project-types" 78 | labels: auto-approve 79 | body: |- 80 | Upgrades project dependencies. See details in [workflow run]. 81 | 82 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 83 | 84 | ------ 85 | 86 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 87 | author: github-actions 88 | committer: github-actions 89 | signoff: true 90 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-dev-deps-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-dev-deps-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 18 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade-dev-deps 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dev dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 80 | branch: github-actions/upgrade-dev-deps-main 81 | title: "chore(deps): upgrade dev dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 18 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | fix(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "fix(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.github/workflows/release.yml 41 | !/.github/pull_request_template.md 42 | !/test/ 43 | !/tsconfig.json 44 | !/tsconfig.dev.json 45 | !/src/ 46 | /lib 47 | /dist/ 48 | !/.eslintrc.json 49 | !/.github/workflows/auto-queue.yml 50 | !/.github/workflows/upgrade-cdklabs-projen-project-types-main.yml 51 | !/.github/workflows/upgrade-main.yml 52 | !/.github/workflows/upgrade-dev-deps-main.yml 53 | !/.github/workflows/integ.yml 54 | !/.projenrc.ts 55 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /test/ 10 | /tsconfig.dev.json 11 | /src/ 12 | !/lib/ 13 | !/lib/**/*.js 14 | !/lib/**/*.d.ts 15 | dist 16 | /tsconfig.json 17 | /.github/ 18 | /.vscode/ 19 | /.idea/ 20 | /.projenrc.js 21 | tsconfig.tsbuildinfo 22 | /.eslintrc.json 23 | /.gitattributes 24 | /.projenrc.ts 25 | /projenrc 26 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-sdk/client-sts", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@stylistic/eslint-plugin", 9 | "version": "^2", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@types/glob", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@types/jest", 18 | "type": "build" 19 | }, 20 | { 21 | "name": "@types/node", 22 | "version": "^18.17.0", 23 | "type": "build" 24 | }, 25 | { 26 | "name": "@types/yargs", 27 | "version": "^17", 28 | "type": "build" 29 | }, 30 | { 31 | "name": "@typescript-eslint/eslint-plugin", 32 | "version": "^8", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "@typescript-eslint/parser", 37 | "version": "^8", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "cdklabs-projen-project-types", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "commit-and-tag-version", 46 | "version": "^12", 47 | "type": "build" 48 | }, 49 | { 50 | "name": "constructs", 51 | "version": "^10.0.0", 52 | "type": "build" 53 | }, 54 | { 55 | "name": "eslint-import-resolver-typescript", 56 | "type": "build" 57 | }, 58 | { 59 | "name": "eslint-plugin-import", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "eslint", 64 | "version": "^9", 65 | "type": "build" 66 | }, 67 | { 68 | "name": "jest", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "jest-junit", 73 | "version": "^16", 74 | "type": "build" 75 | }, 76 | { 77 | "name": "projen", 78 | "type": "build" 79 | }, 80 | { 81 | "name": "ts-jest", 82 | "type": "build" 83 | }, 84 | { 85 | "name": "ts-node", 86 | "type": "build" 87 | }, 88 | { 89 | "name": "typescript", 90 | "type": "build" 91 | }, 92 | { 93 | "name": "@aws-sdk/client-codeartifact", 94 | "type": "runtime" 95 | }, 96 | { 97 | "name": "@aws-sdk/credential-providers", 98 | "type": "runtime" 99 | }, 100 | { 101 | "name": "@aws-sdk/types", 102 | "type": "runtime" 103 | }, 104 | { 105 | "name": "@types/fs-extra", 106 | "version": "^8.0.0", 107 | "type": "runtime" 108 | }, 109 | { 110 | "name": "fs-extra", 111 | "version": "^8.0.0", 112 | "type": "runtime" 113 | }, 114 | { 115 | "name": "glob", 116 | "version": "10.0.0", 117 | "type": "runtime" 118 | }, 119 | { 120 | "name": "p-queue", 121 | "version": "6", 122 | "type": "runtime" 123 | }, 124 | { 125 | "name": "shlex", 126 | "type": "runtime" 127 | }, 128 | { 129 | "name": "yargs", 130 | "version": "^17", 131 | "type": "runtime" 132 | }, 133 | { 134 | "name": "zx", 135 | "type": "runtime" 136 | } 137 | ], 138 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 139 | } 140 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/auto-queue.yml", 8 | ".github/workflows/build.yml", 9 | ".github/workflows/integ.yml", 10 | ".github/workflows/pull-request-lint.yml", 11 | ".github/workflows/release.yml", 12 | ".github/workflows/upgrade-cdklabs-projen-project-types-main.yml", 13 | ".github/workflows/upgrade-dev-deps-main.yml", 14 | ".github/workflows/upgrade-main.yml", 15 | ".gitignore", 16 | ".npmignore", 17 | ".projen/deps.json", 18 | ".projen/files.json", 19 | ".projen/tasks.json", 20 | "LICENSE", 21 | "tsconfig.dev.json", 22 | "tsconfig.json" 23 | ], 24 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 25 | } 26 | -------------------------------------------------------------------------------- /.projen/jest-snapshot-resolver.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const libtest = "lib/__tests__"; 3 | const srctest= "src/__tests__"; 4 | module.exports = { 5 | resolveSnapshotPath: (test, ext) => { 6 | const fullpath = test.replace(libtest, srctest); 7 | return path.join(path.dirname(fullpath), '__snapshots__', path.basename(fullpath, '.js') + '.ts' + ext); 8 | }, 9 | resolveTestPath: (snap, ext) => { 10 | const filename = path.basename(snap, '.ts' + ext) + '.js'; 11 | const dir = path.dirname(path.dirname(snap)).replace(srctest, libtest); 12 | return path.join(dir, filename); 13 | }, 14 | testPathForConsistencyCheck: "some/__tests__/example.test.js" 15 | }; -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 37 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 38 | }, 39 | "steps": [ 40 | { 41 | "builtin": "release/bump-version" 42 | } 43 | ], 44 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 45 | }, 46 | "clobber": { 47 | "name": "clobber", 48 | "description": "hard resets to HEAD of origin and cleans the local repo", 49 | "env": { 50 | "BRANCH": "$(git branch --show-current)" 51 | }, 52 | "steps": [ 53 | { 54 | "exec": "git checkout -b scratch", 55 | "name": "save current HEAD in \"scratch\" branch" 56 | }, 57 | { 58 | "exec": "git checkout $BRANCH" 59 | }, 60 | { 61 | "exec": "git fetch origin", 62 | "name": "fetch latest changes from origin" 63 | }, 64 | { 65 | "exec": "git reset --hard origin/$BRANCH", 66 | "name": "hard reset to origin commit" 67 | }, 68 | { 69 | "exec": "git clean -fdx", 70 | "name": "clean all untracked files" 71 | }, 72 | { 73 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 74 | } 75 | ], 76 | "condition": "git diff --exit-code > /dev/null" 77 | }, 78 | "compile": { 79 | "name": "compile", 80 | "description": "Only compile", 81 | "steps": [ 82 | { 83 | "exec": "tsc --build" 84 | } 85 | ] 86 | }, 87 | "default": { 88 | "name": "default", 89 | "description": "Synthesize project files", 90 | "steps": [ 91 | { 92 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 93 | } 94 | ] 95 | }, 96 | "eject": { 97 | "name": "eject", 98 | "description": "Remove projen from the project", 99 | "env": { 100 | "PROJEN_EJECTING": "true" 101 | }, 102 | "steps": [ 103 | { 104 | "spawn": "default" 105 | } 106 | ] 107 | }, 108 | "eslint": { 109 | "name": "eslint", 110 | "description": "Runs eslint against the codebase", 111 | "env": { 112 | "ESLINT_USE_FLAT_CONFIG": "false" 113 | }, 114 | "steps": [ 115 | { 116 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 117 | "receiveArgs": true 118 | } 119 | ] 120 | }, 121 | "install": { 122 | "name": "install", 123 | "description": "Install project dependencies and update lockfile (non-frozen)", 124 | "steps": [ 125 | { 126 | "exec": "yarn install --check-files" 127 | } 128 | ] 129 | }, 130 | "install:ci": { 131 | "name": "install:ci", 132 | "description": "Install project dependencies using frozen lockfile", 133 | "steps": [ 134 | { 135 | "exec": "yarn install --check-files --frozen-lockfile" 136 | } 137 | ] 138 | }, 139 | "integ": { 140 | "name": "integ", 141 | "steps": [ 142 | { 143 | "exec": "jest --testMatch \"/test/**/*.integ.ts\"" 144 | } 145 | ] 146 | }, 147 | "package": { 148 | "name": "package", 149 | "description": "Creates the distribution package", 150 | "steps": [ 151 | { 152 | "exec": "mkdir -p dist/js" 153 | }, 154 | { 155 | "exec": "npm pack --pack-destination dist/js" 156 | }, 157 | { 158 | "spawn": "package-legacy" 159 | } 160 | ] 161 | }, 162 | "package-legacy": { 163 | "name": "package-legacy", 164 | "steps": [ 165 | { 166 | "exec": "cp package.json package.json.bak" 167 | }, 168 | { 169 | "exec": "node ./scripts/update-package-name.js jsii-release" 170 | }, 171 | { 172 | "exec": "npm pack" 173 | }, 174 | { 175 | "exec": "mv ./jsii-release*.tgz dist/js" 176 | }, 177 | { 178 | "exec": "cp package.json.bak package.json" 179 | }, 180 | { 181 | "exec": "rm package.json.bak" 182 | } 183 | ] 184 | }, 185 | "post-compile": { 186 | "name": "post-compile", 187 | "description": "Runs after successful compilation" 188 | }, 189 | "post-upgrade": { 190 | "name": "post-upgrade", 191 | "description": "Runs after upgrading dependencies" 192 | }, 193 | "pre-compile": { 194 | "name": "pre-compile", 195 | "description": "Prepare the project for compilation" 196 | }, 197 | "release": { 198 | "name": "release", 199 | "description": "Prepare a release from \"main\" branch", 200 | "env": { 201 | "RELEASE": "true" 202 | }, 203 | "steps": [ 204 | { 205 | "exec": "rm -fr dist" 206 | }, 207 | { 208 | "spawn": "bump" 209 | }, 210 | { 211 | "spawn": "build" 212 | }, 213 | { 214 | "spawn": "unbump" 215 | }, 216 | { 217 | "exec": "git diff --ignore-space-at-eol --exit-code" 218 | } 219 | ] 220 | }, 221 | "test": { 222 | "name": "test", 223 | "description": "Run tests", 224 | "steps": [ 225 | { 226 | "exec": "jest --passWithNoTests --updateSnapshot", 227 | "receiveArgs": true 228 | }, 229 | { 230 | "spawn": "eslint" 231 | } 232 | ] 233 | }, 234 | "test:watch": { 235 | "name": "test:watch", 236 | "description": "Run jest in watch mode", 237 | "steps": [ 238 | { 239 | "exec": "jest --watch" 240 | } 241 | ] 242 | }, 243 | "unbump": { 244 | "name": "unbump", 245 | "description": "Restores version to 0.0.0", 246 | "env": { 247 | "OUTFILE": "package.json", 248 | "CHANGELOG": "dist/changelog.md", 249 | "BUMPFILE": "dist/version.txt", 250 | "RELEASETAG": "dist/releasetag.txt", 251 | "RELEASE_TAG_PREFIX": "", 252 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 253 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 254 | }, 255 | "steps": [ 256 | { 257 | "builtin": "release/reset-version" 258 | } 259 | ] 260 | }, 261 | "upgrade": { 262 | "name": "upgrade", 263 | "description": "upgrade dependencies", 264 | "env": { 265 | "CI": "0" 266 | }, 267 | "steps": [ 268 | { 269 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=prod --filter=@aws-sdk/client-codeartifact,@aws-sdk/credential-providers,@aws-sdk/types,shlex,zx" 270 | }, 271 | { 272 | "exec": "yarn install --check-files" 273 | }, 274 | { 275 | "exec": "yarn upgrade @aws-sdk/client-codeartifact @aws-sdk/credential-providers @aws-sdk/types @types/fs-extra fs-extra glob p-queue shlex yargs zx" 276 | }, 277 | { 278 | "exec": "npx projen" 279 | }, 280 | { 281 | "spawn": "post-upgrade" 282 | } 283 | ] 284 | }, 285 | "upgrade-cdklabs-projen-project-types": { 286 | "name": "upgrade-cdklabs-projen-project-types", 287 | "description": "upgrade cdklabs-projen-project-types", 288 | "env": { 289 | "CI": "0" 290 | }, 291 | "steps": [ 292 | { 293 | "exec": "npx npm-check-updates@16 --upgrade --target=latest --peer --no-deprecated --dep=dev,peer,prod,optional --filter=cdklabs-projen-project-types,projen" 294 | }, 295 | { 296 | "exec": "yarn install --check-files" 297 | }, 298 | { 299 | "exec": "yarn upgrade cdklabs-projen-project-types projen" 300 | }, 301 | { 302 | "exec": "npx projen" 303 | }, 304 | { 305 | "spawn": "post-upgrade" 306 | } 307 | ] 308 | }, 309 | "upgrade-dev-deps": { 310 | "name": "upgrade-dev-deps", 311 | "description": "upgrade dev dependencies", 312 | "env": { 313 | "CI": "0" 314 | }, 315 | "steps": [ 316 | { 317 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev --filter=@aws-sdk/client-sts,@types/glob,@types/jest,eslint-import-resolver-typescript,eslint-plugin-import,jest,ts-jest,ts-node,typescript" 318 | }, 319 | { 320 | "exec": "yarn install --check-files" 321 | }, 322 | { 323 | "exec": "yarn upgrade @aws-sdk/client-sts @stylistic/eslint-plugin @types/glob @types/jest @types/node @types/yargs @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version constructs eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit ts-jest ts-node typescript" 324 | }, 325 | { 326 | "exec": "npx projen" 327 | }, 328 | { 329 | "spawn": "post-upgrade" 330 | } 331 | ] 332 | }, 333 | "watch": { 334 | "name": "watch", 335 | "description": "Watch & compile in the background", 336 | "steps": [ 337 | { 338 | "exec": "tsc --build -w" 339 | } 340 | ] 341 | } 342 | }, 343 | "env": { 344 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 345 | }, 346 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 347 | } 348 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import * as cdklabs from 'cdklabs-projen-project-types'; 3 | import { github } from 'projen'; 4 | 5 | const project = new cdklabs.CdklabsTypeScriptProject({ 6 | private: false, 7 | projenrcTs: true, 8 | defaultReleaseBranch: 'main', 9 | name: 'publib', 10 | description: 'Release jsii modules to multiple package managers', 11 | releaseToNpm: true, 12 | repository: 'https://github.com/cdklabs/publib.git', 13 | authorUrl: 'https://aws.amazon.com', 14 | homepage: 'https://github.com/cdklabs/publib', 15 | devDeps: [ 16 | 'ts-node', 17 | '@aws-sdk/client-sts', 18 | '@types/glob', 19 | '@types/node@^18.17.0', 20 | '@types/yargs@^17', 21 | 'cdklabs-projen-project-types', 22 | ], 23 | autoApproveUpgrades: true, 24 | deps: [ 25 | '@aws-sdk/client-codeartifact', 26 | '@aws-sdk/credential-providers', 27 | '@aws-sdk/types', 28 | 'glob@10.0.0', // Can't use a newer version of glob, it adds a CLI that depends on 'jackspeak' which has crazy dependencies 29 | 'yargs@^17', 30 | 'zx', 31 | 'p-queue@6', // Last non-ESM version 32 | ], 33 | enablePRAutoMerge: true, 34 | setNodeEngineVersion: false, 35 | tsconfig: { 36 | compilerOptions: { 37 | // Conflict between `commit-and-tag-version@12` and modern TypeScript. 38 | // `commit-and-tag-version` depends on an old version of lru-cache, which TypeScript 39 | // doesn't agree with the typings of. Skip checking those files to make the build succeed. 40 | skipLibCheck: true, 41 | }, 42 | }, 43 | }); 44 | 45 | // Necessary to work around a typing issue in transitive dependency lru-cache@10 now that we've moved to a modern TS 46 | // project.package.addPackageResolutions('lru-cache@^11'); 47 | 48 | // we can't use 9.x because it doesn't work with node 10. 49 | const fsExtraVersion = '^8.0.0'; 50 | 51 | project.addDeps('shlex', `fs-extra@${fsExtraVersion}`, `@types/fs-extra@${fsExtraVersion}`); 52 | 53 | const legacy = project.addTask('package-legacy'); 54 | legacy.exec('cp package.json package.json.bak'); 55 | legacy.exec('node ./scripts/update-package-name.js jsii-release'); 56 | legacy.exec('npm pack'); 57 | legacy.exec('mv ./jsii-release*.tgz dist/js'); 58 | legacy.exec('cp package.json.bak package.json'); 59 | legacy.exec('rm package.json.bak'); 60 | 61 | project.packageTask.spawn(legacy); 62 | 63 | // map all "jsii-release-*" to "jsii-release-shim" as executables 64 | for (const f of readdirSync('./bin').filter(file => file.startsWith('publib'))) { 65 | const shim = ['jsii-release', f.split('-')[1]].filter(x => x).join('-'); 66 | project.addBins({ [shim]: './bin/jsii-release-shim' }); 67 | } 68 | 69 | const integ = project.addTask('integ'); 70 | // This replaces the 'testMatch' in package.json with a different glob 71 | integ.exec('jest --testMatch "/test/**/*.integ.ts"'); 72 | 73 | ////////////////////////////////////////////////////////////////////// 74 | 75 | const test = github.GitHub.of(project)?.addWorkflow('integ'); 76 | test?.on({ 77 | pullRequestTarget: { 78 | types: [ 79 | 'labeled', 80 | 'opened', 81 | 'synchronize', 82 | 'reopened', 83 | 'ready_for_review', 84 | ], 85 | }, 86 | mergeGroup: {}, 87 | }); 88 | 89 | // Select `IntegTestCredentials` or `IntegTestCredentialsRequireApproval` depending on whether this is 90 | // a PR from a fork or not (assumption: writers can be trusted, forkers cannot). 91 | // 92 | // Because we have an 'if/else' condition that is quite annoying to encode with outputs, have a mutable variable by 93 | // means of a file on disk, export it as an output afterwards. 94 | test?.addJob('determine_env', { 95 | permissions: { 96 | contents: github.workflows.JobPermission.READ, 97 | }, 98 | runsOn: ['ubuntu-latest'], 99 | steps: [ 100 | { 101 | name: 'Print event output for debugging in case the condition is incorrect', 102 | run: 'cat $GITHUB_EVENT_PATH', 103 | }, 104 | { 105 | name: 'Start requiring approval', 106 | run: 'echo IntegTestCredentialsRequireApproval > .envname', 107 | }, 108 | { 109 | name: 'Run automatically if in a mergeGroup or PR created from this repo', 110 | // In a mergeGroup event, or a non-forked request, run without confirmation 111 | if: "${{ github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository }}", 112 | run: 'echo IntegTestCredentials > .envname', 113 | }, 114 | { 115 | id: 'output', 116 | name: 'Output the value', 117 | run: 'echo "env_name=$(cat .envname)" >> "$GITHUB_OUTPUT"', 118 | }, 119 | ], 120 | outputs: { 121 | env_name: { stepId: 'output', outputName: 'env_name' }, 122 | }, 123 | }); 124 | 125 | // Job name matches a branch protection rule check configured elsewhere 126 | test?.addJob('integ', { 127 | permissions: { 128 | contents: github.workflows.JobPermission.READ, 129 | idToken: github.workflows.JobPermission.WRITE, 130 | }, 131 | runsOn: ['ubuntu-latest'], 132 | needs: ['determine_env'], 133 | environment: '${{needs.determine_env.outputs.env_name}}', 134 | steps: [ 135 | { 136 | name: 'Federate into AWS', 137 | uses: 'aws-actions/configure-aws-credentials@v4', 138 | with: { 139 | 'aws-region': 'us-east-1', 140 | 'role-to-assume': '${{ secrets.AWS_ROLE_TO_ASSUME }}', 141 | 'role-session-name': 'publib-integ-test', 142 | }, 143 | }, 144 | { 145 | name: 'Checkout', 146 | uses: 'actions/checkout@v4', 147 | with: { 148 | ref: '${{ github.event.pull_request.head.sha }}', 149 | // Need this because we are running on pull_request_target 150 | repository: '${{ github.event.pull_request.head.repo.full_name }}', 151 | }, 152 | }, 153 | { 154 | name: 'Setup Node.js', 155 | uses: 'actions/setup-node@v4', 156 | with: { 157 | 'cache': 'yarn', 158 | 'node-version': '20', 159 | }, 160 | }, 161 | { 162 | name: 'Yarn install', 163 | run: 'yarn install --frozen-lockfile', 164 | }, 165 | { 166 | name: 'Run integration tests', 167 | run: 'yarn integ', 168 | }, 169 | ], 170 | }); 171 | 172 | project.synth(); 173 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.33](https://github.com/aws/jsii-release/compare/v0.2.32...v0.2.33) (2021-05-11) 6 | 7 | ### [0.2.32](https://github.com/aws/jsii-release/compare/v0.2.31...v0.2.32) (2021-05-09) 8 | 9 | ### [0.2.31](https://github.com/aws/jsii-release/compare/v0.2.30...v0.2.31) (2021-05-09) 10 | 11 | ### [0.2.30](https://github.com/aws/jsii-release/compare/v0.2.29...v0.2.30) (2021-04-16) 12 | 13 | ### [0.2.29](https://github.com/aws/jsii-release/compare/v0.2.28...v0.2.29) (2021-04-16) 14 | 15 | ### [0.2.28](https://github.com/aws/jsii-release/compare/v0.2.27...v0.2.28) (2021-04-15) 16 | 17 | ### [0.2.27](https://github.com/aws/jsii-release/compare/v0.2.26...v0.2.27) (2021-04-15) 18 | 19 | ### [0.2.26](https://github.com/aws/jsii-release/compare/v0.2.25...v0.2.26) (2021-04-15) 20 | 21 | ### [0.2.25](https://github.com/aws/jsii-release/compare/v0.2.24...v0.2.25) (2021-04-15) 22 | 23 | ### [0.2.24](https://github.com/aws/jsii-release/compare/v0.2.23...v0.2.24) (2021-04-13) 24 | 25 | ### [0.2.23](https://github.com/aws/jsii-release/compare/v0.2.22...v0.2.23) (2021-04-13) 26 | 27 | ### [0.2.22](https://github.com/aws/jsii-release/compare/v0.2.21...v0.2.22) (2021-04-13) 28 | 29 | ### [0.2.21](https://github.com/aws/jsii-release/compare/v0.2.20...v0.2.21) (2021-04-13) 30 | 31 | ### [0.2.20](https://github.com/aws/jsii-release/compare/v0.2.19...v0.2.20) (2021-04-13) 32 | 33 | ### [0.2.19](https://github.com/aws/jsii-release/compare/v0.2.17...v0.2.19) (2021-04-13) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * **go:** Release is not idempotent to existing tags ([#72](https://github.com/aws/jsii-release/issues/72)) ([74e8f0f](https://github.com/aws/jsii-release/commit/74e8f0fac1d7d27109fc422f6723958ec34d8e89)) 39 | 40 | ### [0.2.18](https://github.com/aws/jsii-release/compare/v0.2.17...v0.2.18) (2021-02-12) 41 | 42 | ### [0.2.17](https://github.com/aws/jsii-release/compare/v0.2.16...v0.2.17) (2021-02-11) 43 | 44 | 45 | ### Features 46 | 47 | * **golang:** Support Multi-version and root level modules ([#34](https://github.com/aws/jsii-release/issues/34)) ([4ef7d59](https://github.com/aws/jsii-release/commit/4ef7d599ff2db405a388248218814edda256984e)) 48 | 49 | ### 0.2.16 (2021-02-04) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * **golang:** Release fails if no changes ([#32](https://github.com/aws/jsii-release/issues/32)) ([9f367d4](https://github.com/aws/jsii-release/commit/9f367d41659c6d7b5324b9d6f5f4d3eea796213f)) 55 | 56 | ### 0.2.15 (2021-02-04) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **maven:** increase timeout to 30min ([de11c0a](https://github.com/aws/jsii-release/commit/de11c0a84923ce4fc211a62d69f811841d199c05)) 62 | 63 | ### 0.2.14 (2021-02-04) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * **maven:** maven central timeouts ([#30](https://github.com/aws/jsii-release/issues/30)) ([cd4a241](https://github.com/aws/jsii-release/commit/cd4a24179fdd45d9c503e8ff2b7294fc09dace46)), closes [#29](https://github.com/aws/jsii-release/issues/29) 69 | 70 | ### 0.2.13 (2021-02-02) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **golang:** Repository are incorrectly tagged ([#27](https://github.com/aws/jsii-release/issues/27)) ([786e503](https://github.com/aws/jsii-release/commit/786e5034a193cb5cbf9711af2405b1c76369e2a8)) 76 | 77 | ### 0.2.12 (2021-02-01) 78 | 79 | ### 0.2.11 (2021-01-31) 80 | 81 | 82 | ### Features 83 | 84 | * golang release ([#23](https://github.com/aws/jsii-release/issues/23)) ([b36b8f9](https://github.com/aws/jsii-release/commit/b36b8f919d721c0ded2a87d5cad6e12bdf155c96)) 85 | 86 | ### 0.2.10 (2020-12-30) 87 | 88 | ### 0.2.9 (2020-12-23) 89 | 90 | 91 | ### Features 92 | 93 | * **NuGet:** Support NuGet GitHub Packages ([#18](https://github.com/aws/jsii-release/issues/18)) ([f3ed13c](https://github.com/aws/jsii-release/commit/f3ed13cb19ee12601cbe5dd008b7c23528a58a5d)) 94 | 95 | ### 0.2.8 (2020-12-13) 96 | 97 | ### 0.2.7 (2020-12-06) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * --pinentry-mode is only available on mac ([217b2a8](https://github.com/aws/jsii-release/commit/217b2a8c695aa5f0e33ae4998f2069adf4b0e7bf)) 103 | 104 | ### 0.2.6 (2020-12-03) 105 | 106 | ### 0.2.5 (2020-12-03) 107 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to work on this repository 2 | 3 | ## Testing Maven publishing 4 | 5 | - Create a Sonatype account at . You can use GitHub to sign in. 6 | - Generate a user token, note the username and password. 7 | - Generate an empty project and give it a jsii config. Example: 8 | 9 | ``` 10 | { 11 | "jsii": { 12 | "versionFormat": "full", 13 | "targets": { 14 | "java": { 15 | "package": "test.test", 16 | "maven": { 17 | "groupId": "test.test", 18 | "artifactId": "test" 19 | } 20 | } 21 | }, 22 | "outdir": "dist" 23 | }, 24 | } 25 | ``` 26 | 27 | Follow the instructions in the README for creating and exporting a GPG key. 28 | 29 | Then run the following command: 30 | 31 | ``` 32 | env MAVEN_GPG_PRIVATE_KEY_FILE=$PWD/mykey.priv MAVEN_GPG_PRIVATE_KEY_PASSPHRASE=mypassphrase MAVEN_STAGING_PROFILE_ID=com.sonatype.software MAVEN_SERVER_ID=central-ossrh MAVEN_USERNAME=**** MAVEN_PASSWORD=**** /path/to/publib/bin/publib-maven 33 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # publib 2 | 3 | > Previously known as `jsii-release` 4 | 5 | A unified toolchain for publishing libraries to popular package managers. 6 | 7 | Supports: 8 | 9 | * npm 10 | * PyPI 11 | * NuGet 12 | * Maven 13 | * Go (GitHub) 14 | 15 | ## Usage 16 | 17 | This is an npm module. You can install it using `yarn add publib` or 18 | `npm install publib`. In most cases it will be installed as a `devDependency` 19 | in your `package.json`. 20 | 21 | This tool expects to find a distribution directory (default name is `dist`) 22 | which contains "ready-to-publish" artifacts for each package manager. 23 | 24 | * `dist/js/*.tgz` - npm tarballs 25 | * `dist/python/*.whl` - Python wheels 26 | * `dist/nuget/*.nupkg` - Nuget packages 27 | * `dist/java/**` - Maven artifacts in local repository structure 28 | * `dist/go/**/go.mod` - Go modules. Each subdirectory should have its own go.mod file. 29 | 30 | Each publisher needs a set of environment variables with credentials as 31 | described below (`NPM_TOKEN`, `TWINE_PASSWORD` etc). 32 | 33 | Then: 34 | 35 | ```shell 36 | publib 37 | ``` 38 | 39 | You can customize the distribution directory through `publib DIR` (the 40 | default is `dist`) 41 | 42 | This command will discover all the artifacts based on the above structure and 43 | will publish them to their respective package manager. 44 | 45 | You can also execute individual publishers: 46 | 47 | * `publib-maven` 48 | * `publib-nuget` 49 | * `publib-npm` 50 | * `publib-pypi` 51 | * `publib-golang` 52 | 53 | ## npm 54 | 55 | Publishes all `*.tgz` files from `DIR` to [npmjs](npmjs.com), [GitHub Packages](https://github.com/features/packages) or [AWS CodeArtifact](https://aws.amazon.com/codeartifact/). 56 | 57 | If AWS CodeArtifact is used as npm registry, a temporary npm authorization token is created using AWS CLI. Therefore, it is necessary to provide the necessary [configuration settings](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html), e.g. by passing access key ID and secret access key to this script. 58 | 59 | **Usage:** 60 | 61 | ```shell 62 | npx publib-npm [DIR] 63 | ``` 64 | 65 | `DIR` is a directory with npm tarballs (*.tgz). Default is `dist/js`. 66 | 67 | **Options (environment variables):** 68 | 69 | | Option | Required | Description | 70 | | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 71 | | `NPM_TOKEN` | Optional | Registry authentication token (either [npm.js publishing token](https://docs.npmjs.com/creating-and-viewing-authentication-tokens) or a [GitHub personal access token](https://help.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-npm-for-use-with-github-packages#authenticating-to-github-packages)), not used for AWS CodeArtifact | 72 | | `NPM_REGISTRY` | Optional | The registry URL (defaults to "registry.npmjs.org"). Use "npm.pkg.github.com" to publish to GitHub Packages. Use repository endpoint for AWS CodeAtifact, e.g. "my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my_repo/". | 73 | | `NPM_DIST_TAG` | Optional | Registers the published package with the given [dist-tag](https://docs.npmjs.com/cli/dist-tag) (e.g. `next`, default is `latest`) | 74 | | `NPM_ACCESS_LEVEL` | Optional | Publishes the package with the given [access level](https://docs.npmjs.com/cli/v8/commands/npm-publish#access) (e.g. `public`, default is `restricted` for scoped packages and `public` for unscoped packages) | 75 | | `AWS_ACCESS_KEY_ID` | Optional | If AWS CodeArtifact is used as registry, an AWS access key can be spedified. | 76 | | `AWS_SECRET_ACCESS_KEY` | Optional | Secret access key that belongs to the AWS access key. | 77 | | `AWS_ROLE_TO_ASSUME` | Optional | If AWS CodeArtifact is used as registry, an AWS role ARN to assume before authorizing. | 78 | | `DISABLE_HTTPS` | Optional | Connect to the registry with HTTP instead of HTTPS (defaults to false). | 79 | 80 | ## Maven 81 | 82 | Publishes all Maven modules in the `DIR` to [Maven Central](https://search.maven.org/). 83 | 84 | > [!IMPORTANT] 85 | > Starting July 2025 you must switch over to the new Maven Central Publisher. Follow these steps: 86 | > 87 | > * Log in to with your existing username and password. 88 | > * Under your account, click **View Namespaces**, then click **Migrate Namespace** for your target namespaces. 89 | > * Generate a new username and password on the new publisher using the **Generate User Token** feature. 90 | > * Configure `MAVEN_SERVER_ID=central-ossrh`. 91 | > * Unset any `MAVEN_ENDPOINT`. 92 | > * Configure the new `MAVEN_USERNAME` and `MAVEN_PASSWORD`. 93 | 94 | If you are still on Nexus and you signed up at SonaType after February 2021, you 95 | need to use this URL: `https://s01.oss.sonatype.org` 96 | ([announcement](https://central.sonatype.org/news/20210223_new-users-on-s01/)). 97 | 98 | **Usage:** 99 | 100 | ```shell 101 | npx publib-maven [DIR] 102 | ``` 103 | 104 | `DIR` is a directory with a local maven layout. Default is `dist/java`. 105 | 106 | **Options (environment variables):** 107 | 108 | The server type is selected using the `MAVEN_SERVER_ID` variable. 109 | 110 | - `MAVEN_SERVER_ID=ossrh`; this is currently the default but will stop working in July 2025. Publish to the old OSSRH Nexus server. 111 | - `MAVEN_SERVER_ID=central-ossrh`; publish to the new Central Publishing platform using a service endpoint more-or-less compatible with the old OSSRH Nexus server. This is required to publish to Maven Central starting July 2025. 112 | - `MAVEN_SERVER_ID=`; publish to a custom Nexus server. 113 | 114 | 115 | | Server | Option | Required | Description | 116 | |----------------------| --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 117 | | (all) | `MAVEN_SERVER_ID` | Yes going forward | Either `ossrh` (default but deprecated), `central-ossrh`, or any other string for a custom Nexus server. | 118 | | (all) | `MAVEN_USERNAME` and `MAVEN_PASSWORD` | Yes | Username and password for maven repository. For Maven Central, you will need to [Create JIRA account](https://issues.sonatype.org/secure/Signup!default.jspa) and then request a [new project](https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134). Read the [OSSRH guide](https://central.sonatype.org/pages/ossrh-guide.html) for more details. | 119 | | (all) | `MAVEN_DRYRUN` | No | Set to "true" for a dry run | 120 | | (all) | `MAVEN_VERBOSE` | No | Make Maven print debug output if set to `true` | 121 | | `central-ossrh` | `MAVEN_GPG_PRIVATE_KEY[_FILE]` and `MAVEN_GPG_PRIVATE_KEY_PASSPHRASE` | Yes | GPG private key or file that includes it. This is used to sign your Maven packages. See instructions below | 122 | | `central-ossrh` | `MAVEN_ENDPOINT` | No | URL of Nexus repository. Defaults to `https://ossrh-staging-api.central.sonatype.com/`. | 123 | | `` | `MAVEN_REPOSITORY_URL` | No | Deployment repository when not deploying to Maven Central | 124 | | `ossrh` (deprecated) | `MAVEN_GPG_PRIVATE_KEY[_FILE]` and `MAVEN_GPG_PRIVATE_KEY_PASSPHRASE` | Yes | GPG private key or file that includes it. This is used to sign your Maven packages. See instructions below | 125 | | `ossrh` (deprecated) | `MAVEN_STAGING_PROFILE_ID` | Yes | Central Publisher (sonatype) staging profile ID, corresponding to namespace (e.g. `com.sonatype.software`). | 126 | | `ossrh` (deprecated) | `MAVEN_ENDPOINT` | No | URL of Nexus repository. Defaults to `https://central.sonatype.com`. | 127 | 128 | **How to create a GPG key** 129 | 130 | Install [GnuPG](https://gnupg.org/). 131 | 132 | Generate your key: 133 | 134 | ```console 135 | $ gpg --full-generate-key 136 | # select RSA only, 4096, passphrase 137 | ``` 138 | 139 | Your selected passphrase goes to `MAVEN_GPG_PRIVATE_KEY_PASSPHRASE`. 140 | 141 | Export and publish the public key: 142 | 143 | ```console 144 | gpg -a --export > public.pem 145 | ``` 146 | 147 | Go to and submit the public key. 148 | You can use `cat public.pem` and copy/paste it into the "Submit Key" dialog. 149 | 150 | Export the private key: 151 | 152 | ```console 153 | gpg -a --export-secret-keys > private.pem 154 | ``` 155 | 156 | Now, either set `MAVEN_GPG_PRIVATE_KEY_FILE` to point to `private.pem` or 157 | export the private key to a single line where newlines are encoded as `\n` 158 | and then assign it to `MAVEN_GPG_PRIVATE_KEY`: 159 | 160 | ```console 161 | echo $(cat -e private.pem) | sed 's/\$ /\\n/g' | sed 's/\$$//' 162 | ``` 163 | 164 | **Publish to GitHub Packages**\ 165 | An example GitHub Actions publish step: 166 | 167 | ```yaml 168 | - name: Publish package 169 | run: npx -p publib publib-maven 170 | env: 171 | MAVEN_SERVER_ID: github 172 | MAVEN_USERNAME: ${{ github.actor }} 173 | MAVEN_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 174 | MAVEN_REPOSITORY_URL: "https://maven.pkg.github.com/${{ github.repository }}" 175 | ``` 176 | 177 | ## NuGet 178 | 179 | Publishes all `*.nupkg` to the [NuGet Gallery](https://www.nuget.org/). 180 | 181 | **Usage:** 182 | 183 | ```shell 184 | npx publib-nuget [DIR] 185 | ``` 186 | 187 | `DIR` is a directory with Nuget packages (*.nupkg). Default is `dist/dotnet`. 188 | 189 | **Options (environment variables):** 190 | 191 | | Option | Required | Description | 192 | | --------------- | -------- | ------------------------------------------------------------------------------ | 193 | | `NUGET_API_KEY` | Required | [NuGet API Key](https://www.nuget.org/account/apikeys) with "Push" permissions | 194 | | `NUGET_SERVER` | Optional | NuGet Server URL (defaults to nuget.org) | 195 | 196 | **Publish to GitHub Packages**\ 197 | You can publish to GitHub Packages instead, with the following options: 198 | 199 | * Set `NUGET_SERVER` to `https://nuget.pkg.github.com/[org or user]`. 200 | * Set `NUGET_API_KEY` to a token with write packages permissions. 201 | * Make sure the repository url in the project file matches the org or user used for the server 202 | 203 | ## PyPI 204 | 205 | Publishes all `*.whl` files to [PyPI](https://pypi.org/). 206 | 207 | **Usage:** 208 | 209 | ```shell 210 | npx publib-pypi [DIR] 211 | ``` 212 | 213 | `DIR` is a directory with Python wheels (*.whl). Default is `dist/python`. 214 | 215 | **Options (environment variables):** 216 | 217 | | Option | Required | Description | 218 | | ---------------------- | -------- | -------------------------------------------------------------- | 219 | | `TWINE_USERNAME` | Required | PyPI username ([register](https://pypi.org/account/register/)) | 220 | | `TWINE_PASSWORD` | Required | PyPI password | 221 | | `TWINE_REPOSITORY_URL` | Optional | The registry URL (defaults to Twine default) | 222 | 223 | ## Golang 224 | 225 | Pushes a directory of golang modules to a GitHub repository. 226 | 227 | **Usage:** 228 | 229 | ```shell 230 | npx publib-golang [DIR] 231 | ``` 232 | 233 | `DIR` is a directory where the golang modules are located (default is `dist/go`). Modules can be located either in subdirectories, (e.g 'dist/go/my-module/go.mod') 234 | or in the root (e.g 'dist/go/go.mod'). 235 | 236 | If you specify the `VERSION` env variable, all modules will recieve that version, otherwise a `version` file is expected to exist in each module directory. 237 | Repository tags will be in the following format: 238 | 239 | * For a module located at the root: `v${module_version}` (e.g `v1.20.1`) 240 | * For modules located inside subdirectories: `/v${module_version}` (e.g `my-module/v3.3.1`) 241 | 242 | **Options (environment variables):** 243 | 244 | | Option | Required | Description | 245 | | ----------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 246 | | `GITHUB_TOKEN` | Required when not in SSH mode, see `GIT_USE_SSH` | [GitHub personal access token.](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | 247 | | `GIT_USE_SSH` | Optional | Set to a non-falsy value to use SSH with [deploy keys](https://docs.github.com/en/developers/overview/managing-deploy-keys#deploy-keys) or your private SSH key. Your system must ready to use the key as publib will not set it up. | 248 | | `GITHUB_USE_SSH` | Deprecated | Legacy alias for `GIT_USE_SSH`. | 249 | | `GH_ENTERPRISE_TOKEN` or
`GITHUB_ENTERPRISE_TOKEN` | Optional | [Custom Authentication token for API requests to GitHub Enterprise](https://cli.github.com/manual/gh_help_environment). | 250 | | `GH_HOST` | Optional | Force use of a different [Hostname for GitHub Enterprise](https://cli.github.com/manual/gh_help_environment). | 251 | | `GITHUB_API_URL` | Optional | If present, used to detect the GitHub instance to target. This is specified by default in [GitHub Actions workflow](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) and should not be set manually. | 252 | | `VERSION` | Optional | Module version. Defaults to the value in the 'version' file of the module directory. Fails if it doesn't exist. | 253 | | `GIT_BRANCH` | Optional | Branch to push to. Defaults to 'main'. | 254 | | `GIT_USER_NAME` | Optional | Username to perform the commit with. Defaults to the git user.name config in the current directory. Fails if it doesn't exist. | 255 | | `GIT_USER_EMAIL` | Optional | Email to perform the commit with. Defaults to the git user.email config in the current directory. Fails if it doesn't exist. | 256 | | `GIT_COMMIT_MESSAGE` | Optional | The commit message. Defaults to 'chore(release): $VERSION'. | 257 | | `DRYRUN` | Optional | Set to "true" for a dry run. | 258 | 259 | ## Publish to CodeArtifact for testing 260 | 261 | This package contains the `publib-ca` CLI tool which is intended to use to publish 262 | packages to CodeArtifact for testing (in a pipeline, before publishing to the 263 | actual public package managers). 264 | 265 | Use the following commands: 266 | 267 | `publib-ca create [--no-gc] [--no-login]` creates a new CodeArtifact repository 268 | with a random name, with upstreams configured for all supported public package 269 | managers. By default this command runs the `gc` and `login` subcommands 270 | automatically. 271 | 272 | `publib-ca login --repo NAME [--cmd COMMAND]` logs in to a CodeArtifact repository and prepares some files that configure package managers for use with this CodeArtifact repository. If `--cmd` is given, the command is run in an environment 273 | where all supported package managers have been configured for the given repository. 274 | Otherwise, activate these settings in the current bash shell by running 275 | `source ~/.publib-ca/usage/activate.bash`. This will set some 276 | environment variables and copy some files into the current directory. (Note: the 277 | CodeArtifact repository used here does not have to be created using `publib-ca create`. It 278 | is fine if it already existed beforehand). 279 | 280 | `publib-ca gc` collects old repositories created using `publib-ca create`. 281 | 282 | `publib-ca publish [--repo NAME] DIRECTORY` publishes all packages in the given 283 | directory to the given repository. If `--repo` is not given, the most recently 284 | logged-into repository is used, if the login session is still valid. 285 | 286 | ## Roadmap 287 | 288 | * [X] GitHub Support: Maven 289 | * [X] GitHub Support: NuGet 290 | * [ ] CodeArtifact Support: Maven 291 | * [ ] CodeArtifact Support: NuGet 292 | * [ ] CodeArtifact Support: Python 293 | 294 | ## License 295 | 296 | Released under the [Apache 2.0](./LICENSE) license. 297 | -------------------------------------------------------------------------------- /bin/jsii-release-shim: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | scriptdir=$(cd $(dirname $0) && pwd) 3 | jsii_command=$(basename $0) 4 | 5 | publib_command="$(echo ${jsii_command} | sed -e "s/jsii-release/publib/")" 6 | 7 | echo "-----------------------------------------------------------------" 8 | echo "WARNING: 'jsii-release' is now released under the name 'publib'" 9 | echo "Please install it and execute '${publib_command}' instead" 10 | echo "-----------------------------------------------------------------" 11 | 12 | actual="${scriptdir}/${publib_command}" 13 | 14 | if [ ! -e "${actual}" ]; then 15 | echo "${publib_command} not found" 16 | exit 1 17 | fi 18 | 19 | exec "${actual}" "$@" 20 | -------------------------------------------------------------------------------- /bin/publib: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # usage: publib [DIR] 3 | # 4 | # DIR is the root directory of the distribution where we will look for `js`, 5 | # `java`, `python` and `dotnet` subdirectories (default is "dist") 6 | # 7 | set -euo pipefail 8 | scriptdir="$(cd $(dirname $0) && pwd)" 9 | 10 | dist="${1:-dist}" 11 | 12 | if [ ! -d "${dist}" ]; then 13 | echo "ERROR: unable to find dist directory under ${dist}" 14 | exit 1 15 | fi 16 | 17 | function release() { 18 | local type=$1 19 | local subdir=$2 20 | 21 | local dist_subdir="${dist}/${subdir}" 22 | if [ -d "${dist_subdir}" ]; then 23 | echo "found ${type} artifacts under ${dist_subdir}" 24 | ${scriptdir}/publib-${type} "${dist_subdir}" 25 | else 26 | echo "${dist_subdir}: no ${type} artifacts" 27 | fi 28 | } 29 | 30 | release maven java 31 | release nuget dotnet 32 | release npm js 33 | release pypi python 34 | release golang go 35 | -------------------------------------------------------------------------------- /bin/publib-ca: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../lib/bin/publib-ca.js'); -------------------------------------------------------------------------------- /bin/publib-golang: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../lib/bin/publib-golang.js'); -------------------------------------------------------------------------------- /bin/publib-maven: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../lib/bin/publib-maven.js'); -------------------------------------------------------------------------------- /bin/publib-npm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | ### 5 | # 6 | # Publishes all *.tgz files to npm 7 | # 8 | # Usage: ./publib-npm [DIR] 9 | # 10 | # DIR: directory where npm tarballs are found (default is `dist/js`). 11 | # 12 | # NPM_TOKEN (optional): registry authentication token (either from npmjs or a GitHub personal access token), not used for AWS CodeArtifact 13 | # NPM_REGISTRY (optional): the registry URL (defaults to "registry.npmjs.org") 14 | # AWS_ACCESS_KEY_ID (optional): If AWS CodeArtifact is used as registry, an AWS access key can be spedified. 15 | # AWS_SECRET_ACCESS_KEY (optional): Secret access key that belongs to the AWS access key. 16 | # AWS_ROLE_TO_ASSUME (optional): If AWS CodeArtifact is used as registry and need to assume role. 17 | # DISABLE_HTTPS (optional): connect to the registry with HTTP instead of HTTPS (defaults to false) 18 | # 19 | ### 20 | 21 | dir="${1:-"dist/js"}" 22 | 23 | 24 | if ! [ -z "${NPM_REGISTRY:-}" ] && [[ $NPM_REGISTRY =~ .codeartifact.*.amazonaws.com ]]; then 25 | codeartifact_account="$(echo $NPM_REGISTRY | cut -d. -f1 | rev | cut -d- -f1 | rev)" 26 | codeartifact_subdomain="$(echo $NPM_REGISTRY | cut -d. -f1)" 27 | codeartifact_domain="$(echo $codeartifact_subdomain | cut -b -$((${#codeartifact_subdomain}-${#codeartifact_account}-1)))" 28 | codeartifact_region="$(echo $NPM_REGISTRY | cut -d. -f4)" 29 | export AWS_DEFAULT_REGION="$(echo $codeartifact_region)" 30 | if [ -n "${AWS_ROLE_TO_ASSUME:-}" ]; then 31 | credentials=`aws sts assume-role --role-session-name "publib-code-artifact" --role-arn ${AWS_ROLE_TO_ASSUME} --output text | sed -n '2 p'` 32 | export AWS_ACCESS_KEY_ID="$(echo $credentials | cut -d' ' -f2)" 33 | export AWS_SECRET_ACCESS_KEY="$(echo $credentials | cut -d' ' -f4)" 34 | export AWS_SESSION_TOKEN="$(echo $credentials | cut -d' ' -f5)" 35 | fi 36 | NPM_TOKEN=`aws codeartifact get-authorization-token --domain $codeartifact_domain --domain-owner $codeartifact_account --region $codeartifact_region --query authorizationToken --output text` 37 | elif [ -z "${NPM_TOKEN:-}" ]; then 38 | echo "NPM_TOKEN is required" 39 | exit 1 40 | fi 41 | 42 | NPM_REGISTRY=${NPM_REGISTRY:-"registry.npmjs.org"} 43 | echo "//${NPM_REGISTRY%%/}/:_authToken=${NPM_TOKEN}" > ~/.npmrc 44 | 45 | # this overrides any registry configuration defined externally. For example, yarn sets the registry to the yarn proxy 46 | # which requires `yarn login`. but since we are logging in through ~/.npmrc, we must make sure we publish directly to npm. 47 | if ! [ -z "${DISABLE_HTTPS:-}" ]; then 48 | export NPM_CONFIG_REGISTRY="http://${NPM_REGISTRY}" 49 | else 50 | export NPM_CONFIG_REGISTRY="https://${NPM_REGISTRY}" 51 | fi 52 | 53 | # dist-tags 54 | tag="" 55 | if [ -n "${NPM_DIST_TAG:-}" ]; then 56 | tag="--tag ${NPM_DIST_TAG}" 57 | echo "Publishing under the following dist-tag: ${NPM_DIST_TAG}" 58 | fi 59 | 60 | # access level 61 | access="" 62 | if [ -n "${NPM_ACCESS_LEVEL:-}" ]; then 63 | if ! [[ "${NPM_ACCESS_LEVEL}" =~ ^(public|restricted)$ ]]; then 64 | echo "Invalid package access level: ${NPM_ACCESS_LEVEL}. Valid values are: public, restricted (default is restricted for scoped packages and public for unscoped packages)" 65 | exit 1 66 | fi 67 | 68 | access="--access ${NPM_ACCESS_LEVEL}" 69 | echo "Publishing package with access level: ${NPM_ACCESS_LEVEL}" 70 | fi 71 | 72 | log=$(mktemp -d)/npmlog.txt 73 | 74 | for file in ${dir}/**.tgz; do 75 | npm publish ${tag} ${access} ${file} 2>&1 | tee ${log} 76 | exit_code="${PIPESTATUS[0]}" 77 | 78 | if [ ${exit_code} -ne 0 ]; then 79 | 80 | # error returned from npmjs 81 | if cat ${log} | grep -q "You cannot publish over the previously published versions"; then 82 | echo "SKIPPING: already published" 83 | continue 84 | fi 85 | 86 | # error returned from github packages 87 | if cat ${log} | grep -q "Cannot publish over existing version"; then 88 | echo "SKIPPING: already published" 89 | continue 90 | fi 91 | 92 | echo "ERROR" 93 | exit 1 94 | fi 95 | done 96 | 97 | echo "SUCCESS" 98 | -------------------------------------------------------------------------------- /bin/publib-nuget: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | scriptdir=$(cd $(dirname $0) && pwd) 3 | set -eu # we don't want "pipefail" to implement idempotency 4 | 5 | ### 6 | # 7 | # Publishes all *.nupkg to NuGet 8 | # 9 | # Usage: ./publib-nuget [DIR] 10 | # 11 | # DIR is a directory with *.nupkg files (default is 'dist/dotnet') 12 | # 13 | # NUGET_API_KEY (required): API key for nuget 14 | # 15 | ### 16 | 17 | cd "${1:-"dist/dotnet"}" 18 | 19 | if [ -z "${NUGET_API_KEY:-}" ]; then 20 | echo "NUGET_API_KEY is required" 21 | exit 1 22 | fi 23 | 24 | packages=$(find . -name '*.nupkg' -not -iname '*.symbols.nupkg') 25 | if [ -z "${packages}" ]; then 26 | echo "❌ No *.nupkg files found under $PWD. Nothing to publish" 27 | exit 1 28 | fi 29 | 30 | echo "Publishing NuGet packages..." 31 | 32 | nuget_source="${NUGET_SERVER:-"https://api.nuget.org/v3/index.json"}" 33 | nuget_symbol_source="https://nuget.smbsrc.net/" 34 | 35 | log=$(mktemp -d)/log.txt 36 | 37 | for package_dir in ${packages}; do 38 | echo "📦 Publishing ${package_dir} to NuGet" 39 | ( 40 | cd $(dirname $package_dir) 41 | nuget_package_name=$(basename $package_dir) 42 | nuget_package_base=${nuget_package_name%.nupkg} 43 | 44 | [ -f "${nuget_package_base}.snupkg" ] || echo "⚠️ No symbols package was found!" 45 | 46 | # The .snupkg will be published at the same time as the .nupkg if both are in the current folder (which is the case) 47 | dotnet nuget push $nuget_package_name -k ${NUGET_API_KEY} -s ${nuget_source} | tee ${log} 48 | exit_code="${PIPESTATUS[0]}" 49 | 50 | # If push failed, check if this was caused because we are trying to publish 51 | # the same version again, which is not an error by searching for a magic string in the log 52 | # ugly, yes! 53 | if cat ${log} | grep -q -e "already exists and cannot be modified" -e "409 (Conflict)"; then 54 | echo "⚠️ Artifact already published. Skipping" 55 | elif [ "${exit_code}" -ne 0 ]; then 56 | echo "❌ Release failed" 57 | exit 1 58 | fi 59 | ) 60 | done 61 | 62 | echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" 63 | echo "✅ All Done!" 64 | -------------------------------------------------------------------------------- /bin/publib-pypi: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | ### 5 | # 6 | # Publishes all *.whl files to PyPI 7 | # 8 | # Usage: ./publib-pypi [DIR] 9 | # 10 | # DIR is where *.whl files are looked up (default is "dist/python") 11 | # 12 | # TWINE_USERNAME (required) 13 | # TWINE_PASSWORD (required) 14 | # 15 | ### 16 | 17 | cd "${1:-"dist/python"}" 18 | 19 | [ -z "${TWINE_USERNAME:-}" ] && { 20 | echo "Missing TWINE_USERNAME" 21 | exit 1 22 | } 23 | 24 | [ -z "${TWINE_PASSWORD:-}" ] && { 25 | echo "Missing TWINE_PASSWORD" 26 | exit 1 27 | } 28 | 29 | if [ -z "$(ls *.whl)" ]; then 30 | echo "cannot find any .whl files in $PWD" 31 | exit 1 32 | fi 33 | 34 | python3 -m pip install --user --upgrade twine 35 | python3 -m twine upload --verbose --skip-existing * 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "publib", 3 | "description": "Release jsii modules to multiple package managers", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cdklabs/publib.git" 7 | }, 8 | "bin": { 9 | "jsii-release": "./bin/jsii-release-shim", 10 | "jsii-release-ca": "./bin/jsii-release-shim", 11 | "jsii-release-golang": "./bin/jsii-release-shim", 12 | "jsii-release-maven": "./bin/jsii-release-shim", 13 | "jsii-release-npm": "./bin/jsii-release-shim", 14 | "jsii-release-nuget": "./bin/jsii-release-shim", 15 | "jsii-release-pypi": "./bin/jsii-release-shim", 16 | "jsii-release-shim": "bin/jsii-release-shim", 17 | "publib": "bin/publib", 18 | "publib-ca": "bin/publib-ca", 19 | "publib-golang": "bin/publib-golang", 20 | "publib-maven": "bin/publib-maven", 21 | "publib-npm": "bin/publib-npm", 22 | "publib-nuget": "bin/publib-nuget", 23 | "publib-pypi": "bin/publib-pypi" 24 | }, 25 | "scripts": { 26 | "build": "npx projen build", 27 | "bump": "npx projen bump", 28 | "clobber": "npx projen clobber", 29 | "compile": "npx projen compile", 30 | "default": "npx projen default", 31 | "eject": "npx projen eject", 32 | "eslint": "npx projen eslint", 33 | "integ": "npx projen integ", 34 | "package": "npx projen package", 35 | "package-legacy": "npx projen package-legacy", 36 | "post-compile": "npx projen post-compile", 37 | "post-upgrade": "npx projen post-upgrade", 38 | "pre-compile": "npx projen pre-compile", 39 | "release": "npx projen release", 40 | "test": "npx projen test", 41 | "test:watch": "npx projen test:watch", 42 | "unbump": "npx projen unbump", 43 | "upgrade": "npx projen upgrade", 44 | "upgrade-cdklabs-projen-project-types": "npx projen upgrade-cdklabs-projen-project-types", 45 | "upgrade-dev-deps": "npx projen upgrade-dev-deps", 46 | "watch": "npx projen watch", 47 | "projen": "npx projen" 48 | }, 49 | "author": { 50 | "name": "Amazon Web Services", 51 | "email": "aws-cdk-dev@amazon.com", 52 | "url": "https://aws.amazon.com", 53 | "organization": true 54 | }, 55 | "devDependencies": { 56 | "@aws-sdk/client-sts": "^3.823.0", 57 | "@stylistic/eslint-plugin": "^2", 58 | "@types/glob": "^8.1.0", 59 | "@types/jest": "^27.5.2", 60 | "@types/node": "^18.17.0", 61 | "@types/yargs": "^17", 62 | "@typescript-eslint/eslint-plugin": "^8", 63 | "@typescript-eslint/parser": "^8", 64 | "cdklabs-projen-project-types": "^0.3.1", 65 | "commit-and-tag-version": "^12", 66 | "constructs": "^10.0.0", 67 | "eslint": "^9", 68 | "eslint-import-resolver-typescript": "^2.7.1", 69 | "eslint-plugin-import": "^2.31.0", 70 | "jest": "^27.5.1", 71 | "jest-junit": "^16", 72 | "projen": "^0.92.9", 73 | "ts-jest": "^27.1.5", 74 | "ts-node": "^10.9.2", 75 | "typescript": "~5.7" 76 | }, 77 | "dependencies": { 78 | "@aws-sdk/client-codeartifact": "^3.821.0", 79 | "@aws-sdk/credential-providers": "^3.821.0", 80 | "@aws-sdk/types": "^3.821.0", 81 | "@types/fs-extra": "^8.0.0", 82 | "fs-extra": "^8.0.0", 83 | "glob": "10.0.0", 84 | "p-queue": "6", 85 | "shlex": "^2.1.2", 86 | "yargs": "^17", 87 | "zx": "^8.5.4" 88 | }, 89 | "main": "lib/index.js", 90 | "license": "Apache-2.0", 91 | "homepage": "https://github.com/cdklabs/publib", 92 | "publishConfig": { 93 | "access": "public" 94 | }, 95 | "version": "0.0.0", 96 | "jest": { 97 | "coverageProvider": "v8", 98 | "testMatch": [ 99 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 100 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 101 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 102 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 103 | ], 104 | "clearMocks": true, 105 | "collectCoverage": true, 106 | "coverageReporters": [ 107 | "json", 108 | "lcov", 109 | "clover", 110 | "cobertura", 111 | "text" 112 | ], 113 | "coverageDirectory": "coverage", 114 | "coveragePathIgnorePatterns": [ 115 | "/node_modules/" 116 | ], 117 | "testPathIgnorePatterns": [ 118 | "/node_modules/" 119 | ], 120 | "watchPathIgnorePatterns": [ 121 | "/node_modules/" 122 | ], 123 | "reporters": [ 124 | "default", 125 | [ 126 | "jest-junit", 127 | { 128 | "outputDirectory": "test-reports" 129 | } 130 | ] 131 | ], 132 | "preset": "ts-jest", 133 | "globals": { 134 | "ts-jest": { 135 | "tsconfig": "tsconfig.dev.json" 136 | } 137 | } 138 | }, 139 | "types": "lib/index.d.ts", 140 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 141 | } 142 | -------------------------------------------------------------------------------- /scripts/update-package-name.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const newName = process.argv[2]; 4 | 5 | if (!newName) { 6 | throw new Error(`Usage: update-package-name.js `); 7 | } 8 | 9 | const filepath = 'package.json'; 10 | const pkg = JSON.parse(fs.readFileSync(filepath)); 11 | pkg.name = newName; 12 | fs.writeFileSync(filepath, JSON.stringify(pkg, undefined, 2)); 13 | -------------------------------------------------------------------------------- /src/bin/publib-ca.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Publib CodeArtifact CLI 3 | */ 4 | /* eslint-disable no-console */ 5 | import * as yargs from 'yargs'; 6 | import { CodeArtifactCli } from '../codeartifact/codeartifact-cli'; 7 | import { header } from '../codeartifact/display'; 8 | 9 | export async function main() { 10 | await yargs 11 | .usage('$0 ') 12 | .option('assume-role-arn', { 13 | description: 'Role to assume before doing CodeArtifact calls', 14 | alias: 'a', 15 | requiresArg: true, 16 | type: 'string', 17 | }) 18 | .command('create', 'Create a temporary CodeArtifact repository with upstreams', cmd => cmd 19 | .option('gc', { 20 | description: 'Garbage collect old repositories', 21 | type: 'boolean', 22 | default: true, 23 | }) 24 | .option('login', { 25 | description: 'Automatically log in to the newly created repository', 26 | type: 'boolean', 27 | default: true, 28 | }), async (args) => { 29 | 30 | const cli = new CodeArtifactCli({ 31 | assumeRoleArn: args['assume-role-arn'], 32 | }); 33 | 34 | if (args.gc) { 35 | await cli.gc(); 36 | } 37 | 38 | const repoName = await cli.create(); 39 | console.log(repoName); 40 | 41 | if (args.login) { 42 | await cli.login(repoName); 43 | cli.usageDir.advertise(); 44 | } 45 | }) 46 | .command('gc', 'Clean up day-old testing repositories', cmd => cmd, async (args) => { 47 | const cli = new CodeArtifactCli({ 48 | assumeRoleArn: args['assume-role-arn'], 49 | }); 50 | await cli.gc(); 51 | }) 52 | .command('login', 'Login to a given repository', cmd => cmd 53 | .option('repo', { 54 | alias: 'r', 55 | description: 'Name of the repository to log in to', 56 | type: 'string', 57 | requiresArg: true, 58 | }) 59 | .option('cmd', { 60 | alias: 'c', 61 | description: 'Run a command in a shell set up for the target repository', 62 | type: 'string', 63 | requiresArg: true, 64 | }), async (args) => { 65 | 66 | const cli = new CodeArtifactCli({ 67 | assumeRoleArn: args['assume-role-arn'], 68 | }); 69 | await cli.login(args.repo); 70 | 71 | if (args.cmd) { 72 | await cli.runCommand(args.cmd); 73 | } else { 74 | cli.usageDir.advertise(); 75 | } 76 | }) 77 | .command('shell', 'Start a subshell with the repository activated', cmd => cmd 78 | .option('repo', { 79 | alias: 'r', 80 | description: 'Name of the repository to log in to', 81 | type: 'string', 82 | requiresArg: true, 83 | demandOption: false, 84 | }), async (args) => { 85 | const cli = new CodeArtifactCli({ 86 | assumeRoleArn: args['assume-role-arn'], 87 | }); 88 | const repo = await cli.login(args.repo); 89 | 90 | const defaultShell = process.platform === 'win32' ? 'cmd' : 'bash'; 91 | 92 | header(`Shell activated for ${repo.repositoryName}`); 93 | await cli.runInteractively(process.env.SHELL ?? defaultShell); 94 | }) 95 | .command('publish ', 'Publish a given directory', cmd => cmd 96 | .positional('DIRECTORY', { 97 | descripton: 'Directory distribution', 98 | type: 'string', 99 | demandOption: true, 100 | }) 101 | .option('repo', { 102 | alias: 'r', 103 | description: 'Name of the repository to create (default: generate unique name)', 104 | type: 'string', 105 | requiresArg: true, 106 | }), async (args) => { 107 | 108 | const cli = new CodeArtifactCli({ 109 | assumeRoleArn: args['assume-role-arn'], 110 | }); 111 | await cli.publish(args.DIRECTORY, args.repo); 112 | }) 113 | .command('delete', 'Delete testing repository', cmd => cmd 114 | .option('repo', { 115 | alias: 'r', 116 | description: 'Name of the repository to cleanup (default: most recently logged in to)', 117 | type: 'string', 118 | requiresArg: true, 119 | }), async (args) => { 120 | 121 | const cli = new CodeArtifactCli({ 122 | assumeRoleArn: args['assume-role-arn'], 123 | }); 124 | await cli.delete(args.repo); 125 | }) 126 | .demandCommand(1, 'You must supply a command') 127 | .help() 128 | .showHelpOnFail(false) 129 | .parse(); 130 | } 131 | 132 | main().catch(e => { 133 | // eslint-disable-next-line no-console 134 | console.error(e); 135 | process.exitCode = 1; 136 | }); 137 | -------------------------------------------------------------------------------- /src/bin/publib-golang.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as go from '../targets/go'; 3 | 4 | const releaser = new go.GoReleaser({ 5 | dir: process.argv[2], 6 | branch: process.env.GIT_BRANCH, 7 | dryRun: (process.env.DRYRUN ?? 'false').toLowerCase() === 'true', 8 | email: process.env.GIT_USER_EMAIL, 9 | username: process.env.GIT_USER_NAME, 10 | version: process.env.VERSION, 11 | }); 12 | 13 | releaser.release(); -------------------------------------------------------------------------------- /src/codeartifact/codeartifact-cli.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as path from 'path'; 3 | import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; 4 | import * as glob from 'glob'; 5 | import { CodeArtifactRepo, CodeArtifactRepoOptions, LoginInformation } from './codeartifact-repo'; 6 | import { shell } from './shell'; 7 | import { uploadJavaPackages, mavenLogin } from './staging/maven'; 8 | import { uploadNpmPackages, npmLogin } from './staging/npm'; 9 | import { uploadDotnetPackages, nugetLogin } from './staging/nuget'; 10 | import { uploadPythonPackages, pipLogin } from './staging/pip'; 11 | import { UsageDir } from './usage-dir'; 12 | 13 | const LOGIN_DATA_KEY = 'login'; 14 | 15 | export interface CodeArtifactCliOptions { 16 | readonly assumeRoleArn?: string; 17 | } 18 | 19 | export class CodeArtifactCli { 20 | public readonly usageDir = UsageDir.default(); 21 | 22 | constructor(private readonly options: CodeArtifactCliOptions = {}) { 23 | } 24 | 25 | private get repoOptions(): CodeArtifactRepoOptions { 26 | return { 27 | credentials: this.options.assumeRoleArn ? fromTemporaryCredentials({ 28 | params: { 29 | RoleArn: this.options.assumeRoleArn, 30 | DurationSeconds: 3600, 31 | RoleSessionName: 'publib-ca', 32 | }, 33 | }) : undefined, 34 | }; 35 | } 36 | 37 | /** 38 | * Create a random repository, return its name 39 | */ 40 | public async create() { 41 | const repo = await CodeArtifactRepo.createRandom(this.repoOptions); 42 | return repo.repositoryName; 43 | } 44 | 45 | /** 46 | * Delete the given repo 47 | */ 48 | public async delete(repoName?: string) { 49 | const repo = await this.repoFromName(repoName); 50 | await repo.delete(); 51 | 52 | if (!repoName) { 53 | await this.usageDir.delete(); 54 | } 55 | } 56 | 57 | /** 58 | * Log in to the given repo, write activation instructins to the usage dir 59 | */ 60 | public async login(repoName?: string) { 61 | const repo = await this.repoFromName(repoName); 62 | const login = await repo.login(); 63 | 64 | await this.usageDir.reset(); 65 | await this.usageDir.putJson(LOGIN_DATA_KEY, login); 66 | 67 | await this.usageDir.addToEnv({ 68 | CODEARTIFACT_REPO: login.repositoryName, 69 | }); 70 | 71 | await npmLogin(login, this.usageDir); 72 | await pipLogin(login, this.usageDir); 73 | await mavenLogin(login, this.usageDir); 74 | await nugetLogin(login, this.usageDir); 75 | 76 | return login; 77 | } 78 | 79 | public async publish(directory: string, repoName?: string) { 80 | const repo = await this.repoFromName(repoName); 81 | const login = await repo.login(); 82 | 83 | await uploadNpmPackages(glob.sync(path.join(directory, 'js', '*.tgz')), login, this.usageDir); 84 | 85 | await uploadPythonPackages(glob.sync(path.join(directory, 'python', '*')), login); 86 | 87 | await uploadJavaPackages(glob.sync(path.join(directory, 'java', '**', '*.pom')), login, this.usageDir); 88 | 89 | await uploadDotnetPackages(glob.sync(path.join(directory, 'dotnet', '**', '*.nupkg')), this.usageDir); 90 | 91 | console.log('🛍 Configuring packages for upstream versions'); 92 | await repo.markAllUpstreamAllow(); 93 | } 94 | 95 | public async gc() { 96 | await CodeArtifactRepo.gc(this.repoOptions); 97 | } 98 | 99 | public async runCommand(command: string) { 100 | await this.usageDir.activateInCurrentProcess(); 101 | await shell(command, { 102 | shell: true, 103 | show: 'always', 104 | }); 105 | } 106 | 107 | public async runInteractively(command: string) { 108 | await this.usageDir.activateInCurrentProcess(); 109 | child_process.execSync(command, { 110 | env: process.env, 111 | stdio: ['inherit', 'inherit', 'inherit'], 112 | }); 113 | } 114 | 115 | /** 116 | * Return a CodeArtifactRepo object, either from the name argument or the most recently activated repository 117 | */ 118 | private async repoFromName(repoName?: string) { 119 | if (repoName) { 120 | return CodeArtifactRepo.existing(repoName, this.repoOptions); 121 | } 122 | 123 | const loginInfo = await this.usageDir.readJson(LOGIN_DATA_KEY) ; 124 | 125 | if (loginInfo && loginInfo.expirationTimeMs > Date.now()) { 126 | const existing = CodeArtifactRepo.existing(loginInfo.repositoryName, this.repoOptions); 127 | existing.setLoginInformation(loginInfo); 128 | return existing; 129 | } 130 | 131 | throw new Error('No repository name given, and no repository activated recently. Login to a repo or pass a repo name.'); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/codeartifact/codeartifact-repo.ts: -------------------------------------------------------------------------------- 1 | import { AssociateExternalConnectionCommand, CodeartifactClient, CreateDomainCommand, CreateRepositoryCommand, DeleteRepositoryCommand, DescribeDomainCommand, DescribeRepositoryCommand, GetAuthorizationTokenCommand, GetRepositoryEndpointCommand, ListPackagesCommand, ListPackagesCommandInput, ListRepositoriesCommand, ListTagsForResourceCommand, PutPackageOriginConfigurationCommand, ResourceNotFoundException, ThrottlingException } from '@aws-sdk/client-codeartifact'; 2 | import { AwsCredentialIdentityProvider } from '@aws-sdk/types'; 3 | import { sleep } from '../help/sleep'; 4 | 5 | const COLLECT_BY_TAG = 'collect-by'; 6 | const REPO_LIFETIME_MS = 24 * 3600 * 1000; // One day 7 | 8 | export interface CodeArtifactRepoOptions { 9 | readonly credentials?: AwsCredentialIdentityProvider; 10 | } 11 | 12 | /** 13 | * A CodeArtifact repository 14 | */ 15 | export class CodeArtifactRepo { 16 | public static readonly DEFAULT_DOMAIN = 'publib-ca'; 17 | 18 | /** 19 | * Create a CodeArtifact repo with a random name 20 | */ 21 | public static async createRandom(options: CodeArtifactRepoOptions = {}) { 22 | const qualifier = Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); 23 | 24 | const repo = new CodeArtifactRepo(`test-${qualifier}`, options); 25 | await repo.create(); 26 | return repo; 27 | } 28 | 29 | /** 30 | * Create a CodeArtifact repository with a given name 31 | */ 32 | public static async createWithName(name: string, options: CodeArtifactRepoOptions = {}) { 33 | const repo = new CodeArtifactRepo(name, options); 34 | await repo.create(); 35 | return repo; 36 | } 37 | 38 | /** 39 | * Reference an existing CodeArtifact repository 40 | */ 41 | public static existing(repositoryName: string, options: CodeArtifactRepoOptions = {}) { 42 | return new CodeArtifactRepo(repositoryName, options); 43 | } 44 | 45 | /** 46 | * Garbage collect repositories 47 | */ 48 | public static async gc(options: CodeArtifactRepoOptions = {}) { 49 | if (!await CodeArtifactRepo.existing('*dummy*').domainExists()) { 50 | return; 51 | } 52 | 53 | const codeArtifact = new CodeartifactClient({ 54 | credentials: options.credentials, 55 | }); 56 | 57 | let nextToken: string | undefined; 58 | do { 59 | const page = await retryThrottled(() => codeArtifact.send(new ListRepositoriesCommand({ nextToken }))); 60 | 61 | for (const repo of page.repositories ?? []) { 62 | const tags = await retryThrottled(() => codeArtifact.send(new ListTagsForResourceCommand({ resourceArn: repo.arn! }))); 63 | const collectable = tags?.tags?.find(t => t.key === COLLECT_BY_TAG && Number(t.value) < Date.now()); 64 | if (collectable) { 65 | // eslint-disable-next-line no-console 66 | console.error('Deleting', repo.name); 67 | await retryThrottled(() => codeArtifact.send(new DeleteRepositoryCommand({ 68 | domain: repo.domainName!, 69 | repository: repo.name!, 70 | }))); 71 | } 72 | } 73 | 74 | nextToken = page.nextToken; 75 | } while (nextToken); 76 | } 77 | 78 | public readonly npmUpstream = 'npm-upstream'; 79 | public readonly pypiUpstream = 'pypi-upstream'; 80 | public readonly nugetUpstream = 'nuget-upstream'; 81 | public readonly mavenUpstream = 'maven-upstream'; 82 | public readonly domain = CodeArtifactRepo.DEFAULT_DOMAIN; 83 | 84 | private readonly codeArtifact; 85 | 86 | private _loginInformation: LoginInformation | undefined; 87 | private readonly send: CodeartifactClient['send']; 88 | 89 | private constructor( 90 | public readonly repositoryName: string, 91 | options: CodeArtifactRepoOptions = {}, 92 | ) { 93 | this.codeArtifact = new CodeartifactClient({ credentials: options.credentials }); 94 | 95 | // Letting the compiler infer the types in this way is the only way to get it to typecheck 96 | this.send = (command) => retryThrottled(() => this.codeArtifact.send(command)); 97 | } 98 | 99 | /** 100 | * Create the repository 101 | */ 102 | public async create() { 103 | await this.ensureDomain(); 104 | await this.ensureUpstreams(); 105 | 106 | await this.ensureRepository(this.repositoryName, { 107 | description: 'Testing repository', 108 | upstreams: [ 109 | this.npmUpstream, 110 | this.pypiUpstream, 111 | this.nugetUpstream, 112 | this.mavenUpstream, 113 | ], 114 | tags: { 115 | [COLLECT_BY_TAG]: `${Date.now() + REPO_LIFETIME_MS}`, 116 | }, 117 | }); 118 | } 119 | 120 | /** 121 | * Absorb old login information 122 | */ 123 | public setLoginInformation(loginInfo: LoginInformation) { 124 | if (loginInfo.repositoryName !== this.repositoryName) { 125 | throw new Error(`This login info seems to be for a different repo. '${this.repositoryName}' != '${loginInfo.repositoryName}'`); 126 | } 127 | this._loginInformation = loginInfo; 128 | } 129 | 130 | public async login(): Promise { 131 | if (this._loginInformation) { 132 | return this._loginInformation; 133 | } 134 | 135 | const durationSeconds = 12 * 3600; 136 | const authToken = await this.send(new GetAuthorizationTokenCommand({ domain: this.domain, durationSeconds })); 137 | 138 | this._loginInformation = { 139 | // eslint-disable-next-line max-len 140 | authToken: authToken.authorizationToken!, 141 | expirationTimeMs: authToken.expiration?.getTime() ?? (Date.now() + durationSeconds * 1000), 142 | repositoryName: this.repositoryName, 143 | npmEndpoint: (await this.send(new GetRepositoryEndpointCommand({ domain: this.domain, repository: this.repositoryName, format: 'npm' }))).repositoryEndpoint!, 144 | mavenEndpoint: (await this.send(new GetRepositoryEndpointCommand({ domain: this.domain, repository: this.repositoryName, format: 'maven' }))).repositoryEndpoint!, 145 | nugetEndpoint: (await this.send(new GetRepositoryEndpointCommand({ domain: this.domain, repository: this.repositoryName, format: 'nuget' }))).repositoryEndpoint!, 146 | pypiEndpoint: (await this.send(new GetRepositoryEndpointCommand({ domain: this.domain, repository: this.repositoryName, format: 'pypi' }))).repositoryEndpoint!, 147 | }; 148 | return this._loginInformation; 149 | } 150 | 151 | public async delete() { 152 | try { 153 | await this.send(new DeleteRepositoryCommand({ 154 | domain: this.domain, 155 | repository: this.repositoryName, 156 | })); 157 | 158 | // eslint-disable-next-line no-console 159 | console.error('Deleted', this.repositoryName); 160 | } catch (e: any) { 161 | if (!isResourceNotFoundException(e)) { throw e; } 162 | // Okay 163 | } 164 | } 165 | 166 | /** 167 | * List all packages and mark them as "allow upstream versions". 168 | * 169 | * If we don't do this and we publish `foo@2.3.4-rc.0`, then we can't 170 | * download `foo@2.3.0` anymore because by default CodeArtifact will 171 | * block different versions from the same package. 172 | */ 173 | public async markAllUpstreamAllow() { 174 | for await (const pkg of this.listPackages({ upstream: 'BLOCK' })) { 175 | await this.send(new PutPackageOriginConfigurationCommand({ 176 | domain: this.domain, 177 | repository: this.repositoryName, 178 | 179 | format: pkg.format!, 180 | package: pkg.package!, 181 | namespace: pkg.namespace!, 182 | restrictions: { 183 | publish: 'ALLOW', 184 | upstream: 'ALLOW', 185 | }, 186 | })); 187 | } 188 | } 189 | 190 | private async ensureDomain() { 191 | if (await this.domainExists()) { return; } 192 | await this.send(new CreateDomainCommand({ 193 | domain: this.domain, 194 | tags: [{ key: 'testing', value: 'true' }], 195 | })); 196 | } 197 | 198 | private async ensureUpstreams() { 199 | await this.ensureRepository(this.npmUpstream, { 200 | description: 'The upstream repository for NPM', 201 | external: 'public:npmjs', 202 | }); 203 | await this.ensureRepository(this.mavenUpstream, { 204 | description: 'The upstream repository for Maven', 205 | external: 'public:maven-central', 206 | }); 207 | await this.ensureRepository(this.nugetUpstream, { 208 | description: 'The upstream repository for NuGet', 209 | external: 'public:nuget-org', 210 | }); 211 | await this.ensureRepository(this.pypiUpstream, { 212 | description: 'The upstream repository for PyPI', 213 | external: 'public:pypi', 214 | }); 215 | } 216 | 217 | private async ensureRepository(name: string, options?: { 218 | readonly description?: string; 219 | readonly external?: string; 220 | readonly upstreams?: string[]; 221 | readonly tags?: Record; 222 | }) { 223 | if (await this.repositoryExists(name)) { return; } 224 | 225 | await this.send(new CreateRepositoryCommand({ 226 | domain: this.domain, 227 | repository: name, 228 | description: options?.description, 229 | upstreams: options?.upstreams?.map(repositoryName => ({ repositoryName })), 230 | tags: options?.tags ? Object.entries(options.tags).map(([key, value]) => ({ key, value })) : undefined, 231 | })); 232 | 233 | if (options?.external) { 234 | const externalConnection = options.external; 235 | await retry(() => this.send(new AssociateExternalConnectionCommand({ 236 | domain: this.domain, 237 | repository: name, 238 | externalConnection, 239 | }))); 240 | } 241 | } 242 | 243 | private async domainExists() { 244 | try { 245 | await this.send(new DescribeDomainCommand({ domain: this.domain })); 246 | return true; 247 | } catch (e: any) { 248 | if (!isResourceNotFoundException(e)) { throw e; } 249 | return false; 250 | } 251 | } 252 | 253 | private async repositoryExists(name: string) { 254 | try { 255 | await this.send(new DescribeRepositoryCommand({ domain: this.domain, repository: name })); 256 | return true; 257 | } catch (e: any) { 258 | if (!isResourceNotFoundException(e)) { throw e; } 259 | return false; 260 | } 261 | } 262 | 263 | private async* listPackages(filter: Pick = {}) { 264 | let response = await this.send(new ListPackagesCommand({ 265 | domain: this.domain, 266 | repository: this.repositoryName, 267 | ...filter, 268 | })); 269 | 270 | while (true) { 271 | for (const p of response.packages ?? []) { 272 | yield p; 273 | } 274 | 275 | if (!response.nextToken) { 276 | break; 277 | } 278 | 279 | response = await this.send(new ListPackagesCommand({ 280 | domain: this.domain, 281 | repository: this.repositoryName, 282 | ...filter, 283 | nextToken: response.nextToken, 284 | })); 285 | } 286 | } 287 | } 288 | 289 | async function retry(block: () => Promise) { 290 | let attempts = 3; 291 | while (true) { 292 | try { 293 | return await block(); 294 | } catch (e: any) { 295 | if (attempts-- === 0) { throw e; } 296 | // eslint-disable-next-line no-console 297 | console.debug(e.message); 298 | await sleep(500); 299 | } 300 | } 301 | } 302 | 303 | async function retryThrottled(block: () => Promise) { 304 | let time = 100; 305 | let attempts = 15; 306 | while (true) { 307 | try { 308 | return await block(); 309 | } catch (e: any) { 310 | // eslint-disable-next-line no-console 311 | console.debug(e); 312 | 313 | if (!(e instanceof ThrottlingException) || --attempts === 0) { 314 | throw e; 315 | } 316 | 317 | await sleep(Math.floor(Math.random() * time)); 318 | time *= 2; 319 | } 320 | } 321 | } 322 | 323 | export interface LoginInformation { 324 | readonly authToken: string; 325 | readonly expirationTimeMs: number; 326 | readonly repositoryName: string; 327 | readonly npmEndpoint: string; 328 | readonly mavenEndpoint: string; 329 | readonly nugetEndpoint: string; 330 | readonly pypiEndpoint: string; 331 | } 332 | 333 | function isResourceNotFoundException(x: any): x is ResourceNotFoundException { 334 | return x instanceof ResourceNotFoundException; 335 | } -------------------------------------------------------------------------------- /src/codeartifact/display.ts: -------------------------------------------------------------------------------- 1 | export function header(caption: string) { 2 | console.log(''); 3 | console.log('/'.repeat(70)); 4 | console.log(`// ${caption}`); 5 | console.log(''); 6 | } -------------------------------------------------------------------------------- /src/codeartifact/files.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs-extra'; 4 | 5 | export async function rmFile(filename: string) { 6 | if (await fs.pathExists(filename)) { 7 | await fs.unlink(filename); 8 | } 9 | } 10 | 11 | export async function addToFile(filename: string, line: string) { 12 | let contents = await fs.pathExists(filename) ? await fs.readFile(filename, { encoding: 'utf-8' }) : ''; 13 | if (!contents.endsWith('\n')) { 14 | contents += '\n'; 15 | } 16 | contents += line + '\n'; 17 | 18 | await writeFile(filename, contents); 19 | } 20 | 21 | export async function writeFile(filename: string, contents: string) { 22 | await fs.mkdirp(path.dirname(filename)); 23 | await fs.writeFile(filename, contents, { encoding: 'utf-8' }); 24 | } 25 | 26 | export async function copyDirectoryContents(dir: string, target: string) { 27 | for (const file of await fs.readdir(path.join(dir))) { 28 | await fs.copyFile(path.join(dir, file), path.join(target, file)); 29 | } 30 | } 31 | 32 | export function findUp(name: string, directory: string = process.cwd()): string | undefined { 33 | const absoluteDirectory = path.resolve(directory); 34 | 35 | const file = path.join(directory, name); 36 | if (fs.existsSync(file)) { 37 | return file; 38 | } 39 | 40 | const { root } = path.parse(absoluteDirectory); 41 | if (absoluteDirectory == root) { 42 | return undefined; 43 | } 44 | 45 | return findUp(name, path.dirname(absoluteDirectory)); 46 | } 47 | 48 | 49 | /** 50 | * Docker-safe home directory 51 | */ 52 | export function homeDir() { 53 | return os.userInfo().homedir ?? os.homedir(); 54 | } 55 | 56 | export async function loadLines(filename: string): Promise { 57 | return await fs.pathExists(filename) ? (await fs.readFile(filename, { encoding: 'utf-8' })).trim().split('\n') : []; 58 | } 59 | 60 | export async function writeLines(filename: string, lines: string[]) { 61 | // Must end in a newline or our bash script won't read it properly 62 | await fs.writeFile(filename, lines.join('\n') + '\n', { encoding: 'utf-8' }); 63 | } 64 | 65 | /** 66 | * Update a spaceless ini file in place 67 | */ 68 | export function updateIniKey(lines: string[], key: string, value: string) { 69 | const prefix = `${key}=`; 70 | let found = false; 71 | for (let i = 0; i < lines.length; i++) { 72 | if (lines[i].startsWith(prefix)) { 73 | lines[i] = prefix + value; 74 | found = true; 75 | break; 76 | } 77 | } 78 | if (!found) { 79 | lines.push(prefix + value); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/codeartifact/shell.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | /** 6 | * A shell command that does what you want 7 | * 8 | * Is platform-aware, handles errors nicely. 9 | */ 10 | export async function shell(command: string | string[], options: ShellOptions = {}): Promise { 11 | if (options.modEnv && options.env) { 12 | throw new Error('Use either env or modEnv but not both'); 13 | } 14 | 15 | // Always output the command 16 | const commandAsString = Array.isArray(command) ? command.join(' ') : command; 17 | (options.output ?? process.stdout).write(`💻 ${commandAsString}\n`); 18 | 19 | let output: NodeJS.WritableStream | undefined = options.output ?? process.stdout; 20 | switch (options.show ?? 'always') { 21 | case 'always': 22 | break; 23 | case 'never': 24 | case 'error': 25 | output = undefined; 26 | break; 27 | } 28 | 29 | if (process.env.VERBOSE) { 30 | output = process.stdout; 31 | } 32 | 33 | const env = options.env ?? (options.modEnv ? { ...process.env, ...options.modEnv } : process.env); 34 | const spawnOptions: child_process.SpawnOptionsWithStdioTuple = { 35 | ...options, 36 | env, 37 | // Need this for Windows where we want .cmd and .bat to be found as well. 38 | shell: true, 39 | stdio: ['ignore', 'pipe', 'pipe'], 40 | }; 41 | 42 | const child = Array.isArray(command) 43 | ? child_process.spawn(command[0], command.slice(1), spawnOptions) 44 | : child_process.spawn(command, spawnOptions); 45 | 46 | return new Promise((resolve, reject) => { 47 | const stdout = new Array(); 48 | const stderr = new Array(); 49 | 50 | child.stdout!.on('data', chunk => { 51 | output?.write(chunk); 52 | stdout.push(chunk); 53 | }); 54 | 55 | child.stderr!.on('data', chunk => { 56 | output?.write(chunk); 57 | if (options.captureStderr ?? true) { 58 | stderr.push(chunk); 59 | } 60 | }); 61 | 62 | child.once('error', reject); 63 | 64 | child.once('close', code => { 65 | const stderrOutput = Buffer.concat(stderr).toString('utf-8'); 66 | const stdoutOutput = Buffer.concat(stdout).toString('utf-8'); 67 | const out = (options.onlyStderr ? stderrOutput : stdoutOutput + stderrOutput).trim(); 68 | if (code === 0 || options.allowErrExit) { 69 | resolve(out); 70 | } else { 71 | if (options.show === 'error') { 72 | (options.output ?? process.stdout).write(out + '\n'); 73 | } 74 | reject(new Error(`'${commandAsString}' exited with error code ${code}.`)); 75 | } 76 | }); 77 | }); 78 | } 79 | 80 | export interface ShellOptions extends child_process.SpawnOptions { 81 | /** 82 | * Properties to add to 'env' 83 | */ 84 | readonly modEnv?: Record; 85 | 86 | /** 87 | * Don't fail when exiting with an error 88 | * 89 | * @default false 90 | */ 91 | readonly allowErrExit?: boolean; 92 | 93 | /** 94 | * Whether to capture stderr 95 | * 96 | * @default true 97 | */ 98 | readonly captureStderr?: boolean; 99 | 100 | /** 101 | * Pass output here 102 | * 103 | * @default stdout unless quiet=true 104 | */ 105 | readonly output?: NodeJS.WritableStream; 106 | 107 | /** 108 | * Only return stderr. For example, this is used to validate 109 | * that when CI=true, all logs are sent to stdout. 110 | * 111 | * @default false 112 | */ 113 | readonly onlyStderr?: boolean; 114 | 115 | /** 116 | * Don't log to stdout 117 | * 118 | * @default always 119 | */ 120 | readonly show?: 'always' | 'never' | 'error'; 121 | } 122 | 123 | /** 124 | * rm -rf reimplementation, don't want to depend on an NPM package for this 125 | */ 126 | export function rimraf(fsPath: string) { 127 | try { 128 | const isDir = fs.lstatSync(fsPath).isDirectory(); 129 | 130 | if (isDir) { 131 | for (const file of fs.readdirSync(fsPath)) { 132 | rimraf(path.join(fsPath, file)); 133 | } 134 | fs.rmdirSync(fsPath); 135 | } else { 136 | fs.unlinkSync(fsPath); 137 | } 138 | } catch (e: any) { 139 | // We will survive ENOENT 140 | if (e.code !== 'ENOENT') { throw e; } 141 | } 142 | } 143 | 144 | export function addToShellPath(x: string) { 145 | const parts = process.env.PATH?.split(':') ?? []; 146 | 147 | if (!parts.includes(x)) { 148 | parts.unshift(x); 149 | } 150 | 151 | process.env.PATH = parts.join(':'); 152 | } 153 | -------------------------------------------------------------------------------- /src/codeartifact/staging/maven.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import * as path from 'path'; 3 | import { pathExists } from 'fs-extra'; 4 | import { parallelShell } from './parallel-shell'; 5 | import { LoginInformation } from '../codeartifact-repo'; 6 | import { header } from '../display'; 7 | import { writeFile } from '../files'; 8 | import { shell } from '../shell'; 9 | import { UsageDir } from '../usage-dir'; 10 | 11 | // Do not try to JIT the Maven binary 12 | const NO_JIT = '-XX:+TieredCompilation -XX:TieredStopAtLevel=1'; 13 | 14 | export async function mavenLogin(login: LoginInformation, usageDir: UsageDir) { 15 | await writeMavenSettingsFile(settingsFile(usageDir), login); 16 | 17 | // Write env var 18 | // Twiddle JVM settings a bit to make Maven survive running on a CodeBuild box. 19 | await usageDir.addToEnv({ 20 | MAVEN_OPTS: `-Duser.home=${usageDir.directory} ${NO_JIT} ${process.env.MAVEN_OPTS ?? ''}`.trim(), 21 | }); 22 | } 23 | 24 | function settingsFile(usageDir: UsageDir) { 25 | // If we configure usageDir as a fake home directory Maven will find this file. 26 | // (No other way to configure the settings file as part of the environment). 27 | return path.join(usageDir.directory, '.m2', 'settings.xml'); 28 | } 29 | 30 | export async function uploadJavaPackages(packages: string[], login: LoginInformation, usageDir: UsageDir) { 31 | if (packages.length === 0) { 32 | return; 33 | } 34 | 35 | header('Java'); 36 | await parallelShell(packages, async (pkg, output) => { 37 | console.log(`⏳ ${pkg}`); 38 | 39 | const sourcesFile = pkg.replace(/.pom$/, '-sources.jar'); 40 | const javadocFile = pkg.replace(/.pom$/, '-javadoc.jar'); 41 | 42 | await shell(['mvn', 43 | `--settings=${settingsFile(usageDir)}`, 44 | 'org.apache.maven.plugins:maven-deploy-plugin:3.0.0:deploy-file', 45 | `-Durl=${login.mavenEndpoint}`, 46 | '-DrepositoryId=codeartifact', 47 | `-DpomFile=${pkg}`, 48 | `-Dfile=${pkg.replace(/.pom$/, '.jar')}`, 49 | ...await pathExists(sourcesFile) ? [`-Dsources=${sourcesFile}`] : [], 50 | ...await pathExists(javadocFile) ? [`-Djavadoc=${javadocFile}`] : []], { 51 | output, 52 | modEnv: { 53 | // Do not try to JIT the Maven binary 54 | MAVEN_OPTS: `${NO_JIT} ${process.env.MAVEN_OPTS ?? ''}`.trim(), 55 | }, 56 | }); 57 | 58 | console.log(`✅ ${pkg}`); 59 | }, 60 | (pkg, output) => { 61 | if (output.toString().includes('409 Conflict')) { 62 | console.log(`❌ ${pkg}: already exists. Skipped.`); 63 | return 'skip'; 64 | } 65 | if (output.toString().includes('Too Many Requests')) { 66 | console.log(`♻️ ${pkg}: Too many requests. Retrying.`); 67 | return 'retry'; 68 | } 69 | return 'fail'; 70 | }); 71 | } 72 | 73 | export async function writeMavenSettingsFile(filename: string, login: LoginInformation) { 74 | await writeFile(filename, ` 75 | 79 | 80 | 81 | codeartifact 82 | aws 83 | ${login.authToken} 84 | 85 | 86 | 87 | 88 | default 89 | 90 | 91 | codeartifact 92 | ${login.mavenEndpoint} 93 | 94 | 95 | 96 | 97 | 98 | default 99 | 100 | `); 101 | } 102 | -------------------------------------------------------------------------------- /src/codeartifact/staging/npm.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import * as path from 'path'; 3 | import { parallelShell } from './parallel-shell'; 4 | import { LoginInformation } from '../codeartifact-repo'; 5 | import { header } from '../display'; 6 | import { updateIniKey, loadLines, writeLines } from '../files'; 7 | import { shell } from '../shell'; 8 | import { UsageDir } from '../usage-dir'; 9 | 10 | export async function npmLogin(login: LoginInformation, usageDir: UsageDir) { 11 | // Creating an ~/.npmrc that references an envvar is what you're supposed to do. (https://docs.npmjs.com/private-modules/ci-server-config) 12 | await writeNpmLoginToken(usageDir, login.npmEndpoint, '${NPM_TOKEN}'); 13 | 14 | // Add variables to env file 15 | await usageDir.addToEnv(npmEnv(usageDir, login)); 16 | } 17 | 18 | function npmEnv(usageDir: UsageDir, login: LoginInformation) { 19 | return { 20 | npm_config_userconfig: path.join(usageDir.directory, '.npmrc'), 21 | npm_config_registry: login.npmEndpoint, 22 | npm_config_always_auth: 'true', // Necessary for NPM 6, otherwise it will sometimes not pass the token 23 | NPM_TOKEN: login.authToken, 24 | }; 25 | } 26 | 27 | export async function uploadNpmPackages(packages: string[], login: LoginInformation, usageDir: UsageDir) { 28 | if (packages.length === 0) { 29 | return; 30 | } 31 | 32 | header('NPM'); 33 | await parallelShell(packages, async (pkg, output) => { 34 | console.log(`⏳ ${pkg}`); 35 | 36 | // path.resolve() is required -- if the filename ends up looking like `js/bla.tgz` then NPM thinks it's a short form GitHub name. 37 | await shell(['npm', 'publish', path.resolve(pkg)], { 38 | modEnv: npmEnv(usageDir, login), 39 | show: 'error', 40 | output, 41 | }); 42 | 43 | console.log(`✅ ${pkg}`); 44 | }, (pkg, output) => { 45 | if (output.toString().includes('code EPUBLISHCONFLICT')) { 46 | console.log(`❌ ${pkg}: already exists. Skipped.`); 47 | return 'skip'; 48 | } 49 | if (output.toString().includes('code EPRIVATE')) { 50 | console.log(`❌ ${pkg}: is private. Skipped.`); 51 | return 'skip'; 52 | } 53 | return 'fail'; 54 | }); 55 | } 56 | 57 | async function writeNpmLoginToken(usageDir: UsageDir, endpoint: string, token: string) { 58 | const rcFile = path.join(usageDir.directory, '.npmrc'); 59 | const lines = await loadLines(rcFile); 60 | 61 | const key = `${endpoint.replace(/^https:/, '')}:_authToken`; 62 | updateIniKey(lines, key, token); 63 | 64 | await writeLines(rcFile, lines); 65 | return rcFile; 66 | } 67 | 68 | // Environment variable, .npmrc in same directory as package.json or in home dir -------------------------------------------------------------------------------- /src/codeartifact/staging/nuget.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { parallelShell } from './parallel-shell'; 3 | import { LoginInformation } from '../codeartifact-repo'; 4 | import { header } from '../display'; 5 | import { writeFile } from '../files'; 6 | import { shell } from '../shell'; 7 | import { UsageDir } from '../usage-dir'; 8 | 9 | export async function nugetLogin(login: LoginInformation, usageDir: UsageDir) { 10 | // NuGet.Config MUST live in the current directory or in the home directory, and there is no environment 11 | // variable to configure its location. 12 | await writeNuGetConfigFile(usageDir.cwdFile('NuGet.Config'), login); 13 | } 14 | 15 | export async function uploadDotnetPackages(packages: string[], usageDir: UsageDir) { 16 | if (packages.length === 0) { 17 | return; 18 | } 19 | 20 | header('.NET'); 21 | await usageDir.copySelectCwdFileHere('NuGet.Config'); 22 | 23 | await parallelShell(packages, async (pkg, output) => { 24 | console.log(`⏳ ${pkg}`); 25 | 26 | await shell(['dotnet', 'nuget', 'push', 27 | pkg, 28 | '--source', 'CodeArtifact', 29 | '--no-symbols', 30 | '--force-english-output', 31 | '--disable-buffering', 32 | '--timeout', '600', 33 | '--skip-duplicate'], { 34 | output, 35 | }); 36 | 37 | console.log(`✅ ${pkg}`); 38 | }, 39 | (pkg, output) => { 40 | if (output.toString().includes('Conflict')) { 41 | console.log(`❌ ${pkg}: already exists. Skipped.`); 42 | return 'skip'; 43 | } 44 | if (output.includes('System.Threading.AbandonedMutexException')) { 45 | console.log(`♻️ ${pkg}: AbandonedMutexException. Probably a sign of throttling, retrying.`); 46 | return 'retry'; 47 | } 48 | if (output.includes('Too Many Requests')) { 49 | console.log(`♻️ ${pkg}: Too many requests. Retrying.`); 50 | return 'retry'; 51 | } 52 | if (output.includes('System.IO.IOException: The system cannot open the device or file specified.')) { 53 | console.log(`♻️ ${pkg}: Some error that we've seen before as a result of throttling. Retrying.`); 54 | return 'retry'; 55 | } 56 | return 'fail'; 57 | }); 58 | } 59 | 60 | async function writeNuGetConfigFile(filename: string, login: LoginInformation) { 61 | // `dotnet nuget push` has an `--api-key` parameter, but CodeArtifact 62 | // does not support that. We must authenticate with Basic auth. 63 | await writeFile(filename, ` 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | `); 79 | } 80 | 81 | // NuGet.Config in current directory -------------------------------------------------------------------------------- /src/codeartifact/staging/parallel-shell.ts: -------------------------------------------------------------------------------- 1 | import PQueue from 'p-queue'; 2 | import { MemoryStream } from '../../help/corking'; 3 | import { sleep } from '../../help/sleep'; 4 | 5 | 6 | export type ErrorResponse = 'fail' | 'skip' | 'retry'; 7 | 8 | /** 9 | * Run a function in parallel with cached output 10 | */ 11 | export async function parallelShell( 12 | inputs: A[], 13 | block: (x: A, output: NodeJS.WritableStream) => Promise, 14 | swallowError?: (x: A, output: string) => ErrorResponse, 15 | ) { 16 | // Limit to 10 for now, too many instances of Maven exhaust the CodeBuild instance memory 17 | const q = new PQueue({ concurrency: Number(process.env.CONCURRENCY) || 10 }); 18 | await q.addAll(inputs.map(input => async () => { 19 | let attempts = 10; 20 | let sleepMs = 500; 21 | while (true) { 22 | const output = new MemoryStream(); 23 | try { 24 | await block(input, output); 25 | return; 26 | } catch (e) { 27 | switch (swallowError?.(input, output.toString())) { 28 | case 'skip': 29 | return; 30 | 31 | case 'retry': 32 | if (--attempts > 0) { 33 | await sleep(Math.floor(Math.random() * sleepMs)); 34 | sleepMs *= 2; 35 | continue; 36 | } 37 | break; 38 | 39 | case 'fail': 40 | case undefined: 41 | break; 42 | } 43 | 44 | // eslint-disable-next-line no-console 45 | console.error(output.toString()); 46 | throw e; 47 | } 48 | } 49 | })); 50 | 51 | await q.onEmpty(); 52 | } -------------------------------------------------------------------------------- /src/codeartifact/staging/pip.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { parallelShell } from './parallel-shell'; 3 | import { LoginInformation } from '../codeartifact-repo'; 4 | import { header } from '../display'; 5 | import { shell } from '../shell'; 6 | import { UsageDir } from '../usage-dir'; 7 | 8 | export async function pipLogin(login: LoginInformation, usageDir: UsageDir) { 9 | // Write pip config file and set environment var 10 | const pipConfig = await usageDir.putFile('pip.conf', [ 11 | '[global]', 12 | `index-url = https://aws:${login.authToken}@${login.pypiEndpoint.replace(/^https:\/\//, '')}simple/`, 13 | ].join('\n')); 14 | 15 | await usageDir.addToEnv({ 16 | PIP_CONFIG_FILE: pipConfig, 17 | }); 18 | } 19 | 20 | export async function uploadPythonPackages(packages: string[], login: LoginInformation) { 21 | if (packages.length === 0) { 22 | return; 23 | } 24 | 25 | header('Python'); 26 | await shell(['pip', 'install', 'twine'], { show: 'error' }); 27 | 28 | // Even though twine supports uploading all packages in one go, we have to upload them 29 | // individually since CodeArtifact does not support Twine's `--skip-existing`. Fun beans. 30 | await parallelShell(packages, async (pkg, output) => { 31 | console.log(`⏳ ${pkg}`); 32 | 33 | await shell(['twine', 'upload', '--verbose', pkg], { 34 | modEnv: { 35 | TWINE_USERNAME: 'aws', 36 | TWINE_PASSWORD: login.authToken, 37 | TWINE_REPOSITORY_URL: login.pypiEndpoint, 38 | }, 39 | show: 'error', 40 | output, 41 | }); 42 | 43 | console.log(`✅ ${pkg}`); 44 | }, (pkg, output) => { 45 | if (output.toString().includes('This package is configured to block new versions') || output.toString().includes('409 Conflict')) { 46 | console.log(`❌ ${pkg}: already exists. Skipped.`); 47 | return 'skip'; 48 | } 49 | if (output.includes('429 Too Many Requests ')) { 50 | console.log(`♻️ ${pkg}: 429 Too Many Requests. Retrying.`); 51 | return 'retry'; 52 | } 53 | return 'fail'; 54 | }); 55 | } -------------------------------------------------------------------------------- /src/codeartifact/usage-dir.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import { copyDirectoryContents, homeDir, loadLines, updateIniKey, writeFile, writeLines } from './files'; 4 | 5 | export const DEFAULT_USAGE_DIR = path.join(homeDir(), '.publib-ca/usage'); 6 | 7 | /** 8 | * The usage directory is where we write per-session config files to access the CodeArtifact repository. 9 | * 10 | * Some config files may be written in a system-global location, but they will not be active unless the 11 | * contents of this directory have been sourced/copied into the current terminal. 12 | * 13 | * CONTRACT 14 | * 15 | * There are two special entries: 16 | * 17 | * - `env`, a file with `key=value` entries for environment variables to set. 18 | * - `cwd/`, a directory with files that need to be copied into the current directory before each command. 19 | * 20 | * Other than these, code may write tempfiles to this directory if it wants, but there is no meaning 21 | * implied for other files. 22 | */ 23 | export class UsageDir { 24 | public static default() { 25 | return new UsageDir(DEFAULT_USAGE_DIR); 26 | } 27 | 28 | public readonly envFile: string; 29 | public readonly cwdDir: string; 30 | 31 | private constructor(public readonly directory: string) { 32 | this.envFile = path.join(this.directory, 'env'); 33 | this.cwdDir = path.join(this.directory, 'cwd'); 34 | } 35 | 36 | public async delete() { 37 | if (await fs.pathExists(this.directory)) { 38 | await fs.remove(this.directory); 39 | } 40 | } 41 | 42 | /** 43 | * Create a fresh empty directory, with helper scripts 44 | */ 45 | public async reset() { 46 | await this.delete(); 47 | await fs.mkdirp(path.join(this.directory, 'cwd')); 48 | await fs.writeFile(path.join(this.directory, 'env'), '', { encoding: 'utf-8' }); 49 | 50 | await this.addToEnv({ 51 | CWD_FILES_DIR: path.join(this.directory, 'cwd'), 52 | }); 53 | 54 | // Write a bash helper to load these settings 55 | await fs.writeFile(path.join(this.directory, 'activate.bash'), [ 56 | `while read -u10 line; do [[ -z $line ]] || export "$line"; done 10<${this.directory}/env`, 57 | 'cp -R $CWD_FILES_DIR/ .', // Copy files from directory even if it is empty 58 | ].join('\n'), { encoding: 'utf-8' }); 59 | } 60 | 61 | /** 62 | * Set the expiration time of the current settings 63 | */ 64 | public async setExpirationTimeMs(timestamp: number) { 65 | await this.addToEnv({ 66 | EXPIRATION_TIME_MS: `${timestamp}`, 67 | }); 68 | } 69 | 70 | /** 71 | * Add settings to the environment variables 72 | */ 73 | public async addToEnv(settings: Record) { 74 | const lines = await loadLines(this.envFile); 75 | for (const [k, v] of Object.entries(settings)) { 76 | updateIniKey(lines, k, v); 77 | } 78 | await writeLines(this.envFile, lines); 79 | } 80 | 81 | /** 82 | * Return the current environment variables 83 | */ 84 | public async currentEnv(): Promise> { 85 | const lines = await loadLines(this.envFile); 86 | 87 | const splitter = /^([a-zA-Z0-9_-]+)\s*=\s*(.*)$/; 88 | 89 | const ret: Record = {}; 90 | for (const line of lines) { 91 | const m = line.match(splitter); 92 | if (m) { 93 | ret[m[1]] = m[2]; 94 | } 95 | } 96 | return ret; 97 | } 98 | 99 | public cwdFile(filename: string) { 100 | return path.join(this.cwdDir, filename); 101 | } 102 | 103 | /** 104 | * Activate in the current process (update process.env), copy the cwd/ directory to the current directory 105 | */ 106 | public async activateInCurrentProcess() { 107 | for (const [k, v] of Object.entries(await this.currentEnv())) { 108 | process.env[k] = v; 109 | } 110 | 111 | await copyDirectoryContents(this.cwdDir, '.'); 112 | } 113 | 114 | public async copySelectCwdFileHere(...filenames: string[]) { 115 | for (const file of filenames) { 116 | await fs.copyFile(path.join(this.cwdDir, file), file); 117 | } 118 | } 119 | 120 | public async putFile(filename: string, contents: string) { 121 | const fileName = path.join(this.directory, filename); 122 | await writeFile(fileName, contents); 123 | return fileName; 124 | } 125 | 126 | public async putCwdFile(filename: string, contents: string) { 127 | await writeFile(path.join(this.cwdDir, filename), contents); 128 | } 129 | 130 | public async putJson(key: string, data: any) { 131 | await writeFile(path.join(this.directory, key + '.json'), JSON.stringify(data, undefined, 2)); 132 | } 133 | 134 | public async readJson(key: string): Promise { 135 | try { 136 | return await fs.readJson(path.join(this.directory, key + '.json')); 137 | } catch (e: any) { 138 | if (e.code === 'ENOENT') { 139 | return undefined; 140 | } 141 | 142 | throw e; 143 | } 144 | } 145 | 146 | /** 147 | * Print to the console on how to activate these settings 148 | */ 149 | public advertise() { 150 | // eslint-disable-next-line no-console 151 | console.log('To activate these settings in the current bash shell:'); 152 | // eslint-disable-next-line no-console 153 | console.log(` source ${this.directory}/activate.bash`); 154 | } 155 | } -------------------------------------------------------------------------------- /src/help/corking.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Routines for corking stdout and stderr 3 | */ 4 | import * as stream from 'stream'; 5 | 6 | export class MemoryStream extends stream.Writable { 7 | private parts = new Array(); 8 | 9 | public _write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void { 10 | this.parts.push(chunk); 11 | callback(); 12 | } 13 | 14 | public buffer() { 15 | return Buffer.concat(this.parts); 16 | } 17 | 18 | public clear() { 19 | this.parts.splice(0, this.parts.length); 20 | } 21 | 22 | public async flushTo(strm: NodeJS.WritableStream): Promise { 23 | const flushed = strm.write(this.buffer()); 24 | if (!flushed) { 25 | return new Promise(ok => strm.once('drain', ok)); 26 | } 27 | } 28 | 29 | public toString() { 30 | return this.buffer().toString(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/help/git.ts: -------------------------------------------------------------------------------- 1 | import * as shell from './shell'; 2 | 3 | /** 4 | * Clones a repository from GitHub. Requires a `GITHUB_TOKEN` env variable. 5 | * 6 | * @param repositoryUrl the repository to clone. 7 | * @param targetDir the clone directory. 8 | */ 9 | export function clone(repositoryUrl: string, targetDir: string) { 10 | const gitHubUseSsh = detectSSH(); 11 | if (gitHubUseSsh) { 12 | const sshRepositoryUrl = repositoryUrl.replace('/', ':'); 13 | shell.run(`git clone git@${sshRepositoryUrl}.git ${targetDir}`); 14 | } else { 15 | const gitHubToken = getToken(detectGHE()); 16 | if (!gitHubToken) { 17 | throw new Error('GITHUB_TOKEN env variable is required when GITHUB_USE_SSH env variable is not used'); 18 | } 19 | shell.run(`git clone https://${gitHubToken}@${repositoryUrl}.git ${targetDir}`); 20 | 21 | } 22 | } 23 | 24 | /** 25 | * Checks if the current environment is an GHE environment. 26 | * 27 | * This check is using GITHUB_API_URL set in GitHub Actions workflow, as well as common gh cli env variables. 28 | * https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables 29 | * https://cli.github.com/manual/gh_help_environment 30 | * 31 | * @return - `true` if GH_HOST or GITHUB_API_URL env var are defined and not equal to the public github endpoint, otherwise `false` 32 | */ 33 | export function detectGHE(): boolean { 34 | const githubApiUrl = process.env.GITHUB_API_URL; 35 | const ghHost = process.env.GH_HOST; 36 | 37 | return (Boolean(ghHost) && ghHost!.trim().toLowerCase() != 'github.com') 38 | || (Boolean(githubApiUrl) && githubApiUrl!.trim().toLowerCase() != 'https://api.github.com'); 39 | } 40 | 41 | /** 42 | * Returns an appropriate github token from the environment. 43 | * 44 | * @return GH_ENTERPRISE_TOKEN or GITHUB_ENTERPRISE_TOKEN or GITHUB_TOKEN if in an GHE environment, otherwise GITHUB_TOKEN 45 | */ 46 | 47 | export function getToken(isGHE: boolean): (string | undefined) { 48 | if (isGHE) { 49 | const githubEnterpiseToken = process.env.GH_ENTERPRISE_TOKEN ?? process.env.GITHUB_ENTERPRISE_TOKEN ?? process.env.GITHUB_TOKEN; 50 | return githubEnterpiseToken; 51 | } 52 | return process.env.GITHUB_TOKEN; 53 | } 54 | 55 | /** 56 | * Checks if SSH should be used to clone repo. 57 | * This checks the presence and values of the GIT_USE_SSH env variable and the deprecated GITHUB_USE_SSH for legacy reason. Returns true if either of these env vars are defined and not falsy. 58 | */ 59 | 60 | export function detectSSH(): boolean { 61 | return Boolean(process.env.GIT_USE_SSH ?? process.env.GITHUB_USE_SSH); 62 | } 63 | 64 | /** 65 | * Query the git index for changes. 66 | * 67 | * @return True if changes exist, False otherwise. 68 | */ 69 | export function diffIndex(): boolean { 70 | try { 71 | shell.run('git diff-index --exit-code HEAD --'); 72 | return false; 73 | } catch (err) { 74 | return true; 75 | } 76 | } 77 | 78 | /** 79 | * Add files to the index. 80 | * 81 | * @param p the path. 82 | */ 83 | export function add(p: string) { 84 | shell.run(`git add ${p}`); 85 | } 86 | 87 | /** 88 | * Commit. 89 | * 90 | * @param message the commit message. 91 | */ 92 | export function commit(message: string) { 93 | shell.run(`git commit -m "${message}"`); 94 | } 95 | 96 | /** 97 | * Initialize a repository. 98 | */ 99 | export function init() { 100 | shell.run('git init'); 101 | } 102 | 103 | /** 104 | * Cerate a tag. 105 | * 106 | * @param name tag name. 107 | * @returns true if the tag was created, false if it already exists. 108 | */ 109 | export function tag(name: string): boolean { 110 | try { 111 | shell.run(`git tag -a ${name} -m ${name}`, { capture: true }); 112 | return true; 113 | } catch (e) { 114 | if (e instanceof Error && e.message.includes('already exists')) { 115 | return false; 116 | } 117 | throw e; 118 | } 119 | } 120 | 121 | /** 122 | * Push a ref to origin. 123 | * 124 | * @param ref the ref 125 | */ 126 | export function push(ref: string) { 127 | shell.run(`git push origin ${ref}`); 128 | } 129 | 130 | /** 131 | * Checkout to a new branch. Creates a new one if `options.createIfMissing` is True and the branch doesn't exist. 132 | * 133 | * @param branch the branch. 134 | * @param options options. 135 | */ 136 | export function checkout(branch: string, options: { createIfMissing?: boolean } ) { 137 | if (options.createIfMissing) { 138 | try { 139 | shell.run(`git show-branch origin/${branch}`, { capture: true }); 140 | } catch (e) { 141 | if (e instanceof Error && e.message.includes('fatal: bad sha1 reference')) { 142 | console.log('Remote branch not found, creating new branch.'); 143 | shell.run(`git checkout -B ${branch}`); 144 | return; 145 | } 146 | } 147 | } 148 | shell.run(`git checkout ${branch}`); 149 | } 150 | 151 | /** 152 | * Fetch the configured git user name for the current directory. 153 | * Returns undefined if not configured. 154 | */ 155 | export function username() { 156 | try { 157 | return shell.run('git config user.name', { capture: true }); 158 | } catch (err) { 159 | if (err instanceof Error) { 160 | console.warn(err.message); 161 | } 162 | return undefined; 163 | } 164 | } 165 | 166 | /** 167 | * Fetch the configured git user email for the current directory. 168 | * Returns undefined if not configured. 169 | */ 170 | export function email() { 171 | try { 172 | return shell.run('git config user.email', { capture: true }); 173 | } catch (err) { 174 | if (err instanceof Error) { 175 | console.warn(err.message); 176 | } 177 | return undefined; 178 | } 179 | } 180 | 181 | /** 182 | * Identify the committer with a username and email. 183 | * 184 | * @param user the username. 185 | * @param email the email address. 186 | */ 187 | export function identify(user: string, address: string) { 188 | shell.run(`git config user.name "${user}"`); 189 | shell.run(`git config user.email "${address}"`); 190 | } 191 | -------------------------------------------------------------------------------- /src/help/os.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | 5 | /** 6 | * Creates a temporary directory inside the global temp dir of the OS. 7 | */ 8 | export function mkdtempSync() { 9 | // mkdtempSync only appends a six char suffix, it doesn't create a nested 10 | // directory. 11 | return fs.mkdtempSync(`${os.tmpdir()}${path.sep}`); 12 | } 13 | -------------------------------------------------------------------------------- /src/help/shell.ts: -------------------------------------------------------------------------------- 1 | import * as child from 'child_process'; 2 | import * as shlex from 'shlex'; 3 | 4 | /** 5 | * Options for the `run` function. 6 | */ 7 | export interface RunOptions { 8 | 9 | /** 10 | * Wokring directory. 11 | * 12 | * @default process.cwd() 13 | */ 14 | readonly cwd?: string; 15 | 16 | /** 17 | * Capture the output of the command and return to caller. 18 | * 19 | * @default - no capture, output is printed to stdout. 20 | */ 21 | readonly capture?: boolean; 22 | 23 | /** 24 | * Run the command inside a shell. 25 | * 26 | * @default false 27 | */ 28 | readonly shell?: boolean; 29 | 30 | /** 31 | * Properties to add to 'env' 32 | */ 33 | readonly modEnv?: Record; 34 | } 35 | 36 | /** 37 | * Run a shell command and return the output. 38 | * Throws if the command fails. 39 | * 40 | * @param command command (e.g 'git commit -m') 41 | */ 42 | export function run(command: string, options: RunOptions = {}): string { 43 | const shsplit = shlex.split(command); 44 | const pipeOrInherit = (options.capture ?? false) ? 'pipe': 'inherit'; 45 | const env = { ...process.env, ...options.modEnv }; 46 | const result = child.spawnSync(shsplit[0], shsplit.slice(1), { 47 | stdio: ['ignore', pipeOrInherit, pipeOrInherit], 48 | cwd: options.cwd, 49 | shell: options.shell, 50 | env, 51 | }); 52 | if (result.error) { 53 | throw result.error; 54 | } 55 | 56 | const stdout = result.stdout?.toString(); 57 | const stderr = result.stderr?.toString(); 58 | 59 | if (result.status !== 0) { 60 | const message = ` 61 | Command failed: ${command}. 62 | Output: ${stdout} 63 | Error: ${stderr}`; 64 | throw new Error(message); 65 | } 66 | return stdout; 67 | } 68 | 69 | /** 70 | * Return the path under which a program is available. 71 | * Empty string if the program is not installed. 72 | * 73 | * @param program program to check (e.g 'git') 74 | */ 75 | export function which(program: string): string { 76 | try { 77 | return run(`which ${program}`, { capture: true }); 78 | } catch (err) { 79 | return ''; 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/help/sleep.ts: -------------------------------------------------------------------------------- 1 | export async function sleep(ms: number) { 2 | return new Promise(ok => setTimeout(ok, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './targets/go'; -------------------------------------------------------------------------------- /src/targets/go.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import * as git from '../help/git'; 4 | import * as os from '../help/os'; 5 | import * as shell from '../help/shell'; 6 | 7 | /** 8 | * Encapsulates some information about the release. 9 | */ 10 | export interface GoRelease { 11 | 12 | /** 13 | * The tags the release created. 14 | */ 15 | readonly tags?: string[]; 16 | 17 | /** 18 | * The commit message of the release. 19 | */ 20 | readonly commitMessage?: string; 21 | 22 | /** 23 | * The directory where the repository was released from. 24 | */ 25 | readonly repoDir?: string; 26 | } 27 | 28 | /** 29 | * Properties for `GoReleaser`. 30 | */ 31 | export interface GoReleaserProps { 32 | 33 | /** 34 | * The source code directory. 35 | * 36 | * @default 'dist/go' 37 | */ 38 | readonly dir?: string; 39 | 40 | /** 41 | * Execute a dry run only. 42 | * 43 | * @default false 44 | */ 45 | readonly dryRun?: boolean; 46 | 47 | /** 48 | * The branch to push to. 49 | * 50 | * @default 'main' 51 | */ 52 | readonly branch?: string; 53 | 54 | /** 55 | * The username to use for the commit. 56 | * 57 | * @default - taken from git config. throws if not configured. 58 | */ 59 | readonly username?: string; 60 | 61 | /** 62 | * The email to use for the commit. 63 | * 64 | * @default - taken from git config. throws if not configured. 65 | */ 66 | readonly email?: string; 67 | 68 | /** 69 | * The version. 70 | * 71 | * @default - taken from the 'version'. throws if doesn't exist. 72 | */ 73 | readonly version?: string; 74 | 75 | /** 76 | * The message to use for the commit marking the release. 77 | */ 78 | readonly message?: string; 79 | } 80 | 81 | /** 82 | * Information about a specific module. 83 | */ 84 | export interface GoModule { 85 | 86 | /** 87 | * Path to the mod file. 88 | */ 89 | readonly modFile: string; 90 | 91 | /** 92 | * The version of the module. 93 | */ 94 | readonly version: string; 95 | 96 | /** 97 | * The cannonical name of the module. (e.g 'github.com/aws/constructs-go/constructs/v3`) 98 | */ 99 | readonly cannonicalName: string; 100 | 101 | /** 102 | * The path inside the repository the module is located in. (e.g 'constructs') 103 | */ 104 | readonly repoPath: string; 105 | 106 | /** 107 | * The repository URL. (e.g 'github.com/aws/constructs') 108 | */ 109 | readonly repoURL: string; 110 | 111 | } 112 | 113 | /** 114 | * Release a set of Golang modules. 115 | */ 116 | export class GoReleaser { 117 | 118 | private readonly version?: string; 119 | private readonly gitCommitMessage?: string; 120 | 121 | private readonly dir: string; 122 | private readonly dryRun: boolean; 123 | private readonly gitBranch: string; 124 | private readonly gitUsername: string; 125 | private readonly gitUseremail: string; 126 | 127 | constructor(props: GoReleaserProps) { 128 | 129 | if (!shell.which('git')) { 130 | throw new Error('git must be available to in order to be able to push Go code to GitHub'); 131 | } 132 | 133 | this.version = props.version; 134 | this.gitCommitMessage = props.message; 135 | this.dir = path.resolve(props.dir ?? path.join(process.cwd(), 'dist', 'go')); 136 | this.gitBranch = props.branch ?? 'main'; 137 | this.dryRun = props.dryRun ?? false; 138 | 139 | const gitUsername = props.username ?? git.username(); 140 | const gitUseremail = props.email ?? git.email(); 141 | 142 | if (!gitUsername) { 143 | throw new Error('Unable to detect username. either configure a git user.name or pass GIT_USER_NAME env variable'); 144 | } 145 | 146 | if (!gitUseremail) { 147 | throw new Error('Unable to detect user email. either configure a git user.email or pass GIT_USER_EMAIL env variable'); 148 | } 149 | 150 | this.gitUseremail = gitUseremail; 151 | this.gitUsername = gitUsername; 152 | } 153 | 154 | /** 155 | * Run the release process. 156 | * 157 | * @returns metadata about the release. 158 | */ 159 | public release(): GoRelease { 160 | const modules = this.collectModules(this.dir); 161 | if (modules.length === 0) { 162 | console.log('No modules detected. Skipping'); 163 | return {}; 164 | } 165 | 166 | console.log('Detected modules:'); 167 | modules.forEach(m => console.log(` - ${m.modFile}`)); 168 | 169 | const repoURL = this.extractRepoURL(modules); 170 | const repoDir = path.join(os.mkdtempSync(), 'repo'); 171 | git.clone(repoURL, repoDir); 172 | 173 | const cwd = process.cwd(); 174 | try { 175 | process.chdir(repoDir); 176 | return this.doRelease(modules, repoDir); 177 | } finally { 178 | process.chdir(cwd); 179 | } 180 | 181 | } 182 | 183 | private doRelease(modules: GoModule[], repoDir: string): GoRelease { 184 | 185 | git.identify(this.gitUsername, this.gitUseremail); 186 | git.checkout(this.gitBranch, { createIfMissing: true }); 187 | this.syncRepo(repoDir); 188 | 189 | git.add('.'); 190 | if (!git.diffIndex()) { 191 | console.log('No changes. Skipping release'); 192 | return {}; 193 | } 194 | 195 | const commitMessage = this.gitCommitMessage ?? this.buildReleaseMessage(modules); 196 | git.commit(commitMessage); 197 | 198 | const tags = []; 199 | for (const module of modules) { 200 | const name = this.buildTagName(module); 201 | const created = git.tag(name); 202 | if (created) { tags.push(name); } 203 | } 204 | 205 | if (tags.length === 0) { 206 | console.log('All tags already exist. Skipping release'); 207 | return {}; 208 | } 209 | 210 | const refs = [...tags, this.gitBranch]; 211 | 212 | if (this.dryRun) { 213 | console.log('==========================================='); 214 | console.log(' 🏜️ DRY-RUN MODE 🏜️'); 215 | console.log('==========================================='); 216 | refs.forEach(t => console.log(`Remote ref will be updated: ${t}`)); 217 | } else { 218 | refs.forEach(t => git.push(t)); 219 | } 220 | return { tags, commitMessage, repoDir }; 221 | } 222 | 223 | private collectModules(dir: string): GoModule[] { 224 | const modules: GoModule[] = []; 225 | for (const p of [...fs.readdirSync(dir), '.']) { 226 | const modFile = path.join(dir, p, 'go.mod'); 227 | if (fs.existsSync(modFile)) { 228 | modules.push(this.parseModule(modFile)); 229 | } 230 | } 231 | return modules; 232 | } 233 | 234 | private parseModule(modFile: string): GoModule { 235 | 236 | const version = this.extractVersion(path.dirname(modFile)); 237 | const majorVersion = Number(version.split('.')[0]); 238 | 239 | // extract the module decleration (e.g 'module github.com/aws/constructs-go/constructs/v3') 240 | const match = fs.readFileSync(modFile).toString().match(/module (.*)/); 241 | if (!match || !match[1]) { 242 | throw new Error(`Unable to detected module declaration in ${modFile}`); 243 | } 244 | 245 | // e.g 'github.com/aws/constructs-go/constructs/v3' 246 | const cannonicalName = match[1]; 247 | 248 | // e.g ['github.com', 'aws', 'constructs-go', 'constructs', 'v3'] 249 | const cannonicalNameParts = cannonicalName.split('/'); 250 | 251 | // e.g 'github.com/aws/constructs-go' 252 | const repoURL = cannonicalNameParts.slice(0, 3).join('/'); 253 | 254 | // e.g 'v3' 255 | const majorVersionSuffix = majorVersion > 1 ? `/v${majorVersion}` : ''; 256 | 257 | if (!cannonicalName.endsWith(majorVersionSuffix)) { 258 | throw new Error(`Module declaration in '${modFile}' expected to end with '${majorVersionSuffix}' since its major version is larger than 1`); 259 | } 260 | 261 | if (!repoURL.startsWith('github.com')) { 262 | if (!(git.detectSSH() || git.detectGHE())) { 263 | throw new Error(`Repository must be hosted on github.com. Found: '${repoURL}' in ${modFile}`); 264 | } 265 | } 266 | let repoPath = cannonicalNameParts 267 | .slice(3) // e.g ['constructs', 'v3'] 268 | .join('/'); // e.g 'constructs/v3' 269 | 270 | // we could have something like 271 | // constructs/v3 272 | // or something like 273 | // constructsv3/v3 274 | // we only want to strip the last 'v3' 275 | const split = repoPath.split('/'); 276 | if (split[split.length - 1] === `v${majorVersion}`) { 277 | split.pop(); 278 | repoPath = split.join('/'); 279 | } 280 | 281 | // strip '/' if exists (wont exist for top level modules) 282 | repoPath = repoPath.endsWith('/') ? repoPath.substr(0, repoPath.length - 1) : repoPath; 283 | 284 | return { modFile, version, cannonicalName, repoPath, repoURL }; 285 | 286 | } 287 | 288 | private buildTagName(m: GoModule) { 289 | return m.repoPath === '' ? `v${m.version}` : `${m.repoPath}/v${m.version}`; 290 | } 291 | 292 | private buildReleaseMessage(modules: GoModule[]) { 293 | const semantic = 'chore(release)'; 294 | const versions = new Set(modules.map(m => m.version)); 295 | if (versions.size === 1) { 296 | // single version (e.g chore(release): v1.2.3) 297 | return `${semantic}: v${versions.values().next().value}`; 298 | } else { 299 | // multiple versions (e.g chore(release): chore(release): module1@v1.2.3 module2@v1.2.3) 300 | return `${semantic}: ${modules.map(m => `${m.repoPath ? `${m.repoPath}@` : ''}v${m.version}`).join(' ')}`; 301 | } 302 | } 303 | 304 | private syncRepo(repoDir: string) { 305 | const topLevel = path.join(repoDir, 'go.mod'); 306 | if (fs.existsSync(topLevel)) { 307 | // with top level modules we sync the entire repository 308 | // so we just empty it out 309 | fs.readdirSync(repoDir) 310 | .filter(f => f !== '.git') 311 | .forEach(f => fs.removeSync(path.join(repoDir, f))); 312 | } else { 313 | // otherwise, we selectively remove the submodules only. 314 | for (const p of fs.readdirSync(repoDir)) { 315 | const submodule = path.join(repoDir, p, 'go.mod'); 316 | if (fs.existsSync(submodule)) { 317 | fs.removeSync(path.join(repoDir, p)); 318 | } 319 | } 320 | } 321 | fs.copySync(this.dir, repoDir, { recursive: true }); 322 | } 323 | 324 | private extractRepoURL(modules: GoModule[]): string { 325 | const repos = new Set(modules.map(m => m.repoURL)); 326 | if (repos.size === 0) { 327 | throw new Error('Unable to detect repository from module files.'); 328 | } 329 | if (repos.size > 1) { 330 | throw new Error('Multiple repositories found in module files'); 331 | } 332 | return repos.values().next().value!; 333 | } 334 | 335 | private extractVersion(moduleDirectory: string): string { 336 | 337 | const versionFile = path.join(moduleDirectory, 'version'); 338 | 339 | const repoVersion = this.version; 340 | const moduleVersion = fs.existsSync(versionFile) ? fs.readFileSync(versionFile).toString() : undefined; 341 | 342 | if (repoVersion && moduleVersion && repoVersion !== moduleVersion) { 343 | throw new Error(`Repo version (${repoVersion}) conflicts with module version (${moduleVersion}) for module in ${moduleDirectory}`); 344 | } 345 | 346 | // just take the one thats defined, they have to the same if both are. 347 | const version = moduleVersion ? moduleVersion : repoVersion; 348 | 349 | if (!version) { 350 | throw new Error(`Unable to determine version of module ${moduleDirectory}. ` 351 | + "Either include a 'version' file, or specify a global version using the VERSION environment variable."); 352 | } 353 | 354 | return version; 355 | 356 | } 357 | 358 | } 359 | -------------------------------------------------------------------------------- /test/__fixtures__/combined/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/combined 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/combined/module1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/combined/module1 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/combined/module1/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/combined/module1/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/combined/module2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/combined/module2 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/combined/module2/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/combined/module2/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/combined/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/combined/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/github-enterprise/go.mod: -------------------------------------------------------------------------------- 1 | module github.corporate-enterprise.com/aws/not-github 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/github-enterprise/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/github-enterprise/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/major-version/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/major-version/v3 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/major-version/module1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/major-version/module1/v3 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/major-version/module1/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/major-version/module1/version: -------------------------------------------------------------------------------- 1 | 3.3.3 -------------------------------------------------------------------------------- /test/__fixtures__/major-version/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/major-version/version: -------------------------------------------------------------------------------- 1 | 3.3.3 -------------------------------------------------------------------------------- /test/__fixtures__/major-versionv3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/major-version/v3 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /test/__fixtures__/major-versionv3/module1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/major-version/module1v3/v3 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /test/__fixtures__/major-versionv3/module1/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/major-versionv3/module1/version: -------------------------------------------------------------------------------- 1 | 3.3.3 -------------------------------------------------------------------------------- /test/__fixtures__/major-versionv3/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/major-versionv3/version: -------------------------------------------------------------------------------- 1 | 3.3.3 -------------------------------------------------------------------------------- /test/__fixtures__/multi-repo/module1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/repo1 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/multi-repo/module1/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/multi-repo/module1/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/multi-repo/module2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/repo2 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/multi-repo/module2/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/multi-repo/module2/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/multi-version/module1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/sub-modules/module1 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/multi-version/module1/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/multi-version/module1/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/multi-version/module2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/sub-modules/module2 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/multi-version/module2/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/multi-version/module2/version: -------------------------------------------------------------------------------- 1 | 1.2.0 -------------------------------------------------------------------------------- /test/__fixtures__/no-major-version-suffix/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/major-version 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/no-major-version-suffix/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/no-major-version-suffix/version: -------------------------------------------------------------------------------- 1 | 3.3.3 -------------------------------------------------------------------------------- /test/__fixtures__/no-modules/file.txt: -------------------------------------------------------------------------------- 1 | just a file -------------------------------------------------------------------------------- /test/__fixtures__/no-version/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/no-version 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/no-version/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/not-github/go.mod: -------------------------------------------------------------------------------- 1 | module gitlab.com/aws/not-github 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/not-github/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/not-github/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/sub-modules/module1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/sub-modules/module1 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/sub-modules/module1/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/sub-modules/module1/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/sub-modules/module2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/sub-modules/module2 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/sub-modules/module2/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/sub-modules/module2/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/__fixtures__/top-level/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/top-level 2 | 3 | go 1.15 -------------------------------------------------------------------------------- /test/__fixtures__/top-level/source.go: -------------------------------------------------------------------------------- 1 | import ( 2 | "fmt" 3 | ) 4 | 5 | func main() { 6 | fmt.Println("Just some go code") 7 | } -------------------------------------------------------------------------------- /test/__fixtures__/top-level/version: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /test/publib-ca.integ.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import * as path from 'path'; 3 | import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; 4 | import * as fs from 'fs-extra'; 5 | import { inTemporaryDirectory } from './with-temporary-directory'; 6 | import { shell } from '../src/codeartifact/shell'; 7 | 8 | jest.setTimeout(60_0000); 9 | 10 | test('this runs with AWS credentials', async () => { 11 | const sts = new STSClient({}); 12 | const response = await sts.send(new GetCallerIdentityCommand({})); 13 | expect(response.Arn).toBeTruthy(); 14 | }); 15 | 16 | test('can create an NPM package, publish and consume it from CodeArtifact', async () => { 17 | await inTemporaryDirectory(async () => { 18 | await shell('npm init -y', { captureStderr: false }); 19 | await fs.writeFile('index.js', 'console.log("It works!");'); 20 | const packageName = (await fs.readJson('package.json')).name; 21 | const tarball = await shell('npm pack --loglevel=silent', { captureStderr: false }); 22 | 23 | // Tarball needs to be in a 'js/' subdirectory 24 | await fs.mkdirp('dist/js'); 25 | await fs.rename(tarball, `dist/js/${tarball}`); 26 | 27 | // Now let's run 'publib-ca' 28 | await publibCa(['create']); 29 | try { 30 | // Publish 31 | await publibCa(['publish', 'dist']); 32 | 33 | // Install 34 | await fs.mkdir('consumer'); 35 | await fs.writeJson('consumer/package.json', { 36 | name: 'consumer', 37 | private: true, 38 | version: '0.0.1', 39 | dependencies: { 40 | [packageName]: '*', 41 | }, 42 | }); 43 | 44 | await publibCa(['login', '--cmd', '"cd consumer && npm install"']); 45 | const output = await shell(`node -e "require('${packageName}');"`, { cwd: 'consumer', captureStderr: false }); 46 | expect(output).toContain('It works!'); 47 | } finally { 48 | // Clean up 49 | await publibCa(['delete']); 50 | } 51 | }); 52 | }); 53 | 54 | async function publibCa(args: string[]) { 55 | const cli = path.resolve(__dirname, '../src/bin/publib-ca.ts'); 56 | return shell(['ts-node', cli, ...args], { captureStderr: false }); 57 | } -------------------------------------------------------------------------------- /test/targets/git-mocked.test.ts: -------------------------------------------------------------------------------- 1 | import * as git from '../../src/help/git'; 2 | import * as shell from '../../src/help/shell'; 3 | 4 | // mock shell.run 5 | jest.mock('../../src/help/shell'); 6 | const mockedShellRun = (shell.run as unknown) as jest.Mock; 7 | 8 | // restore env after each test 9 | const OLD_ENV = process.env; 10 | 11 | beforeEach(() => { 12 | jest.resetModules(); // Most important - it clears the cache 13 | process.env = { ...OLD_ENV }; // Make a copy 14 | }); 15 | 16 | afterAll(() => { 17 | process.env = OLD_ENV; // Restore old environment 18 | }); 19 | 20 | // test 21 | test('clone with token', () => { 22 | process.env.GITHUB_TOKEN = 'my-token'; 23 | 24 | git.clone('github.com/cdklabs/publib', 'target'); 25 | 26 | expect(mockedShellRun.mock.calls).toHaveLength(1); 27 | expect(mockedShellRun.mock.calls[0]).toEqual(['git clone https://my-token@github.com/cdklabs/publib.git target']); 28 | }); 29 | 30 | test('clone with ssh', () => { 31 | process.env.GITHUB_USE_SSH = '1'; 32 | 33 | git.clone('github.com/cdklabs/publib', 'target'); 34 | 35 | expect(mockedShellRun.mock.calls).toHaveLength(1); 36 | expect(mockedShellRun.mock.calls[0]).toEqual(['git clone git@github.com:cdklabs/publib.git target']); 37 | }); 38 | 39 | test('throw exception without token or ssh', () => { 40 | const t = () => git.clone('github.com/cdklabs/publib', 'target'); 41 | expect(t).toThrow('GITHUB_TOKEN env variable is required when GITHUB_USE_SSH env variable is not used'); 42 | }); 43 | 44 | test('throw exception without ghe authentication for github enterprise repo', () => { 45 | const t = () => git.clone('github.corporate-enterprise.com/cdklabs/publib', 'target'); 46 | expect(t).toThrow('GITHUB_TOKEN env variable is required when GITHUB_USE_SSH env variable is not used'); 47 | }); 48 | 49 | test('throw exception with incomplete ghe authentication for github enterprise repo', () => { 50 | process.env.GITHUB_ENTERPRISE_TOKEN = 'valid-token'; 51 | const t = () => git.clone('github.corporate-enterprise.com/cdklabs/publib', 'target'); 52 | expect(t).toThrow('GITHUB_TOKEN env variable is required when GITHUB_USE_SSH env variable is not used'); 53 | }); 54 | 55 | test('clone with provided ghe authentication for github enterprise repo but no set github api url', () => { 56 | process.env.GH_ENTERPRISE_TOKEN = 'valid-token'; 57 | process.env.GH_HOST = 'github.corporate-enterprise.com'; 58 | git.clone('github.corporate-enterprise.com/cdklabs/publib', 'target'); 59 | expect(mockedShellRun.mock.calls).toHaveLength(1); 60 | expect(mockedShellRun.mock.calls[0]).toEqual(['git clone https://valid-token@github.corporate-enterprise.com/cdklabs/publib.git target']); 61 | }); 62 | 63 | test('clone with provided ghe authentication for github enterprise repo and with non-public github api url', () => { 64 | process.env.GH_ENTERPRISE_TOKEN = 'valid-token'; 65 | process.env.GH_HOST = 'github.corporate-enterprise.com'; 66 | process.env.GITHUB_API_URL = 'https://api.github.corporate-enterprise.com'; 67 | git.clone('github.corporate-enterprise.com/cdklabs/publib', 'target'); 68 | expect(mockedShellRun.mock.calls).toHaveLength(1); 69 | expect(mockedShellRun.mock.calls[0]).toEqual(['git clone https://valid-token@github.corporate-enterprise.com/cdklabs/publib.git target']); 70 | }); 71 | -------------------------------------------------------------------------------- /test/targets/git.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as os from 'node:os'; 3 | import * as path from 'node:path'; 4 | import * as git from '../../src/help/git'; 5 | import * as shell from '../../src/help/shell'; 6 | 7 | let shellRunSpy: jest.SpyInstance>; 8 | beforeEach(() => { 9 | shellRunSpy = jest.spyOn(shell, 'run'); 10 | }); 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | 16 | test('checkout with createIfMissing', () => { 17 | withTmpDir(() => { 18 | git.init(); 19 | git.checkout('main', { createIfMissing: true }); 20 | }); 21 | expect(shellRunSpy.mock.calls).toHaveLength(3); // init, show-branch, checkout -B 22 | expect(shellRunSpy.mock.calls[2]).toEqual(['git checkout -B main']); 23 | }); 24 | 25 | function withTmpDir(fn: (tmpDir: string) => void) { 26 | const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); 27 | const cwd = process.cwd(); 28 | try { 29 | process.chdir(tmpDir); 30 | fn(tmpDir); 31 | } finally { 32 | fs.rmSync(tmpDir, { recursive: true, force: true }); 33 | process.chdir(cwd); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/targets/go.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import { GoReleaser, GoReleaserProps } from '../../src'; 4 | import * as git from '../../src/help/git'; 5 | import * as os from '../../src/help/os'; 6 | import * as shell from '../../src/help/shell'; 7 | 8 | interface Initializers { 9 | postInit?: (repoDir: string) => void; 10 | } 11 | // restore env after each test 12 | const OLD_ENV = process.env; 13 | 14 | beforeEach(() => { 15 | jest.resetModules(); // Most important - it clears the cache 16 | process.env = { ...OLD_ENV }; // Make a copy 17 | }); 18 | 19 | afterAll(() => { 20 | process.env = OLD_ENV; // Restore old environment 21 | }); 22 | 23 | function initRepo(repoDir: string, postInit?: (repoDir: string) => void) { 24 | const cwd = process.cwd(); 25 | try { 26 | process.chdir(repoDir); 27 | git.init(); 28 | git.add('.'); 29 | git.identify('publib-test', '<>'); 30 | git.commit('Initial Commit'); 31 | if (postInit) { postInit(repoDir); }; 32 | } finally { 33 | process.chdir(cwd); 34 | } 35 | } 36 | 37 | function createReleaser( 38 | fixture: string, 39 | props: Omit = {}, 40 | initializers: Initializers = {}, 41 | ) { 42 | 43 | const fixturePath = path.join(__dirname, '..', '__fixtures__', fixture); 44 | const sourceDir = path.join(os.mkdtempSync(), fixture); 45 | fs.copySync(fixturePath, sourceDir, { recursive: true }); 46 | 47 | // create the releaser with a copy of the fixture to allow 48 | // source customization. 49 | const releaser = new GoReleaser({ 50 | dir: sourceDir, 51 | dryRun: true, 52 | username: 'publib-tester', 53 | email: 'publib@test.com', 54 | ...props, 55 | }); 56 | 57 | (git as any).clone = function(_: string, targetDir: string) { 58 | // the cloned repo is always the original fixture. 59 | fs.copySync(fixturePath, targetDir, { recursive: true }); 60 | initRepo(targetDir, initializers.postInit); 61 | }; 62 | 63 | (git as any).checkout = function(branch: string, options: { createIfMissing?: boolean }) { 64 | // skip logic for comparing against remote since we don't have one 65 | if (options.createIfMissing) { 66 | shell.run(`git checkout -B ${branch}`); 67 | } else { 68 | shell.run(`git checkout ${branch}`); 69 | } 70 | }; 71 | 72 | return { releaser, sourceDir }; 73 | } 74 | 75 | test('top-level', () => { 76 | 77 | const { releaser, sourceDir } = createReleaser('top-level'); 78 | 79 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 80 | const release = releaser.release(); 81 | 82 | expect(release.tags).toEqual(['v1.1.0']); 83 | expect(release.commitMessage).toEqual('chore(release): v1.1.0'); 84 | 85 | }); 86 | 87 | test('sub-modules', () => { 88 | 89 | const { releaser, sourceDir } = createReleaser('sub-modules'); 90 | 91 | fs.writeFileSync(path.join(sourceDir, 'module1', 'file'), 'test'); 92 | const release = releaser.release(); 93 | 94 | expect(release.tags).toEqual(['module1/v1.1.0', 'module2/v1.1.0']); 95 | expect(release.commitMessage).toEqual('chore(release): v1.1.0'); 96 | 97 | }); 98 | 99 | test('combined', () => { 100 | 101 | const { releaser, sourceDir } = createReleaser('combined'); 102 | 103 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 104 | const release = releaser.release(); 105 | 106 | expect(release.tags).toEqual(['module1/v1.1.0', 'module2/v1.1.0', 'v1.1.0']); 107 | expect(release.commitMessage).toEqual('chore(release): v1.1.0'); 108 | 109 | }); 110 | 111 | test('multi-version', () => { 112 | 113 | const { releaser, sourceDir } = createReleaser('multi-version'); 114 | fs.writeFileSync(path.join(sourceDir, 'module1', 'file'), 'test'); 115 | 116 | const release = releaser.release(); 117 | 118 | expect(release.tags).toEqual(['module1/v1.1.0', 'module2/v1.2.0']); 119 | expect(release.commitMessage).toEqual('chore(release): module1@v1.1.0 module2@v1.2.0'); 120 | 121 | }); 122 | 123 | test('throws when submodules use multiple repos', () => { 124 | 125 | const { releaser } = createReleaser('multi-repo'); 126 | 127 | expect(() => releaser.release()).toThrow(/Multiple repositories found in module files/); 128 | 129 | }); 130 | 131 | test('throws when version file doesnt exist and no global version', () => { 132 | 133 | const { releaser, sourceDir } = createReleaser('no-version'); 134 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 135 | 136 | expect(() => releaser.release()).toThrow(/Unable to determine version of module/); 137 | 138 | }); 139 | 140 | test('uses global version', () => { 141 | 142 | const { releaser, sourceDir } = createReleaser('no-version', { version: '1.0.0' }); 143 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 144 | 145 | const release = releaser.release(); 146 | expect(release.tags).toEqual(['v1.0.0']); 147 | 148 | }); 149 | 150 | test('throws if module repo domain is not github.com without SSH', () => { 151 | 152 | const { releaser, sourceDir } = createReleaser('not-github'); 153 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 154 | expect(() => releaser.release()).toThrow(/Repository must be hosted on github.com/); 155 | 156 | }); 157 | 158 | test('does not throw if module repo domain is not github.com with SSH', () => { 159 | 160 | process.env.GITHUB_USE_SSH = '1'; 161 | const { releaser, sourceDir } = createReleaser('not-github'); 162 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 163 | const release = releaser.release(); 164 | 165 | expect(release.tags).toEqual(['v1.1.0']); 166 | 167 | }); 168 | 169 | test('throws if module repo domain is not github.com with incomplete GHE auth', () => { 170 | 171 | process.env.GH_ENTERPRISE_TOKEN = 'valid-token'; 172 | const { releaser, sourceDir } = createReleaser('github-enterprise'); 173 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 174 | expect(() => releaser.release()).toThrow(/Repository must be hosted on github.com/); 175 | 176 | }); 177 | 178 | test('does not throw if module repo domain is not github.com with complete GHE auth', () => { 179 | 180 | process.env.GH_ENTERPRISE_TOKEN = 'valid-token'; 181 | process.env.GH_HOST = 'github.corporate-enterprise.com'; 182 | 183 | process.env.GITHUB_API_URL = 'https://api.github.corporate-enterprise.com'; 184 | 185 | const { releaser, sourceDir } = createReleaser('github-enterprise'); 186 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 187 | const release = releaser.release(); 188 | 189 | expect(release.tags).toEqual(['v1.1.0']); 190 | 191 | }); 192 | 193 | 194 | test('considers deleted files', () => { 195 | 196 | const { releaser, sourceDir } = createReleaser('top-level'); 197 | 198 | fs.unlinkSync(path.join(sourceDir, 'source.go')); 199 | const release = releaser.release(); 200 | 201 | expect(release.tags).toEqual(['v1.1.0']); 202 | 203 | }); 204 | 205 | test('considers deleted modules', () => { 206 | 207 | const { releaser, sourceDir } = createReleaser('sub-modules'); 208 | 209 | fs.removeSync(path.join(sourceDir, 'module1')); 210 | const release = releaser.release(); 211 | 212 | expect(release.tags).toEqual(['module2/v1.1.0']); 213 | 214 | }); 215 | 216 | test('considers added files', () => { 217 | 218 | const { releaser, sourceDir } = createReleaser('top-level'); 219 | 220 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 221 | const release = releaser.release(); 222 | 223 | expect(release.tags).toEqual(['v1.1.0']); 224 | 225 | }); 226 | 227 | test('considers added modules', () => { 228 | 229 | const { releaser, sourceDir } = createReleaser('sub-modules'); 230 | 231 | const module3 = path.join(sourceDir, 'module3'); 232 | fs.mkdirSync(module3); 233 | fs.writeFileSync(path.join(module3, 'go.mod'), 'module github.com/aws/sub-modules/module3'); 234 | fs.writeFileSync(path.join(module3, 'version'), '1.0.0'); 235 | const release = releaser.release(); 236 | 237 | expect(release.tags).toEqual(['module1/v1.1.0', 'module2/v1.1.0', 'module3/v1.0.0']); 238 | 239 | }); 240 | 241 | test('skips when no changes', () => { 242 | 243 | const { releaser } = createReleaser('top-level'); 244 | const release = releaser.release(); 245 | 246 | expect(release.tags).toBeUndefined(); 247 | 248 | }); 249 | 250 | test('does not include major version suffix in tag names', () => { 251 | 252 | const { releaser, sourceDir } = createReleaser('major-version'); 253 | 254 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 255 | const release = releaser.release(); 256 | 257 | expect(release.tags).toEqual(['module1/v3.3.3', 'v3.3.3']); 258 | 259 | }); 260 | 261 | test('does not strip major version from package name in tag names', () => { 262 | 263 | const { releaser, sourceDir } = createReleaser('major-versionv3'); 264 | 265 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 266 | const release = releaser.release(); 267 | 268 | expect(release.tags).toEqual(['module1v3/v3.3.3', 'v3.3.3']); 269 | 270 | }); 271 | 272 | test('no-ops on a directory with no modules', () => { 273 | 274 | const { releaser } = createReleaser('no-modules'); 275 | const release = releaser.release(); 276 | 277 | expect(release.tags).toBeUndefined(); 278 | 279 | }); 280 | 281 | test('accepts a custom git identity', () => { 282 | 283 | const username = 'some-user'; 284 | const email = 'some-user@example.com'; 285 | const { releaser, sourceDir } = createReleaser('top-level', { username, email }); 286 | 287 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 288 | const release = releaser.release(); 289 | 290 | const lastCommit = shell.run('git log -1', { capture: true, cwd: release.repoDir }); 291 | expect(lastCommit).toContain(`Author: ${username} <${email}>`); 292 | 293 | }); 294 | 295 | test('throws when global version conflicts with version file', () => { 296 | 297 | const { releaser } = createReleaser('top-level', { version: '10.0.0' }); 298 | 299 | expect(() => releaser.release()).toThrow(/Repo version \(10.0.0\) conflicts with module version \(1.1.0\)/); 300 | 301 | }); 302 | 303 | test('throws when no major version suffix', () => { 304 | 305 | const { releaser } = createReleaser('no-major-version-suffix'); 306 | 307 | expect(() => releaser.release()).toThrow(/expected to end with '\/v3'/); 308 | 309 | }); 310 | 311 | test('skips when all tags already exist', () => { 312 | 313 | const { releaser, sourceDir } = createReleaser('sub-modules', {}, { 314 | postInit: (_: string) => { 315 | git.tag('module1/v1.1.0'); 316 | git.tag('module2/v1.1.0'); 317 | }, 318 | }); 319 | 320 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 321 | 322 | const release = releaser.release(); 323 | expect(release).toStrictEqual({}); 324 | 325 | }); 326 | 327 | test('creates missing tags only', () => { 328 | 329 | const { releaser, sourceDir } = createReleaser('sub-modules', {}, { 330 | postInit: (_: string) => { 331 | git.tag('module2/v1.1.0'); 332 | }, 333 | }); 334 | 335 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 336 | 337 | const release = releaser.release(); 338 | expect(release.tags).toEqual(['module1/v1.1.0']); 339 | 340 | }); 341 | 342 | test('releases on separate branch', () => { 343 | 344 | const { releaser, sourceDir } = createReleaser('top-level', { 345 | branch: 'boo', 346 | }); 347 | 348 | fs.writeFileSync(path.join(sourceDir, 'file'), 'test'); 349 | const release = releaser.release(); 350 | 351 | expect(release.tags).toEqual(['v1.1.0']); 352 | expect(release.commitMessage).toEqual('chore(release): v1.1.0'); 353 | 354 | }); 355 | -------------------------------------------------------------------------------- /test/with-temporary-directory.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | 5 | export async function withTemporaryDirectory(block: (dir: string) => Promise) { 6 | const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'publib-test')); 7 | 8 | try { 9 | await block(tmpDir); 10 | } finally { 11 | fs.rmSync(tmpDir, { force: true, recursive: true }); 12 | } 13 | } 14 | 15 | export async function inTemporaryDirectory(block: () => Promise) { 16 | return withTemporaryDirectory(async (dir) => { 17 | const origDir = process.cwd(); 18 | process.chdir(dir); 19 | 20 | try { 21 | await block(); 22 | } finally { 23 | process.chdir(origDir); 24 | } 25 | }); 26 | } -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2020" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2020", 27 | "skipLibCheck": true 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "test/**/*.ts", 32 | ".projenrc.ts", 33 | "projenrc/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2020" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2020", 29 | "skipLibCheck": true 30 | }, 31 | "include": [ 32 | "src/**/*.ts" 33 | ], 34 | "exclude": [] 35 | } 36 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.33" 3 | } 4 | --------------------------------------------------------------------------------