├── .babelrc.json ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── funding.yml └── workflows │ └── validate.yml ├── .gitignore ├── action.yml ├── build └── index.js ├── jest.config.json ├── license.txt ├── package-lock.json ├── package.json ├── readme.md ├── readme ├── banner.jpg ├── banner.psd ├── description.md ├── example.md ├── output.png └── tokenSteps │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ ├── 06.png │ ├── 07.png │ └── 08.png ├── src ├── index.js ├── lib │ ├── DescriptionProperty.js │ ├── HomepageProperty.js │ ├── KeywordsProperty.js │ ├── Property.js │ ├── chalk.js │ ├── esm │ │ ├── commit-from-action.js │ │ ├── get-boolean-action-input.js │ │ ├── has-content.js │ │ ├── read-file-string.js │ │ └── zahl.js │ ├── getBranchName.js │ ├── getCommitMessage.js │ ├── logError.js │ └── normalizeArray.js └── pullBody.hbs ├── tsconfig.json └── webpack.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "jaid" 4 | ] 5 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | extends browserslist-config-jaid-node -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = false 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.txt] 15 | trim_trailing_whitespace = false 16 | 17 | [*.hbs] 18 | trim_trailing_whitespace = false 19 | 20 | [/package.json] 21 | insert_final_newline = true # npm install rewrites the JSON file with a final newline, so we manually keep them to prevent unneeded stashes -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | /node_modules/ 3 | /test/fixtures/ 4 | /build/ 5 | /readme/*.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "jaid" 4 | ] 5 | } -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: Jaid 2 | custom: https://twitch.tv/products/jaidchen -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate and autofix 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: actions/checkout 10 | uses: actions/checkout@v2.3.4 11 | - name: npm install 12 | uses: jaid/action-npm-install@master 13 | - name: Jest 14 | uses: jaid/action-jest@master 15 | with: 16 | githubToken: ${{ secrets.GITHUB_TOKEN }} 17 | - name: Sync Node Meta 18 | uses: jaid/action-sync-node-meta@master 19 | with: 20 | githubToken: ${{ secrets.GITHUB_TOKEN }} 21 | - name: Uptodater 22 | uses: jaid/action-uptodater@master 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | fix: "true" 26 | approve: "true" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dotenv 2 | .env 3 | 4 | # Node packages 5 | node_modules 6 | 7 | # npm 8 | package-lock.json 9 | !/package-lock.json 10 | npm-debug.log 11 | npm-debug.log.* 12 | .npmrc 13 | /report.*.json 14 | 15 | # pnpm 16 | shrinkwrap.yaml 17 | !/shrinkwrap.yaml 18 | pnpm-debug.log 19 | 20 | # Yarn 21 | yarn.lock 22 | !/yarn.lock 23 | yarn-error.log 24 | 25 | # Generated or temporary content 26 | dist 27 | temp 28 | 29 | # IDEs 30 | /debug.log 31 | /.vscode/ 32 | /.idea/ 33 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Sync package.json with repo info 2 | description: GitHub Action that syncs package.json wpository metadata (description, homepage, topics/keywords). 3 | author: Jaid 4 | runs: 5 | using: node16 6 | main: build/index.js 7 | branding: 8 | icon: refresh-cw 9 | color: green 10 | inputs: 11 | githubToken: 12 | description: Repository token for allowing the action to make commits or change the repository info. If direction is "overwrite-file", this input be set from forwarding secrets.GITHUB_TOKEN in the workflow file. If direction is "overwrite-github", a custom personal access token with "repo" scope has to be created. 13 | required: true 14 | approve: 15 | description: If true and direction is "overwrite-file", pull requests created by this action are automatically approved and merged. 16 | default: "true" 17 | required: false 18 | removeBranch: 19 | description: If true and direction is "overwrite-file" and approve is also true, automatically merged pull requests will delete their branch afterwards. 20 | default: "true" 21 | required: false 22 | commitMessage: 23 | description: Commit message for package.json changes (only for direction "overwrite-file"). Substring “{changes}” will be replaced with a list of changed package.json fields. 24 | default: "autofix: Updated package.json[{changes}]" 25 | required: false 26 | syncDescription: 27 | description: If true, package.json[description] will be synced with GitHub repository description. 28 | default: "true" 29 | required: false 30 | syncHomepage: 31 | description: If true, package.json[homepage] will be synced with GitHub repository homepage. 32 | default: "true" 33 | required: false 34 | syncKeywords: 35 | description: If true, package.json[keywords] will be synced with GitHub repository topics. 36 | default: "true" 37 | required: false 38 | direction: 39 | description: The syncing direction, can be "overwrite-file" or "overwrite-github". If "overwrite-file", the file package.json will be edited in a pull request according to the GitHub repository info. If "overwrite-github", the GitHub repository info will be changed according to the content of the package.json file. 40 | default: "overwrite-file" 41 | required: false 42 | jsonFinalNewline: 43 | description: If true and direction is "overwrite-file", the updated package.json will have a final newline. 44 | default: "true" 45 | required: false 46 | branch: 47 | description: The name of the branch to make changes on (only for direction "overwrite-file"). Substring “{random}” will be replaced with randomized characters. 48 | default: action-sync-node-meta 49 | required: false -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "coverageDirectory": "/dist/jest/coverage", 4 | "collectCoverageFrom": [ 5 | "/dist/package/development/**", 6 | "!/node_modules/" 7 | ], 8 | "coverageReporters": [ 9 | "text-summary", 10 | "html" 11 | ], 12 | "testPathIgnorePatterns": [ 13 | "/node_modules/", 14 | "/dist/" 15 | ], 16 | "modulePathIgnorePatterns": [ 17 | "/dist", 18 | "/build" 19 | ], 20 | "moduleNameMapper": { 21 | "^root": "", 22 | "^src": "/src", 23 | "^lib": "/src/lib" 24 | }, 25 | "verbose": false 26 | } -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2021, Jaid (https://github.com/jaid) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "webpackConfigJaid": "githubAction", 4 | "version": "2.0.0", 5 | "author": "Jaid (https://github.com/Jaid)", 6 | "dependencies": { 7 | "@absolunet/fsp": "^1.7.0", 8 | "@actions/core": "^1.8.2", 9 | "@actions/github": "^5.0.3", 10 | "chalk": "^5.0.1", 11 | "commit-from-action": "^2.0.6", 12 | "detect-indent": "^7.0.0", 13 | "get-boolean-action-input": "^1.0.2", 14 | "has-content": "^1.1.1", 15 | "immer": "^9.0.14", 16 | "lodash": "^4.17.21", 17 | "purdy": "^3.5.1", 18 | "read-file-string": "^1.1.2", 19 | "readable-ms": "^2.0.4", 20 | "upper-case-first": "^2.0.2", 21 | "zahl": "^2.0.6" 22 | }, 23 | "devDependencies": { 24 | "babel-preset-jaid": "^14.0.0", 25 | "browserslist-config-jaid-node": "^3.0.0", 26 | "eslint": "^8.16.0", 27 | "eslint-config-jaid": "^1.59.1", 28 | "tsconfig-jaid": "^2.1.1", 29 | "webpack-config-jaid": "^16.1.1" 30 | }, 31 | "scripts": { 32 | "build": "rm -rf build && NODE_ENV=production webpack", 33 | "buildPush": "npm run build && git add build && git-flush-cli 'Rebuilt src/'", 34 | "testOnGithub": "name=$(package-name-cli) && git-flush-cli 'Testing changes' && npm run buildPush && cd ../test && git pull && echo $(date-now) >> changefile.txt && git-flush-cli \"Random commit for testing action Jaid/$name\" && cd ../$name", 35 | "prepareRelease": "npm run buildPush", 36 | "prepareActionJest": "npm run build:prod" 37 | }, 38 | "description": "GitHub Action that syncs package.json with the repository metadata.", 39 | "funding": "https://github.com/sponsors/jaid", 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/jaid/action-sync-node-meta" 43 | }, 44 | "name": "action-sync-node-meta", 45 | "homepage": "https://github.com/Jaid/action-sync-node-meta", 46 | "keywords": [ 47 | "action", 48 | "action-sync-node-meta", 49 | "actions", 50 | "bot", 51 | "github-action", 52 | "github-actions", 53 | "github-api", 54 | "metadata", 55 | "node", 56 | "node-js", 57 | "nodejs", 58 | "repository", 59 | "sync-node-meta", 60 | "util", 61 | "utility", 62 | "workflow" 63 | ], 64 | "type": "module" 65 | } 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # action-sync-node-meta 2 | 3 | 4 | License Sponsor action-sync-node-meta 5 | Build status Commits since v2.0.0 Last commit Issues 6 | 7 | **GitHub Action that syncs package.json with the repository metadata.** 8 | 9 | 10 | There are values that are meant to be the same. Why not automatically keep them synchronized? 11 | 12 | ![Banner](readme/banner.jpg) 13 | 14 | ### Example output 15 | 16 | ![Example output](readme/output.png) 17 | 18 | 19 | 20 | 21 | 22 | ## Example 23 | 24 | ### overwrite-file 25 | 26 | Example workflow that runs whenever commits are pushed on branch `master`. 27 | This will overwrite the `package.json` file if it differs from the GitHub repository info. 28 | 29 | This is the recommended syncing direction, because of the more simple setup (no need to manually add a secret to the repository settings) and the advantages of git commits (better monitoring, revertability). 30 | 31 | `.github/workflows/example.yml` 32 | ```yaml 33 | name: Sync package.json with repository info 34 | on: 35 | push: 36 | branches: [master] 37 | jobs: 38 | build: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: actions/checkout 42 | uses: actions/checkout@v2.3.4 43 | - name: Jaid/action-sync-node-meta 44 | uses: jaid/action-sync-node-meta@v2.0.0 45 | with: 46 | githubToken: ${{ secrets.GITHUB_TOKEN }} 47 | ``` 48 | 49 | ### overwrite-github 50 | 51 | Example workflow that runs whenever commits are pushed on branch `master`. 52 | This will change the GitHub repository info whenever it differs from the content of `package.json`. 53 | 54 | The secret `customGithubToken` is forwarded to the input `githubToken`. It has to be a [personal access token](https://github.com/settings/tokens) with scope "repo" added in [your repository's secrets settings](https://github.com/YOUR_NAME/YOUR_REPOSITORY/settings/secrets). 55 | 56 | `.github/workflows/example2.yml` 57 | ```yaml 58 | name: Sync repository info with package.json 59 | on: 60 | push: 61 | branches: [master] 62 | jobs: 63 | build: 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: actions/checkout 67 | uses: actions/checkout@v2.3.4 68 | - name: Jaid/action-sync-node-meta 69 | uses: jaid/action-sync-node-meta@v2.0.0 70 | with: 71 | direction: overwrite-github 72 | githubToken: ${{ secrets.customGithubToken }} 73 | ``` 74 | 75 |
76 | Detailed setup 77 | Go to your account settings and then to “Developer settings”. 78 | 79 | ![Token setup: Step 1](readme/tokenSteps/01.png) 80 | 81 | Go to “Personal access tokens”. 82 | 83 | ![Token setup: Step 2](readme/tokenSteps/02.png) 84 | 85 | Click “Generate new token”. 86 | 87 | ![Token setup: Step 3](readme/tokenSteps/03.png) 88 | 89 | Give it a good title, so you still know what your token does in one year. Add „repo“ permissions. 90 | 91 | ![Token setup: Step 4](readme/tokenSteps/04.png) 92 | 93 | Copy the generated token. 94 | 95 | ![Token setup: Step 5](readme/tokenSteps/05.png) 96 | 97 | Go to the repository that uses action-sync-node-meta. Go to “Settings”, “Secrets”. 98 | 99 | ![Token setup: Step 6](readme/tokenSteps/06.png) 100 | 101 | Click “New repository secret”. 102 | 103 | ![Token setup: Step 7](readme/tokenSteps/07.png) 104 | 105 | Add the secret token from your clipboard. Name the token “repoGithubToken” or anything you like. 106 | 107 | ![Token setup: Step 8](readme/tokenSteps/08.png) 108 | 109 | Now pass the token to action-sync-node-meta in your workflow file. 110 | 111 | ```yaml 112 | - name: Jaid/action-sync-node-meta 113 | uses: jaid/action-sync-node-meta@v2.0.0 114 | with: 115 | direction: overwrite-github 116 | githubToken: ${{ secrets.repoGithubToken }} 117 | ``` 118 | 119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | ## Options 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 |
DefaultInfo
githubToken*Repository token for allowing the action to make commits or change the repository info. If direction is "overwrite-file", this input be set from forwarding secrets.GITHUB_TOKEN in the workflow file. If direction is "overwrite-github", a custom personal access token with "repo" scope has to be created.
approvetrueIf true and direction is "overwrite-file", pull requests created by this action are automatically approved and merged.
branchaction-sync-node-metaThe name of the branch to make changes on (only for direction "overwrite-file"). Substring “{random}” will be replaced with randomized characters.
commitMessageautofix: Updated package.json[{changes}]Commit message for package.json changes (only for direction "overwrite-file"). Substring “{changes}” will be replaced with a list of changed package.json fields.
directionoverwrite-fileThe syncing direction, can be "overwrite-file" or "overwrite-github". If "overwrite-file", the file package.json will be edited in a pull request according to the GitHub repository info. If "overwrite-github", the GitHub repository info will be changed according to the content of the package.json file.
jsonFinalNewlinetrueIf true and direction is "overwrite-file", the updated package.json will have a final newline.
removeBranchtrueIf true and direction is "overwrite-file" and approve is also true, automatically merged pull requests will delete their branch afterwards.
syncDescriptiontrueIf true, package.json[description] will be synced with GitHub repository description.
syncHomepagetrueIf true, package.json[homepage] will be synced with GitHub repository homepage.
syncKeywordstrueIf true, package.json[keywords] will be synced with GitHub repository topics.
200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | ## Development 214 | 215 | 216 | 217 | Setting up: 218 | ```bash 219 | git clone git@github.com:jaid/action-sync-node-meta.git 220 | cd action-sync-node-meta 221 | npm install 222 | ``` 223 | 224 | 225 | ## License 226 | [MIT License](https://raw.githubusercontent.com/jaid/action-sync-node-meta/master/license.txt) 227 | Copyright © 2021, Jaid \ (https://github.com/jaid) 228 | 229 | -------------------------------------------------------------------------------- /readme/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/banner.jpg -------------------------------------------------------------------------------- /readme/banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/banner.psd -------------------------------------------------------------------------------- /readme/description.md: -------------------------------------------------------------------------------- 1 | There are values that are meant to be the same. Why not automatically keep them synchronized? 2 | 3 | ![Banner](readme/banner.jpg) 4 | 5 | ### Example output 6 | 7 | ![Example output](readme/output.png) -------------------------------------------------------------------------------- /readme/example.md: -------------------------------------------------------------------------------- 1 | ### overwrite-file 2 | 3 | Example workflow that runs whenever commits are pushed on branch `master`. 4 | This will overwrite the `package.json` file if it differs from the GitHub repository info. 5 | 6 | This is the recommended syncing direction, because of the more simple setup (no need to manually add a secret to the repository settings) and the advantages of git commits (better monitoring, revertability). 7 | 8 | `.github/workflows/example.yml` 9 | ```yaml 10 | name: Sync package.json with repository info 11 | on: 12 | push: 13 | branches: [master] 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: actions/checkout 19 | uses: actions/checkout@v2.3.4 20 | - name: Jaid/action-sync-node-meta 21 | uses: jaid/action-sync-node-meta@v1.4.0 22 | with: 23 | githubToken: ${{ secrets.GITHUB_TOKEN }} 24 | ``` 25 | 26 | ### overwrite-github 27 | 28 | Example workflow that runs whenever commits are pushed on branch `master`. 29 | This will change the GitHub repository info whenever it differs from the content of `package.json`. 30 | 31 | The secret `customGithubToken` is forwarded to the input `githubToken`. It has to be a [personal access token](https://github.com/settings/tokens) with scope "repo" added in [your repository's secrets settings](https://github.com/YOUR_NAME/YOUR_REPOSITORY/settings/secrets). 32 | 33 | `.github/workflows/example2.yml` 34 | ```yaml 35 | name: Sync repository info with package.json 36 | on: 37 | push: 38 | branches: [master] 39 | jobs: 40 | build: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: actions/checkout 44 | uses: actions/checkout@v2.3.4 45 | - name: Jaid/action-sync-node-meta 46 | uses: jaid/action-sync-node-meta@v1.4.0 47 | with: 48 | direction: overwrite-github 49 | githubToken: ${{ secrets.customGithubToken }} 50 | ``` 51 | 52 |
53 | Detailed setup 54 | Go to your account settings and then to “Developer settings”. 55 | 56 | ![Token setup: Step 1](readme/tokenSteps/01.png) 57 | 58 | Go to “Personal access tokens”. 59 | 60 | ![Token setup: Step 2](readme/tokenSteps/02.png) 61 | 62 | Click “Generate new token”. 63 | 64 | ![Token setup: Step 3](readme/tokenSteps/03.png) 65 | 66 | Give it a good title, so you still know what your token does in one year. Add „repo“ permissions. 67 | 68 | ![Token setup: Step 4](readme/tokenSteps/04.png) 69 | 70 | Copy the generated token. 71 | 72 | ![Token setup: Step 5](readme/tokenSteps/05.png) 73 | 74 | Go to the repository that uses action-sync-node-meta. Go to “Settings”, “Secrets”. 75 | 76 | ![Token setup: Step 6](readme/tokenSteps/06.png) 77 | 78 | Click “New repository secret”. 79 | 80 | ![Token setup: Step 7](readme/tokenSteps/07.png) 81 | 82 | Add the secret token from your clipboard. Name the token “repoGithubToken” or anything you like. 83 | 84 | ![Token setup: Step 8](readme/tokenSteps/08.png) 85 | 86 | Now pass the token to action-sync-node-meta in your workflow file. 87 | 88 | ```yaml 89 | - name: Jaid/action-sync-node-meta 90 | uses: jaid/action-sync-node-meta@v1.4.0 91 | with: 92 | direction: overwrite-github 93 | githubToken: ${{ secrets.repoGithubToken }} 94 | ``` 95 | 96 |
97 | -------------------------------------------------------------------------------- /readme/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/output.png -------------------------------------------------------------------------------- /readme/tokenSteps/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/01.png -------------------------------------------------------------------------------- /readme/tokenSteps/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/02.png -------------------------------------------------------------------------------- /readme/tokenSteps/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/03.png -------------------------------------------------------------------------------- /readme/tokenSteps/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/04.png -------------------------------------------------------------------------------- /readme/tokenSteps/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/05.png -------------------------------------------------------------------------------- /readme/tokenSteps/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/06.png -------------------------------------------------------------------------------- /readme/tokenSteps/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/07.png -------------------------------------------------------------------------------- /readme/tokenSteps/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaid/action-sync-node-meta/6eb235046d1bcfe3460260051044ca3bb455c5eb/readme/tokenSteps/08.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | 3 | import fsp from "@absolunet/fsp" 4 | import {debug, endGroup, getInput, info, setFailed, startGroup} from "@actions/core" 5 | import {context, getOctokit} from "@actions/github" 6 | import detectIndent from "detect-indent" 7 | import purdy from "purdy" 8 | import readFileString from "./lib/esm/read-file-string.js" 9 | 10 | import chalk from "./lib/chalk.js" 11 | import DescriptionProperty from "./lib/DescriptionProperty.js" 12 | import CommitManager from "./lib/esm/commit-from-action.js" 13 | import getActionBooleanInput from "./lib/esm/get-boolean-action-input.js" 14 | import hasContent from "./lib/esm/has-content.js" 15 | import zahl from "./lib/esm/zahl.js" 16 | import getBranchName from "./lib/getBranchName.js" 17 | import getCommitMessage from "./lib/getCommitMessage.js" 18 | import HomepageProperty from "./lib/HomepageProperty.js" 19 | import KeywordsProperty from "./lib/KeywordsProperty.js" 20 | import logError from "./lib/logError.js" 21 | import pullBody from "./pullBody.hbs" 22 | 23 | const githubToken = getInput("githubToken", {required: true}) 24 | // TODO Is preview mercy still needed? It was needed in April 2020. 25 | const octokit = getOctokit(githubToken, { 26 | previews: ["mercy"], // mercy preview gives us topics 27 | }).rest 28 | 29 | async function main() { 30 | let syncFailed = false 31 | const syncingDirection = getInput("direction", {required: true}).toLowerCase() 32 | const overwriteFile = syncingDirection === "overwrite-file" 33 | if (!overwriteFile && syncingDirection !== "overwrite-github") { 34 | throw new Error("Invalid direction input. Must be either \"overwrite-file\" or \"overwrite-github\".") 35 | } 36 | if (overwriteFile) { 37 | info("Syncing direction: GitHub repository info → package.json") 38 | } else { 39 | info("Syncing direction: package.json → GitHub repository info") 40 | } 41 | const pkgFile = path.resolve("package.json") 42 | const [repositoryResponse, pkgString] = await Promise.all([ 43 | octokit.repos.get(context.repo), 44 | readFileString(pkgFile), 45 | ]) 46 | if (!pkgString) { 47 | info("No package.json found, skipping") 48 | return 49 | } 50 | let pkg = JSON.parse(pkgString) 51 | debug(`Loaded ${zahl(pkg, "field")} from ${pkgFile}`) 52 | /** 53 | * @type {import("lib/Property").Repository} 54 | */ 55 | const repository = repositoryResponse.data 56 | const constructorContext = { 57 | repository, 58 | pkg, 59 | overwriteFile, 60 | } 61 | const propertyClasses = [DescriptionProperty, HomepageProperty, KeywordsProperty] 62 | const properties = propertyClasses.map(PropertyClass => new PropertyClass(constructorContext)) 63 | const enabledProperties = properties.filter(property => property.enabled) 64 | if (enabledProperties.length === 0) { 65 | throw new Error("None of the sync properties is enabled!") 66 | } 67 | const results = [] 68 | for (const property of properties) { 69 | const title = property.getTitle() 70 | const result = { 71 | property, 72 | title, 73 | } 74 | results.push(result) 75 | try { 76 | const pkgKey = property.getPkgKey() 77 | const pkgValue = property.getPkgValue() 78 | const repositoryKey = property.getRepositoryKey() 79 | const repositoryValue = property.getRepositoryValue() 80 | Object.assign(result, { 81 | property, 82 | pkgKey, 83 | pkgValue, 84 | repositoryKey, 85 | repositoryValue, 86 | enabled: property.enabled, 87 | }) 88 | if (!property.enabled) { 89 | continue 90 | } 91 | const isEqual = property.compare(pkgValue, repositoryValue) 92 | result.isEqual = isEqual 93 | if (!isEqual) { 94 | if (overwriteFile) { 95 | pkg = property.applyPkgUpdate(pkg, repositoryValue) 96 | } else { 97 | await property.applyGithubUpdate(octokit, context.repo, pkgValue) 98 | } 99 | } 100 | } catch (error) { 101 | result.error = error 102 | syncFailed = true 103 | } 104 | } 105 | const changedResults = results.filter(result => { 106 | if (!result.enabled) { 107 | return false 108 | } 109 | if (result.isEqual) { 110 | return false 111 | } 112 | return true 113 | }) 114 | if (overwriteFile && hasContent(changedResults)) { 115 | const indent = detectIndent(pkgString).indent || " " 116 | const json = JSON.stringify(pkg, null, indent) 117 | const jsonFinalNewline = getActionBooleanInput("jsonFinalNewline") 118 | const newContent = jsonFinalNewline ? `${json}\n` : json 119 | await fsp.outputFile(pkgFile, newContent) 120 | const changedKeys = changedResults.map(result => result.pkgKey) 121 | let commitManager 122 | try { 123 | commitManager = new CommitManager({ 124 | autoApprove: "approve", 125 | autoRemoveBranch: "removeBranch", 126 | branch: getBranchName(), 127 | pullRequestTitle: "Applied a fix from action-sync-node-meta", 128 | commitMessage: getCommitMessage(changedKeys), 129 | pullRequestBody: manager => pullBody({ 130 | ...context.repo, 131 | sha7: context.sha?.slice(0, 8), 132 | autoApprove: manager.autoApprove, 133 | sha: context.sha, 134 | actionRepo: "Jaid/action-sync-node-meta", 135 | actionPage: "https://github.com/marketplace/actions/sync-node-meta", 136 | branch: manager.branch, 137 | }), 138 | mergeMessage: manager => `Automatically merged Node metadata update from #${manager.pullNumber}`, 139 | }) 140 | await commitManager.push() 141 | } catch (error) { 142 | logError(error) 143 | syncFailed = true 144 | } finally { 145 | await commitManager.finalize() 146 | } 147 | } 148 | for (const result of results) { 149 | let color 150 | let suffix 151 | if (!result.enabled) { 152 | color = chalk.bgGray 153 | suffix = "disabled" 154 | } else if (result.error) { 155 | color = chalk.bgRed 156 | suffix = "failed" 157 | } else if (result.isEqual) { 158 | color = chalk.bgYellow 159 | suffix = "equal" 160 | } else { 161 | color = chalk.bgGreen 162 | suffix = "changed" 163 | } 164 | startGroup(color(` ${result.title.padEnd(30 - suffix.length)}${suffix} `)) 165 | const purdyOptions = {} 166 | info(`${chalk.cyan(`pkg.${result.pkgKey}:`)} ${purdy.stringify(result.pkgValue, purdyOptions)}`) 167 | info(`${chalk.cyan(`repository.${result.repositoryKey}:`)} ${purdy.stringify(result.repositoryValue, purdyOptions)}`) 168 | if (result.error) { 169 | logError(result.error) 170 | } else if (!result.enabled) { 171 | info("This sync has been disabled in workflow.") 172 | } else if (result.isEqual) { 173 | info("These values seem to be the same.") 174 | } else if (overwriteFile) { 175 | info(`They are not equal! Updated pkg.${result.pkgKey} value.`) 176 | } else { 177 | info(`They are not equal! Updating repository.${result.repositoryKey} value.`) 178 | } 179 | for (const logMessage of result.property.logMessages) { 180 | info(logMessage) 181 | } 182 | endGroup() 183 | } 184 | if (syncFailed) { 185 | throw new Error("Syncing failed") 186 | } 187 | } 188 | 189 | info(`${process.env.REPLACE_PKG_NAME} v${process.env.REPLACE_PKG_VERSION}`) 190 | 191 | main().catch(error => { 192 | setFailed("jaid/action-sync-node-meta threw an Error") 193 | logError(error) 194 | }) -------------------------------------------------------------------------------- /src/lib/DescriptionProperty.js: -------------------------------------------------------------------------------- 1 | import getActionBooleanInput from "./esm/get-boolean-action-input.js" 2 | import Property from "./Property.js" 3 | 4 | export default class DescriptionProperty extends Property { 5 | 6 | getPkgKey() { 7 | return "description" 8 | } 9 | 10 | getRepositoryValue() { 11 | const key = this.getRepositoryKey() 12 | return this.repository[key] || undefined 13 | } 14 | 15 | shouldSkip() { 16 | const syncDescription = getActionBooleanInput("syncDescription") 17 | if (!syncDescription) { 18 | return "input.syncDescription is false" 19 | } 20 | if (!this.overwriteFile && typeof this.pkg.description !== "string") { 21 | return "package.json[description] is not a string" 22 | } 23 | return false 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/lib/HomepageProperty.js: -------------------------------------------------------------------------------- 1 | import getActionBooleanInput from "./esm/get-boolean-action-input.js" 2 | import Property from "./Property.js" 3 | 4 | export default class HomepageProperty extends Property { 5 | 6 | getPkgKey() { 7 | return "homepage" 8 | } 9 | 10 | getRepositoryValue() { 11 | const key = this.getRepositoryKey() 12 | return this.repository[key] || undefined 13 | } 14 | 15 | shouldSkip() { 16 | const syncHomepage = getActionBooleanInput("syncHomepage") 17 | if (!syncHomepage) { 18 | return "input.syncHomepage is false" 19 | } 20 | if (!this.overwriteFile && typeof this.pkg.homepage !== "string") { 21 | return "package.json[homepage] is not a string" 22 | } 23 | return false 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/lib/KeywordsProperty.js: -------------------------------------------------------------------------------- 1 | import {isEqual} from "lodash" 2 | 3 | import getActionBooleanInput from "./esm/get-boolean-action-input.js" 4 | import normalizeArray from "./normalizeArray.js" 5 | import Property from "./Property.js" 6 | 7 | export default class KeywordsProperty extends Property { 8 | 9 | getPkgKey() { 10 | return "keywords" 11 | } 12 | 13 | getRepositoryKey() { 14 | return "topics" 15 | } 16 | 17 | getRepositoryValue() { 18 | const key = this.getRepositoryKey() 19 | return this.repository[key].length > 0 ? this.repository[key] : undefined 20 | } 21 | 22 | /** 23 | * @param {*} pkgValue 24 | * @param {*} repositoryValue 25 | */ 26 | compare(pkgValue, repositoryValue) { 27 | const pkgValueNormalized = normalizeArray(pkgValue) 28 | const repositoryValueNormalized = normalizeArray(repositoryValue) 29 | return isEqual(pkgValueNormalized, repositoryValueNormalized) 30 | } 31 | 32 | /** 33 | * @param {import("@octokit/rest").Octokit} octokit 34 | * @param {Object} repo 35 | * @param {string} repo.repo 36 | * @param {string} repo.owner 37 | * @param {*} pkgValue 38 | * @return {Promise} 39 | */ 40 | async applyGithubUpdate(octokit, repo, pkgValue) { 41 | const endpoint = "PUT /repos/:owner/:repo/topics" 42 | const options = { 43 | ...repo, 44 | names: normalizeArray(pkgValue), 45 | } 46 | await this.requestGithubApi(octokit, endpoint, options) 47 | } 48 | 49 | shouldSkip() { 50 | const syncKeywords = getActionBooleanInput("syncKeywords") 51 | if (!syncKeywords) { 52 | return "input.syncKeywords is false" 53 | } 54 | if (!this.overwriteFile && !Array.isArray(this.pkg.keywords)) { 55 | return "package.json[keywords] is not an array" 56 | } 57 | return false 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/lib/Property.js: -------------------------------------------------------------------------------- 1 | import immer from "immer" 2 | import {isEqual} from "lodash" 3 | import readableMs from "readable-ms" 4 | import {upperCaseFirst} from "upper-case-first" 5 | 6 | /** 7 | * @typedef {Object} ConstructorContext 8 | * @prop {Repository} repository 9 | * @prop {Pkg} pkg 10 | * @prop {boolean} overwriteFile 11 | */ 12 | 13 | /** 14 | * @typedef {Object} Repository 15 | * @prop {string} description 16 | * @prop {boolean} fork 17 | * @prop {string} full_name 18 | * @prop {string} homepage 19 | * @prop {string} html_url 20 | * @prop {License} license 21 | */ 22 | 23 | /** 24 | * @typedef {Object} License 25 | * @prop {string} key 26 | * @prop {string} name 27 | * @prop {string} node_id 28 | * @prop {string} spdx_id 29 | * @prop {string} url 30 | */ 31 | 32 | /** 33 | * @typedef {Object} Pkg 34 | * @prop {string} homepage 35 | * @prop {string} description 36 | * @prop {string[]} keywords 37 | */ 38 | 39 | /** 40 | * @class Property 41 | */ 42 | export default class Property { 43 | 44 | /** 45 | * @type {string[]} 46 | */ 47 | logMessages = [] 48 | 49 | /** 50 | * @type {boolean} 51 | */ 52 | enabled = true 53 | 54 | /** 55 | * @param {ConstructorContext} defaultValues 56 | */ 57 | constructor(defaultValues) { 58 | this.repository = defaultValues.repository 59 | this.pkg = defaultValues.pkg 60 | this.overwriteFile = defaultValues.overwriteFile 61 | const shouldSkip = this.shouldSkip() 62 | if (shouldSkip !== false) { 63 | this.log(`Syncing for this property is not enabled: ${shouldSkip}`) 64 | this.enabled = false 65 | } 66 | } 67 | 68 | /** 69 | * @return {string} 70 | */ 71 | getPkgKey() { 72 | throw new Error("This must be implemented by child class") 73 | } 74 | 75 | /** 76 | * @return {string} 77 | */ 78 | getRepositoryKey() { 79 | return this.getPkgKey() 80 | } 81 | 82 | /** 83 | * @return {*} 84 | */ 85 | getPkgValue() { 86 | const key = this.getPkgKey() 87 | return this.pkg[key] 88 | } 89 | 90 | /** 91 | * @return {*} 92 | */ 93 | getRepositoryValue() { 94 | throw new Error("This must be implemented by child class") 95 | } 96 | 97 | /** 98 | * @return {false|string} 99 | */ 100 | shouldSkip() { 101 | return false 102 | } 103 | 104 | /** 105 | * @return {string} 106 | */ 107 | getTitle() { 108 | const key = this.getPkgKey() 109 | return upperCaseFirst(key) 110 | } 111 | 112 | /** 113 | * @param {*} pkgValue 114 | * @param {*} repositoryValue 115 | */ 116 | compare(pkgValue, repositoryValue) { 117 | return isEqual(pkgValue, repositoryValue) 118 | } 119 | 120 | /** 121 | * @param {Pkg} pkgBefore 122 | * @param {*} repositoryValue 123 | * @return {Pkg} 124 | */ 125 | applyPkgUpdate(pkgBefore, repositoryValue) { 126 | const pkgKey = this.getPkgKey() 127 | return immer(pkgBefore, state => { 128 | if (repositoryValue === undefined) { 129 | delete state[pkgKey] 130 | } else { 131 | state[pkgKey] = repositoryValue 132 | } 133 | }) 134 | } 135 | 136 | /** 137 | * @param {string} message 138 | */ 139 | log(message) { 140 | this.logMessages.push(message) 141 | } 142 | 143 | /** 144 | * @param {import("@octokit/rest").Octokit} octokit 145 | * @param {string} endpoint 146 | * @param {Object} [options] 147 | * @return {Promise} 148 | */ 149 | async requestGithubApi(octokit, endpoint, options) { 150 | this.log(`API endpoint: ${endpoint}`) 151 | this.log(`API options: ${Object.keys(options).join(", ")}`) 152 | const startTime = Date.now() 153 | const result = await octokit.request(endpoint, options) 154 | const ms = Date.now() - startTime 155 | this.log(`${result.headers.status} in ${readableMs(ms)}`) 156 | } 157 | 158 | /** 159 | * @param {import("@octokit/rest").Octokit} octokit 160 | * @param {Object} repo 161 | * @param {string} repo.repo 162 | * @param {string} repo.owner 163 | * @param {*} pkgValue 164 | * @return {Promise} 165 | */ 166 | async applyGithubUpdate(octokit, repo, pkgValue) { 167 | const endpoint = "PATCH /repos/:owner/:repo" 168 | const options = { 169 | ...repo, 170 | [this.getRepositoryKey()]: pkgValue, 171 | } 172 | await this.requestGithubApi(octokit, endpoint, options) 173 | } 174 | 175 | } -------------------------------------------------------------------------------- /src/lib/chalk.js: -------------------------------------------------------------------------------- 1 | import {Chalk} from "chalk" 2 | 3 | // GitHub Actions CI supports color, chalk just does not know that 4 | // So we force a certain chalk level 5 | const customChalk = new Chalk({ 6 | level: 2, 7 | }) 8 | 9 | export default customChalk -------------------------------------------------------------------------------- /src/lib/esm/commit-from-action.js: -------------------------------------------------------------------------------- 1 | import commonJsModule from "commit-from-action" 2 | 3 | export default commonJsModule.default -------------------------------------------------------------------------------- /src/lib/esm/get-boolean-action-input.js: -------------------------------------------------------------------------------- 1 | import commonJsModule from "get-boolean-action-input" 2 | 3 | export default commonJsModule.default -------------------------------------------------------------------------------- /src/lib/esm/has-content.js: -------------------------------------------------------------------------------- 1 | import commonJsModule from "has-content" 2 | 3 | export default commonJsModule.default -------------------------------------------------------------------------------- /src/lib/esm/read-file-string.js: -------------------------------------------------------------------------------- 1 | import commonJsModule from "read-file-string" 2 | 3 | export default commonJsModule.default -------------------------------------------------------------------------------- /src/lib/esm/zahl.js: -------------------------------------------------------------------------------- 1 | import commonJsModule from "zahl" 2 | 3 | export default commonJsModule.default -------------------------------------------------------------------------------- /src/lib/getBranchName.js: -------------------------------------------------------------------------------- 1 | import {getInput} from "@actions/core" 2 | import {customAlphabet} from "nanoid" 3 | 4 | const idLength = 8 5 | const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", idLength) 6 | 7 | const replacedToken = "{random}" 8 | 9 | /** 10 | * @function 11 | * @return {string} 12 | */ 13 | export default () => { 14 | const input = getInput("branch") 15 | if (!input.includes(replacedToken)) { 16 | return input 17 | } 18 | const generatedId = nanoid() 19 | return input.replace(replacedToken, generatedId) 20 | } -------------------------------------------------------------------------------- /src/lib/getCommitMessage.js: -------------------------------------------------------------------------------- 1 | import {getInput} from "@actions/core" 2 | 3 | const replacedToken = "{changes}" 4 | 5 | /** 6 | * @function 7 | * @param {string[]} changedKeys 8 | * @return {string} 9 | */ 10 | export default changedKeys => { 11 | const input = getInput("commitMessage") 12 | return input.replace(replacedToken, changedKeys.join(", ")) 13 | } -------------------------------------------------------------------------------- /src/lib/logError.js: -------------------------------------------------------------------------------- 1 | import {error as consoleError} from "@actions/core" 2 | 3 | /** 4 | * @param {Error|string} error 5 | */ 6 | export default function logError(error) { 7 | if (error instanceof Error) { 8 | consoleError(error.stack) 9 | } else { 10 | consoleError(error) 11 | } 12 | } -------------------------------------------------------------------------------- /src/lib/normalizeArray.js: -------------------------------------------------------------------------------- 1 | import {sortBy, uniq} from "lodash" 2 | 3 | /** 4 | * @param {*} input 5 | */ 6 | export default input => { 7 | if (!Array.isArray(input)) { 8 | return input 9 | } 10 | return sortBy(uniq(input)) 11 | } -------------------------------------------------------------------------------- /src/pullBody.hbs: -------------------------------------------------------------------------------- 1 | {{#if sha}} 2 | Hewwo! I am [{{actionRepo}}]({{actionPage}}) and run for commit [`{{sha7}}`](https://github.com/{{owner}}/{{repo}}/commit/{{sha}}/checks). 3 | {{else}} 4 | Hewwo! I am [{{actionRepo}}]({{actionPage}})! 5 | {{/if}} 6 | 7 | Here are updates I am intending to apply. 8 | {{#if autoApprove}} 9 | Auto approving pull requests is activated. Branch `{{branch}}` will be automatically deleted afterwards. 10 | {{/if}} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig-jaid/base", 3 | "include": [ 4 | "src/**/*.js", 5 | "src/**/*.ts", 6 | "node_modules/webpack-config-jaid/assets.d.ts" 7 | ], 8 | "typeAcquisition": { 9 | "enable": true 10 | }, 11 | "compilerOptions": { 12 | "baseUrl": "." 13 | } 14 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import configure from "webpack-config-jaid" 2 | 3 | export default configure() --------------------------------------------------------------------------------