├── .eslintrc ├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── README.md ├── create-or-update-files.js ├── create-or-update-files.test.js ├── index.js ├── package-lock.json └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2020, 4 | "impliedStrict": true, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "impliedStrict": true, 8 | "experimentalObjectRestSpread": true 9 | } 10 | }, 11 | "plugins": [ 12 | "prettier" 13 | ], 14 | "rules": { 15 | "prettier/prettier": "error" 16 | } 17 | } -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Upgraded Prettier 2 | 770f9ed5831744b2616ac781a9afbe8cab66687e 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | production-dependencies: 9 | dependency-type: "production" 10 | update-types: 11 | - "minor" 12 | - "patch" 13 | development-dependencies: 14 | dependency-type: "development" 15 | update-types: 16 | - "minor" 17 | - "patch" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x, 18.x] 12 | 13 | env: 14 | CI: true 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm ci 22 | - run: npm test 23 | - run: npm run lint 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | ref: ${{ github.event.release.target_commitish }} 13 | - name: Use Node.js 18 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 18 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm ci 19 | - run: git config --global user.name "Michael Heap" 20 | - run: git config --global user.email "m@michaelheap.com" 21 | - run: npm version ${{ github.event.release.tag_name }} 22 | - run: npm run build --if-present 23 | - run: npm test --if-present 24 | - run: npm publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | - run: git push 28 | env: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | - run: git tag -f ${{ github.event.release.tag_name }} ${{ github.event.release.target_commitish }} 31 | env: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | - run: git push origin ${{ github.event.release.tag_name }} --force 34 | env: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # octokit-commit-multiple-files 2 | 3 | This plugin is an alternative to using `octokit.rest.repos.createOrUpdateFile` which allows you to edit the contents of a single file. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install octokit-commit-multiple-files --save 9 | ``` 10 | 11 | ## Usage 12 | 13 | This plugin accepts `owner`, `repo`, `path` and `branch` like `.createOrUpdateFile` ([Octokit Docs](https://octokit.github.io/rest.js/v18#repos-create-or-update-file)). 14 | 15 | If the `branch` provided does not exist, the plugin will error. To automatically create it, set `createBranch` to true. You may provide a `base` branch if you choose to do this, or the plugin will use the repo's default branch as the base. 16 | 17 | In addition, it accepts `changes` which is an array of objects containing a `message` and a `files` object 18 | 19 | ```javascript 20 | let { Octokit } = require("@octokit/rest"); 21 | Octokit = Octokit.plugin(require("octokit-commit-multiple-files")); 22 | 23 | const octokit = new Octokit(); 24 | 25 | const commits = await octokit.createOrUpdateFiles({ 26 | owner, 27 | repo, 28 | branch, 29 | createBranch, 30 | changes: [ 31 | { 32 | message: "Your commit message", 33 | files: { 34 | "test.md": `# This is a test 35 | 36 | I hope it works`, 37 | "test2.md": { 38 | contents: `Something else`, 39 | }, 40 | }, 41 | }, 42 | { 43 | message: "This is a separate commit", 44 | files: { 45 | "second.md": "Where should we go today?", 46 | }, 47 | }, 48 | ], 49 | }); 50 | ``` 51 | 52 | If you want to upload non-text data, you can `base64`` encode the content and provide that as the value. Here's an example that would upload a small GitHub icon to a repository: 53 | 54 | ```javascript 55 | const commits = await octokit.createOrUpdateFiles({ 56 | owner, 57 | repo, 58 | branch, 59 | createBranch, 60 | changes: [ 61 | { 62 | message: "Add Icon", 63 | files: { 64 | "icon.png": `iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADFUlEQVR42u2WXyjzYRTHvz/N3zZ/5sJyoWEUEleKrYbcaEXKXCBWVsrFkii3LrVwo6Sslj/hhqS4cecOtcWFtUbI34RlNn83786p1fu+F+9+a/Ou982pp56e3+l3Ps855/k+j6BWqwMABMTBPoMmBAE+4xE8ZN8A3wCiAYqKinB8fAy/3/9Hv6ysLCQnJ+P6+jp2AEqlEvPz87i/v8fc3By2t7dRUFCAzMxM/k7rNDo7O1FbWwu73Q6TyRQ7gOrqapjNZtFpvby8RFtbW+wAVCoVrFaraICTkxPORswAhoaG0NzcLBqAbGxsDKurq9EDyGQyrK+vQyKRRARwdnaG9vb26AHKy8sxNTXF8/39fSwsLODx8RGpqalIS0sjPYfP58Pz8zOys7NhMBj4xNB6XV0dPj4+ogMoKyvD9PQ0bm5u0NHRgZaWFg5ssVh+8aOaJyUlYWVlBUtLS5BKpXwiogaQy+VYW1vDxsYGxsfHUV9fj6urK9ze3uLi4oJ9UlJSoNVqcXd3x6kfHBzkzDU2NkZeAhKShIQETiH9mHY7MjKC8/NzDA8Pw9DdDc/TEzY3Nznt5CcIArq6urgU1C8TExO8To1IPjTIh9ZIL8ICkDMNr9eLl5cX3g39rK+vj1NKYA6HgwXq7e2Nz31+fj6X4PX1lcvT39+Pg4MDVkUqBwWn8fDwEFkJEhMT8f7+jtLSUhiNRjidTuzu7rLUymTSIJAfHo+HS1VRUYGamhrMzs5ib28vbPpFAVDNqRFnZmY4lQqFAg0NDejt7eUskdHOSKi2trZwenoqKrBoALKenh4EAgEsLi7yZTQ6OoqqqiqeU3DSiJ2dHQwMDEQUXDRAeno6JicnuSGpLwoLC2Gz2ZCTk8NZcbvdyM3NhV6v/xqAEASda2oq6niqcUlJCQMcHR2hsrISra2tXwfwsy0vL+Pw8BDFxcXc9X8VgBqPhIlqTgB0QlwuFzQaDZqamsI+WKIG0Ol0fOYzMjJY+UgH8vLyWKpJDwjuSwFCRi8ieqL9Po/U/p1H6TfA/wsQvL0CQuhWiYP9AJQGkyweNFh0AAAAAElFTkSuQmCC` 65 | } 66 | }, 67 | ], 68 | }); 69 | ``` 70 | 71 | By default the plugin will append commits to an existing branch. If you want to reset the branch to the state of `base` before adding any changes, set `forkFromBaseBranch: true`: 72 | 73 | ```javascript 74 | const commits = await octokit.rest.repos.createOrUpdateFiles({ 75 | owner, 76 | repo, 77 | branch, 78 | createBranch: true, 79 | base: "main", 80 | forkFromBaseBranch: true 81 | changes: [ 82 | { 83 | message: "Your commit message", 84 | files: { 85 | "test.md": `This example wiped out any previous commits on 'branch' before adding this change`, 86 | }, 87 | }, 88 | ], 89 | }); 90 | ``` 91 | 92 | In addition, you can set the `mode` of a file change. For example, if you wanted to update a submodule pointer: 93 | 94 | ```javascript 95 | { 96 | "message": "This is a submodule commit", 97 | "files": { 98 | "my_submodule": { 99 | "contents": "your-commit-sha", 100 | "mode": "160000", 101 | "type": "commit" 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | In addition, you can set the `filesToDelete` property as an array of strings (file paths) to set files for deletion in a single commit (works with updates and creations). 108 | 109 | ```javascript 110 | { 111 | "message": "This commit removes files", 112 | "filesToDelete": ['path/to/my/file.txt', 'path/to/another.js'], 113 | } 114 | ``` 115 | 116 | - Note that the `ignoreDeletionFailures` property is set to false by default (works in a context of a single change). 117 | - If `ignoreDeletionFailures` is set to false, an error will be thrown if any file set for deletion is missing and the commits will stop processing. Any commits made before this will still be applied. Any changes in this `change` will not be committed. No future changes will be applied. 118 | - If `ignoreDeletionFailures` is set to true, missing files that are set for deletion will be ignored. 119 | - If a file is created and deleted in the same `change`, the file will be created/updated 120 | 121 | ```javascript 122 | { 123 | "message": "This commit removes files", 124 | "filesToDelete": ['path/to/my/file.txt', 'path/to/another.js'], 125 | "ignoreDeletionFailures": true, 126 | } 127 | ``` 128 | 129 | - If `batchSize` is set, then file deletions and file uploads will use batched concurrent requests as opposed to iterating through them. This can be helpful for uploading many small files. Beware of your Github usage limits. 130 | 131 | ```javascript 132 | { 133 | "batchSize": 10 134 | } 135 | -------------------------------------------------------------------------------- /create-or-update-files.js: -------------------------------------------------------------------------------- 1 | function isBase64(str) { 2 | // Handle buffer inputs 3 | if (Buffer.isBuffer(str)) { 4 | str = str.toString("utf8"); 5 | } 6 | 7 | var notBase64 = /[^A-Z0-9+\/=]/i; 8 | const isString = typeof str === "string" || str instanceof String; 9 | 10 | if (!isString) { 11 | let invalidType; 12 | if (str === null) { 13 | invalidType = "null"; 14 | } else { 15 | invalidType = typeof str; 16 | if ( 17 | invalidType === "object" && 18 | str.constructor && 19 | str.constructor.hasOwnProperty("name") 20 | ) { 21 | invalidType = str.constructor.name; 22 | } else { 23 | invalidType = `a ${invalidType}`; 24 | } 25 | } 26 | throw new TypeError(`Expected string but received ${invalidType}.`); 27 | } 28 | 29 | const len = str.length; 30 | if (!len || len % 4 !== 0 || notBase64.test(str)) { 31 | return false; 32 | } 33 | const firstPaddingChar = str.indexOf("="); 34 | return ( 35 | firstPaddingChar === -1 || 36 | firstPaddingChar === len - 1 || 37 | (firstPaddingChar === len - 2 && str[len - 1] === "=") 38 | ); 39 | } 40 | 41 | module.exports = function (octokit, opts) { 42 | return new Promise(async (resolve, reject) => { 43 | // Up front validation 44 | try { 45 | for (const req of ["owner", "repo", "branch"]) { 46 | if (!opts[req]) { 47 | return reject(`'${req}' is a required parameter`); 48 | } 49 | } 50 | 51 | if (!opts.changes || !opts.changes.length) { 52 | return reject("No changes provided"); 53 | } 54 | 55 | if (!opts.batchSize) { 56 | opts.batchSize = 1; 57 | } 58 | 59 | if (typeof opts.batchSize !== "number") { 60 | return reject(`batchSize must be a number`); 61 | } 62 | 63 | // Destructuring for easier access later 64 | let { 65 | owner, 66 | repo, 67 | base, 68 | branch: branchName, 69 | createBranch, 70 | committer, 71 | author, 72 | changes, 73 | batchSize, 74 | forkFromBaseBranch, 75 | } = opts; 76 | 77 | let branchAlreadyExists = true; 78 | let baseTree; 79 | 80 | // Does the target branch already exist? 81 | baseTree = await loadRef(octokit, owner, repo, branchName); 82 | if (!baseTree || forkFromBaseBranch) { 83 | if (!createBranch && !baseTree) { 84 | return reject( 85 | `The branch '${branchName}' doesn't exist and createBranch is 'false'`, 86 | ); 87 | } 88 | 89 | if (!baseTree) { 90 | branchAlreadyExists = false; 91 | } 92 | 93 | // If not we use the base branch. If not provided, use the 94 | // default from the repo 95 | if (!base) { 96 | // Work out the default branch 97 | base = ( 98 | await octokit.rest.repos.get({ 99 | owner, 100 | repo, 101 | }) 102 | ).data.default_branch; 103 | } 104 | 105 | baseTree = await loadRef(octokit, owner, repo, base); 106 | 107 | if (!baseTree) { 108 | return reject(`The branch '${base}' doesn't exist`); 109 | } 110 | } 111 | 112 | // Create blobs 113 | const commits = []; 114 | for (const change of changes) { 115 | const message = change.message; 116 | if (!message) { 117 | return reject(`changes[].message is a required parameter`); 118 | } 119 | 120 | const hasFiles = change.files && Object.keys(change.files).length > 0; 121 | 122 | const hasFilesToDelete = 123 | Array.isArray(change.filesToDelete) && 124 | change.filesToDelete.length > 0; 125 | 126 | if (!hasFiles && !hasFilesToDelete) { 127 | return reject( 128 | `either changes[].files or changes[].filesToDelete are required`, 129 | ); 130 | } 131 | 132 | const treeItems = []; 133 | // Handle file deletions 134 | if (hasFilesToDelete) { 135 | for (const batch of chunk(change.filesToDelete, batchSize)) { 136 | await Promise.all( 137 | batch.map(async (fileName) => { 138 | const exists = await fileExistsInRepo( 139 | octokit, 140 | owner, 141 | repo, 142 | fileName, 143 | baseTree, 144 | ); 145 | 146 | // If it doesn't exist, and we're not ignoring missing files 147 | // reject the promise 148 | if (!exists && !change.ignoreDeletionFailures) { 149 | return reject( 150 | `The file ${fileName} could not be found in the repo`, 151 | ); 152 | } 153 | 154 | // At this point it either exists, or we're ignoring failures 155 | if (exists) { 156 | treeItems.push({ 157 | path: fileName, 158 | sha: null, // sha as null implies that the file should be deleted 159 | mode: "100644", 160 | type: "commit", 161 | }); 162 | } 163 | }), 164 | ); 165 | } 166 | } 167 | 168 | if (hasFiles) { 169 | for (const batch of chunk(Object.keys(change.files), batchSize)) { 170 | await Promise.all( 171 | batch.map(async (fileName) => { 172 | const properties = change.files[fileName] || ""; 173 | 174 | const contents = properties.contents || properties; 175 | const mode = properties.mode || "100644"; 176 | const type = properties.type || "blob"; 177 | 178 | if (!contents) { 179 | return reject(`No file contents provided for ${fileName}`); 180 | } 181 | 182 | const fileSha = await createBlob( 183 | octokit, 184 | owner, 185 | repo, 186 | contents, 187 | type, 188 | ); 189 | 190 | treeItems.push({ 191 | path: fileName, 192 | sha: fileSha, 193 | mode: mode, 194 | type: type, 195 | }); 196 | }), 197 | ); 198 | } 199 | } 200 | 201 | // no need to issue further requests if there are no updates, creations and deletions 202 | if (treeItems.length === 0) { 203 | continue; 204 | } 205 | 206 | // Add those blobs to a tree 207 | const tree = await createTree( 208 | octokit, 209 | owner, 210 | repo, 211 | treeItems, 212 | baseTree, 213 | ); 214 | 215 | // Create a commit that points to that tree 216 | const commit = await createCommit( 217 | octokit, 218 | owner, 219 | repo, 220 | committer, 221 | author, 222 | message, 223 | tree, 224 | baseTree, 225 | ); 226 | 227 | // Update the base tree if we have another commit to make 228 | baseTree = commit.sha; 229 | commits.push(commit); 230 | } 231 | 232 | // Create a ref that points to that tree 233 | let action = "createRef"; 234 | let updateRefBase = "refs/"; 235 | 236 | // Or if it already exists, we'll update that existing ref 237 | if (branchAlreadyExists) { 238 | action = "updateRef"; 239 | updateRefBase = ""; 240 | } 241 | 242 | await octokit.rest.git[action]({ 243 | owner, 244 | repo, 245 | force: true, 246 | ref: `${updateRefBase}heads/${branchName}`, 247 | sha: baseTree, 248 | }); 249 | 250 | // Return the new branch name so that we can use it later 251 | // e.g. to create a pull request 252 | return resolve({ commits }); 253 | } catch (e) { 254 | return reject(e); 255 | } 256 | }); 257 | }; 258 | 259 | async function fileExistsInRepo(octokit, owner, repo, path, branch) { 260 | try { 261 | await octokit.rest.repos.getContent({ 262 | method: "HEAD", 263 | owner, 264 | repo, 265 | path, 266 | ref: branch, 267 | }); 268 | return true; 269 | } catch (e) { 270 | return false; 271 | } 272 | } 273 | 274 | async function createCommit( 275 | octokit, 276 | owner, 277 | repo, 278 | committer, 279 | author, 280 | message, 281 | tree, 282 | baseTree, 283 | ) { 284 | return ( 285 | await octokit.rest.git.createCommit({ 286 | owner, 287 | repo, 288 | message, 289 | committer, 290 | author, 291 | tree: tree.sha, 292 | parents: [baseTree], 293 | }) 294 | ).data; 295 | } 296 | 297 | async function createTree(octokit, owner, repo, treeItems, baseTree) { 298 | return ( 299 | await octokit.rest.git.createTree({ 300 | owner, 301 | repo, 302 | tree: treeItems, 303 | base_tree: baseTree, 304 | }) 305 | ).data; 306 | } 307 | 308 | async function createBlob(octokit, owner, repo, contents, type) { 309 | if (type === "commit") { 310 | return contents; 311 | } else { 312 | let content = contents; 313 | 314 | if (!isBase64(content)) { 315 | content = Buffer.from(contents).toString("base64"); 316 | } 317 | 318 | const file = ( 319 | await octokit.rest.git.createBlob({ 320 | owner, 321 | repo, 322 | content, 323 | encoding: "base64", 324 | }) 325 | ).data; 326 | return file.sha; 327 | } 328 | } 329 | 330 | async function loadRef(octokit, owner, repo, ref) { 331 | try { 332 | const x = await octokit.rest.git.getRef({ 333 | owner, 334 | repo, 335 | ref: `heads/${ref}`, 336 | }); 337 | return x.data.object.sha; 338 | } catch (e) { 339 | // console.log(e); 340 | } 341 | } 342 | 343 | const chunk = (input, size) => { 344 | return input.reduce((arr, item, idx) => { 345 | return idx % size === 0 346 | ? [...arr, [item]] 347 | : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]]; 348 | }, []); 349 | }; 350 | -------------------------------------------------------------------------------- /create-or-update-files.test.js: -------------------------------------------------------------------------------- 1 | const plugin = require("./create-or-update-files"); 2 | const { Octokit } = require("@octokit/rest"); 3 | 4 | const nock = require("nock"); 5 | nock.disableNetConnect(); 6 | const octokit = new Octokit(); 7 | 8 | function run(body) { 9 | return plugin(octokit, body); 10 | } 11 | 12 | const validRequest = { 13 | owner: "mheap", 14 | repo: "test-repo", 15 | branch: "new-branch-name", 16 | createBranch: true, 17 | base: "base-branch-name", 18 | changes: [ 19 | { 20 | message: "Your commit message", 21 | files: { 22 | "test.md": `# This is a test 23 | 24 | I hope it works`, 25 | "test2.md": { 26 | contents: `Something else`, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }; 32 | 33 | // Destructuring for easier access later 34 | let { owner, repo, base, branch } = validRequest; 35 | const mockCommitList = { 36 | commits: [{ sha: "ef105a72c03ce2743d90944c2977b1b5563b43c0" }], 37 | }; 38 | const mockSecondCommitList = { 39 | commits: [{ sha: "45d77edc93556e3a997bf73d5ed4d9fb57068928" }], 40 | }; 41 | const mockSubmoduleCommitList = { 42 | commits: [{ sha: "ef105a72c03ce2743d90944c2977b1b5563b43c0" }], 43 | }; 44 | 45 | for (let req of ["owner", "repo", "branch"]) { 46 | const body = { ...validRequest }; 47 | delete body[req]; 48 | test(`missing parameter (${req})`, () => { 49 | expect(run(body)).rejects.toEqual(`'${req}' is a required parameter`); 50 | }); 51 | } 52 | 53 | test(`missing parameter (changes)`, () => { 54 | const body = { ...validRequest }; 55 | delete body["changes"]; 56 | expect(run(body)).rejects.toEqual(`No changes provided`); 57 | }); 58 | 59 | test(`empty parameter (changes)`, () => { 60 | const body = { ...validRequest, changes: [] }; 61 | expect(run(body)).rejects.toEqual(`No changes provided`); 62 | }); 63 | 64 | test(`non-number batchSize (changes)`, () => { 65 | const body = { ...validRequest, batchSize: "5" }; 66 | expect(run(body)).rejects.toEqual(`batchSize must be a number`); 67 | }); 68 | 69 | test(`branch does not exist, createBranch false`, async () => { 70 | mockGetRef(branch, `sha-${branch}`, false); 71 | const body = { ...validRequest, createBranch: false }; 72 | 73 | await expect(run(body)).rejects.toEqual( 74 | `The branch 'new-branch-name' doesn't exist and createBranch is 'false'`, 75 | ); 76 | }); 77 | 78 | test(`branch does not exist, provided base does not exist`, async () => { 79 | mockGetRef(branch, `sha-${branch}`, false); 80 | mockGetRef(base, `sha-${base}`, false); 81 | const body = { ...validRequest }; 82 | 83 | await expect(run(body)).rejects.toEqual( 84 | `The branch 'base-branch-name' doesn't exist`, 85 | ); 86 | }); 87 | 88 | test(`no commit message`, async () => { 89 | const repoDefaultBranch = "master"; 90 | mockGetRef(branch, `sha-${branch}`, true); 91 | mockGetRef(base, `sha-${base}`, true); 92 | mockGetRef(repoDefaultBranch, `sha-${repoDefaultBranch}`, true); 93 | 94 | const body = { 95 | ...validRequest, 96 | changes: [ 97 | { 98 | files: { 99 | "test.md": null, 100 | }, 101 | }, 102 | ], 103 | }; 104 | await expect(run(body)).rejects.toEqual( 105 | `changes[].message is a required parameter`, 106 | ); 107 | }); 108 | 109 | test(`no files provided (empty object)`, async () => { 110 | const repoDefaultBranch = "master"; 111 | mockGetRef(branch, `sha-${branch}`, true); 112 | mockGetRef(base, `sha-${base}`, true); 113 | mockGetRef(repoDefaultBranch, `sha-${repoDefaultBranch}`, true); 114 | 115 | const body = { 116 | ...validRequest, 117 | changes: [{ message: "Test Commit", files: {} }], 118 | }; 119 | await expect(run(body)).rejects.toEqual( 120 | `either changes[].files or changes[].filesToDelete are required`, 121 | ); 122 | }); 123 | 124 | test(`no files provided (missing object)`, async () => { 125 | const repoDefaultBranch = "master"; 126 | mockGetRef(branch, `sha-${branch}`, true); 127 | mockGetRef(base, `sha-${base}`, true); 128 | mockGetRef(repoDefaultBranch, `sha-${repoDefaultBranch}`, true); 129 | 130 | const body = { ...validRequest, changes: [{ message: "Test Commit" }] }; 131 | await expect(run(body)).rejects.toEqual( 132 | `either changes[].files or changes[].filesToDelete are required`, 133 | ); 134 | }); 135 | 136 | test(`no file contents provided`, async () => { 137 | const repoDefaultBranch = "master"; 138 | mockGetRef(branch, `sha-${branch}`, true); 139 | mockGetRef(base, `sha-${base}`, true); 140 | mockGetRef(repoDefaultBranch, `sha-${repoDefaultBranch}`, true); 141 | 142 | const body = { 143 | ...validRequest, 144 | changes: [ 145 | { 146 | message: "This is a test", 147 | files: { 148 | "test.md": null, 149 | }, 150 | }, 151 | ], 152 | }; 153 | await expect(run(body)).rejects.toEqual( 154 | `No file contents provided for test.md`, 155 | ); 156 | }); 157 | 158 | test(`success (submodule, branch exists)`, async () => { 159 | const body = { 160 | ...validRequest, 161 | changes: [ 162 | { 163 | message: "Your submodule commit message", 164 | files: { 165 | my_submodule: { 166 | contents: "new-submodule-sha", 167 | mode: "160000", 168 | type: "commit", 169 | }, 170 | }, 171 | }, 172 | ], 173 | }; 174 | 175 | mockGetRef(branch, `sha-${branch}`, true); 176 | mockCreateTreeSubmodule(`sha-${branch}`); 177 | mockCommitSubmodule(`sha-${branch}`); 178 | mockUpdateRef(branch); 179 | 180 | await expect(run(body)).resolves.toEqual(mockSubmoduleCommitList); 181 | }); 182 | 183 | test(`success (branch exists)`, async () => { 184 | const body = { 185 | ...validRequest, 186 | }; 187 | mockGetRef(branch, `sha-${branch}`, true); 188 | mockCreateBlobFileOne(); 189 | mockCreateBlobFileTwo(); 190 | mockCreateTree(`sha-${branch}`); 191 | mockCommit(`sha-${branch}`); 192 | mockUpdateRef(branch); 193 | 194 | await expect(run(body)).resolves.toEqual(mockCommitList); 195 | }); 196 | 197 | test(`success (branch exists, forkFromBaseBranch: true)`, async () => { 198 | const body = { 199 | ...validRequest, 200 | forkFromBaseBranch: true, 201 | }; 202 | mockGetRef(base, `sha-${base}`, true); 203 | mockGetRef(branch, `sha-${branch}`, true); 204 | mockCreateBlobFileOne(); 205 | mockCreateBlobFileTwo(); 206 | mockCreateTree(`sha-${base}`); 207 | mockCommit(`sha-${base}`); 208 | mockUpdateRef(branch); 209 | 210 | await expect(run(body)).resolves.toEqual(mockCommitList); 211 | }); 212 | 213 | test(`success (base64 encoded body)`, async () => { 214 | const body = { 215 | ...validRequest, 216 | changes: [ 217 | { 218 | message: "Your commit message", 219 | files: { 220 | "test.md": "SGVsbG8gV29ybGQ=", 221 | "test2.md": { 222 | contents: `Something else`, 223 | }, 224 | }, 225 | }, 226 | ], 227 | }; 228 | mockGetRef(branch, `sha-${branch}`, true); 229 | mockCreateBlobBase64PreEncoded(); 230 | mockCreateBlobFileTwo(); 231 | mockCreateTree(`sha-${branch}`); 232 | mockCommit(`sha-${branch}`); 233 | mockUpdateRef(branch); 234 | 235 | await expect(run(body)).resolves.toEqual(mockCommitList); 236 | }); 237 | 238 | test(`success (buffer body provided)`, async () => { 239 | const body = { 240 | ...validRequest, 241 | changes: [ 242 | { 243 | message: "Your commit message", 244 | files: { 245 | "test.md": Buffer.from( 246 | `# This is a test 247 | 248 | I hope it works`, 249 | ), 250 | "test2.md": { 251 | contents: `Something else`, 252 | }, 253 | }, 254 | }, 255 | ], 256 | }; 257 | 258 | mockGetRef(branch, `sha-${branch}`, true); 259 | mockCreateBlobFileOne(); 260 | mockCreateBlobFileTwo(); 261 | mockCreateTree(`sha-${branch}`); 262 | mockCommit(`sha-${branch}`); 263 | mockUpdateRef(branch); 264 | 265 | await expect(run(body)).resolves.toEqual(mockCommitList); 266 | }); 267 | 268 | test(`success (committer details)`, async () => { 269 | const committer = { 270 | name: "Ashley Person", 271 | email: "a.person@example.com", 272 | }; 273 | const body = { 274 | ...validRequest, 275 | committer, 276 | }; 277 | mockGetRef(branch, `sha-${branch}`, true); 278 | mockCreateBlobFileOne(); 279 | mockCreateBlobFileTwo(); 280 | mockCreateTree(`sha-${branch}`); 281 | mockCommit(`sha-${branch}`, { 282 | committer, 283 | }); 284 | mockUpdateRef(branch); 285 | 286 | await expect(run(body)).resolves.toEqual(mockCommitList); 287 | }); 288 | 289 | test(`success (author details)`, async () => { 290 | const author = { 291 | name: "Ashley Person", 292 | email: "a.person@example.com", 293 | }; 294 | const body = { 295 | ...validRequest, 296 | author, 297 | }; 298 | mockGetRef(branch, `sha-${branch}`, true); 299 | mockCreateBlobFileOne(); 300 | mockCreateBlobFileTwo(); 301 | mockCreateTree(`sha-${branch}`); 302 | mockCommit(`sha-${branch}`, { 303 | author, 304 | }); 305 | mockUpdateRef(branch); 306 | 307 | await expect(run(body)).resolves.toEqual(mockCommitList); 308 | }); 309 | 310 | test(`success (createBranch, base provided)`, async () => { 311 | const body = { 312 | ...validRequest, 313 | createBranch: true, 314 | }; 315 | mockGetRef(branch, `sha-${branch}`, false); 316 | mockGetRef(base, `sha-${base}`, true); 317 | mockCreateBlobFileOne(); 318 | mockCreateBlobFileTwo(); 319 | mockCreateTree(`sha-${base}`); 320 | mockCommit(`sha-${base}`); 321 | mockCreateRef(branch); 322 | 323 | await expect(run(body)).resolves.toEqual(mockCommitList); 324 | }); 325 | 326 | test(`success (createBranch, use default base branch)`, async () => { 327 | const body = { 328 | ...validRequest, 329 | createBranch: true, 330 | }; 331 | delete body.base; 332 | 333 | const repoDefaultBranch = "master"; 334 | 335 | mockGetRef(branch, `sha-${branch}`, false); 336 | mockGetRepo(repoDefaultBranch); 337 | mockGetRef(repoDefaultBranch, `sha-${repoDefaultBranch}`, true); 338 | mockCreateBlobFileOne(); 339 | mockCreateBlobFileTwo(); 340 | mockCreateTree(`sha-${repoDefaultBranch}`); 341 | mockCommit(`sha-${repoDefaultBranch}`); 342 | mockCreateRef(branch); 343 | 344 | await expect(run(body)).resolves.toEqual(mockCommitList); 345 | }); 346 | 347 | test(`success (createBranch, use default base branch, multiple commits)`, async () => { 348 | const body = { 349 | ...validRequest, 350 | createBranch: true, 351 | }; 352 | 353 | body.changes.push({ 354 | message: "This is the second commit", 355 | files: { 356 | "second.md": "With some contents", 357 | }, 358 | }); 359 | delete body.base; 360 | 361 | const repoDefaultBranch = "master"; 362 | 363 | mockGetRef(branch, `sha-${branch}`, false); 364 | mockGetRepo(repoDefaultBranch); 365 | mockGetRef(repoDefaultBranch, `sha-${repoDefaultBranch}`, true); 366 | mockCreateBlobFileOne(); 367 | mockCreateBlobFileTwo(); 368 | mockCreateBlobFileThree(); 369 | mockCreateBlobFileFour(); 370 | mockCreateTree(`sha-${repoDefaultBranch}`); 371 | mockCreateTreeSecond(`ef105a72c03ce2743d90944c2977b1b5563b43c0`); 372 | mockCommit(`sha-${repoDefaultBranch}`); 373 | mockCommitSecond(`ef105a72c03ce2743d90944c2977b1b5563b43c0`); 374 | mockCreateRef(branch, `45d77edc93556e3a997bf73d5ed4d9fb57068928`); 375 | 376 | await expect(run(body)).resolves.toEqual({ 377 | commits: [...mockCommitList.commits, ...mockSecondCommitList.commits], 378 | }); 379 | }); 380 | 381 | test("success (ignore missing deleted files)", async () => { 382 | mockGetRef(branch, `sha-${branch}`, false); 383 | mockGetRef(base, `sha-${base}`, true); 384 | mockCreateBlobFileTwo(); 385 | mockCreateBlobFileThree(); 386 | mockCreateBlobFileFour(); 387 | mockGetContents("wow-this-file-disappeared", `sha-${base}`, false); 388 | mockCreateTree(`sha-${base}`); 389 | mockCreateTreeWithIgnoredDelete(`sha-${base}`); 390 | mockCommitSecond(`sha-${base}`); 391 | mockCreateRefSecond(branch); 392 | 393 | const changes = [ 394 | { 395 | message: "This is the second commit", 396 | filesToDelete: ["wow-this-file-disappeared"], 397 | ignoreDeletionFailures: true, 398 | files: { 399 | "wow-this-file-was-created": { 400 | contents: "hi", 401 | }, 402 | }, 403 | }, 404 | ]; 405 | 406 | const body = { 407 | ...validRequest, 408 | changes, 409 | }; 410 | 411 | await expect(run(body)).resolves.toEqual(mockSecondCommitList); 412 | }); 413 | 414 | test("success (fileToDelete exists)", async () => { 415 | mockGetRef(branch, `sha-${branch}`, false); 416 | mockGetRef(base, `sha-${base}`, true); 417 | mockCreateBlobFileTwo(); 418 | mockCreateBlobFileThree(); 419 | mockCreateBlobFileFour(); 420 | mockGetContents("wow-this-file-disappeared", `sha-${base}`, true); 421 | mockCreateTreeWithDelete(`sha-${base}`); 422 | mockCommitSecond(`sha-${base}`); 423 | mockCreateRefSecond(branch); 424 | 425 | const changes = [ 426 | { 427 | message: "This is the second commit", 428 | filesToDelete: ["wow-this-file-disappeared"], 429 | files: { 430 | "wow-this-file-was-created": { 431 | contents: "hi", 432 | }, 433 | }, 434 | }, 435 | ]; 436 | 437 | const body = { 438 | ...validRequest, 439 | changes, 440 | }; 441 | 442 | await expect(run(body)).resolves.toEqual(mockSecondCommitList); 443 | }); 444 | 445 | test("success (fileToDelete exists, no content provided)", async () => { 446 | mockGetRef(branch, `sha-${branch}`, false); 447 | mockGetRef(base, `sha-${base}`, true); 448 | mockGetContents("wow-this-file-disappeared", `sha-${base}`, true); 449 | mockCreateTreeWithDeleteOnly(`sha-${base}`); 450 | mockCommitSecond(`sha-${base}`); 451 | mockCreateRefSecond(branch); 452 | 453 | const changes = [ 454 | { 455 | message: "This is the second commit", 456 | filesToDelete: ["wow-this-file-disappeared"], 457 | }, 458 | ]; 459 | 460 | const body = { 461 | ...validRequest, 462 | changes, 463 | }; 464 | 465 | await expect(run(body)).resolves.toEqual(mockSecondCommitList); 466 | }); 467 | 468 | test("failure (fileToDelete is missing)", async () => { 469 | mockGetRef(branch, `sha-${branch}`, false); 470 | mockGetRef(base, `sha-${base}`, true); 471 | mockCreateBlobFileTwo(); 472 | mockCreateBlobFileThree(); 473 | mockGetContents("wow-this-file-disappeared", `sha-${base}`, false); 474 | mockCreateTree(`sha-${base}`); 475 | mockCommit(`sha-${base}`); 476 | mockCreateRef(branch); 477 | 478 | const changes = [ 479 | { 480 | message: "Hello there", 481 | filesToDelete: ["wow-this-file-disappeared"], 482 | ignoreDeletionFailures: false, 483 | files: { 484 | "wow-this-file-was-created": { 485 | contents: "hi", 486 | }, 487 | }, 488 | }, 489 | ]; 490 | 491 | const body = { 492 | ...validRequest, 493 | changes, 494 | }; 495 | 496 | await expect(run(body)).rejects.toEqual( 497 | "The file wow-this-file-disappeared could not be found in the repo", 498 | ); 499 | }); 500 | 501 | test("Loads plugin", () => { 502 | const TestOctokit = Octokit.plugin(require(".")); 503 | const testOctokit = new TestOctokit(); 504 | expect(testOctokit).toHaveProperty("createOrUpdateFiles"); 505 | }); 506 | 507 | test("Does not overwrite other methods", () => { 508 | const TestOctokit = Octokit.plugin(require(".")); 509 | const testOctokit = new TestOctokit(); 510 | expect(testOctokit).toHaveProperty("rest.repos.acceptInvitation"); 511 | }); 512 | 513 | function mockGetRef(branch, sha, success) { 514 | const m = nock("https://api.github.com").get( 515 | `/repos/${owner}/${repo}/git/ref/heads%2F${branch}`, 516 | ); 517 | 518 | const body = { 519 | object: { 520 | sha: sha, 521 | }, 522 | }; 523 | 524 | if (success) { 525 | m.reply(200, body); 526 | } else { 527 | m.reply(404); 528 | } 529 | } 530 | 531 | function mockCreateBlob(content, sha) { 532 | const expectedBody = { content: content, encoding: "base64" }; 533 | const m = nock("https://api.github.com").post( 534 | `/repos/${owner}/${repo}/git/blobs`, 535 | expectedBody, 536 | ); 537 | 538 | const body = { 539 | sha: sha, 540 | url: `https://api.github.com/repos/mheap/action-test/git/blobs/${sha}`, 541 | }; 542 | 543 | m.reply(200, body); 544 | } 545 | 546 | function mockCreateBlobFileOne() { 547 | return mockCreateBlob( 548 | "IyBUaGlzIGlzIGEgdGVzdAoKSSBob3BlIGl0IHdvcmtz", 549 | "afb296bb7f3e327767bdda481c4877ba4a09e02e", 550 | ); 551 | } 552 | 553 | function mockCreateBlobFileTwo() { 554 | return mockCreateBlob( 555 | "U29tZXRoaW5nIGVsc2U=", 556 | "a71ee6d9405fed4f6fd181c61ceb40ef10905d30", 557 | ); 558 | } 559 | 560 | function mockCreateBlobFileThree() { 561 | return mockCreateBlob( 562 | "V2l0aCBzb21lIGNvbnRlbnRz", 563 | "f65b65200aea4fecbe0db6ddac1c0848cdda1d9b", 564 | ); 565 | } 566 | 567 | function mockCreateBlobFileFour() { 568 | return mockCreateBlob("aGk=", "f65b65200aea4fecbe0db6ddac1c0848cdda1d9b"); 569 | } 570 | 571 | function mockCreateBlobBase64PreEncoded() { 572 | return mockCreateBlob( 573 | "SGVsbG8gV29ybGQ=", 574 | "afb296bb7f3e327767bdda481c4877ba4a09e02e", 575 | ); 576 | } 577 | 578 | function mockCreateTreeSubmodule(baseTree) { 579 | const expectedBody = { 580 | tree: [ 581 | { 582 | path: "my_submodule", 583 | sha: "new-submodule-sha", 584 | mode: "160000", 585 | type: "commit", 586 | }, 587 | ], 588 | base_tree: baseTree, 589 | }; 590 | 591 | const m = nock("https://api.github.com").post( 592 | `/repos/${owner}/${repo}/git/trees`, 593 | expectedBody, 594 | ); 595 | 596 | const body = { 597 | sha: "4112258c05f8ce2b0570f1bbb1a330c0f9595ff9", 598 | }; 599 | 600 | m.reply(200, body); 601 | } 602 | 603 | function mockCreateTree(baseTree) { 604 | const expectedBody = { 605 | tree: [ 606 | { 607 | path: "test.md", 608 | sha: "afb296bb7f3e327767bdda481c4877ba4a09e02e", 609 | mode: "100644", 610 | type: "blob", 611 | }, 612 | { 613 | path: "test2.md", 614 | sha: "a71ee6d9405fed4f6fd181c61ceb40ef10905d30", 615 | mode: "100644", 616 | type: "blob", 617 | }, 618 | ], 619 | base_tree: baseTree, 620 | }; 621 | 622 | const m = nock("https://api.github.com").post( 623 | `/repos/${owner}/${repo}/git/trees`, 624 | expectedBody, 625 | ); 626 | 627 | const body = { 628 | sha: "4112258c05f8ce2b0570f1bbb1a330c0f9595ff9", 629 | }; 630 | 631 | m.reply(200, body); 632 | } 633 | 634 | function mockCreateTreeSecond(baseTree) { 635 | const expectedBody = { 636 | tree: [ 637 | { 638 | path: "second.md", 639 | sha: "f65b65200aea4fecbe0db6ddac1c0848cdda1d9b", 640 | mode: "100644", 641 | type: "blob", 642 | }, 643 | ], 644 | base_tree: baseTree, 645 | }; 646 | 647 | const m = nock("https://api.github.com").post( 648 | `/repos/${owner}/${repo}/git/trees`, 649 | expectedBody, 650 | ); 651 | 652 | const body = { 653 | sha: "fffff6bbf5ab983d31b1cca28e204b71ab722764", 654 | }; 655 | 656 | m.reply(200, body); 657 | } 658 | 659 | function mockCreateTreeWithIgnoredDelete(baseTree) { 660 | const expectedBody = { 661 | tree: [ 662 | { 663 | path: "wow-this-file-was-created", 664 | sha: "f65b65200aea4fecbe0db6ddac1c0848cdda1d9b", 665 | mode: "100644", 666 | type: "blob", 667 | }, 668 | ], 669 | base_tree: baseTree, 670 | }; 671 | 672 | const m = nock("https://api.github.com").post( 673 | `/repos/${owner}/${repo}/git/trees`, 674 | expectedBody, 675 | ); 676 | 677 | const body = { 678 | sha: "fffff6bbf5ab983d31b1cca28e204b71ab722764", 679 | }; 680 | 681 | m.reply(200, body); 682 | } 683 | 684 | function mockCreateTreeWithDeleteOnly(baseTree) { 685 | // The order here is important. Removals must be applied before creations 686 | const expectedBody = { 687 | tree: [ 688 | { 689 | path: "wow-this-file-disappeared", 690 | sha: null, 691 | mode: "100644", 692 | type: "commit", 693 | }, 694 | ], 695 | base_tree: baseTree, 696 | }; 697 | 698 | const m = nock("https://api.github.com").post( 699 | `/repos/${owner}/${repo}/git/trees`, 700 | expectedBody, 701 | ); 702 | 703 | const body = { 704 | sha: "fffff6bbf5ab983d31b1cca28e204b71ab722764", 705 | }; 706 | 707 | m.reply(200, body); 708 | } 709 | 710 | function mockCreateTreeWithDelete(baseTree) { 711 | // The order here is important. Removals must be applied before creations 712 | const expectedBody = { 713 | tree: [ 714 | { 715 | path: "wow-this-file-disappeared", 716 | sha: null, 717 | mode: "100644", 718 | type: "commit", 719 | }, 720 | { 721 | path: "wow-this-file-was-created", 722 | sha: "f65b65200aea4fecbe0db6ddac1c0848cdda1d9b", 723 | mode: "100644", 724 | type: "blob", 725 | }, 726 | ], 727 | base_tree: baseTree, 728 | }; 729 | 730 | const m = nock("https://api.github.com").post( 731 | `/repos/${owner}/${repo}/git/trees`, 732 | expectedBody, 733 | ); 734 | 735 | const body = { 736 | sha: "fffff6bbf5ab983d31b1cca28e204b71ab722764", 737 | }; 738 | 739 | m.reply(200, body); 740 | } 741 | 742 | function mockCommitSubmodule(baseTree) { 743 | const expectedBody = { 744 | message: "Your submodule commit message", 745 | tree: "4112258c05f8ce2b0570f1bbb1a330c0f9595ff9", 746 | parents: [baseTree], 747 | }; 748 | 749 | const m = nock("https://api.github.com").post( 750 | `/repos/${owner}/${repo}/git/commits`, 751 | expectedBody, 752 | ); 753 | 754 | const body = { 755 | sha: "ef105a72c03ce2743d90944c2977b1b5563b43c0", 756 | }; 757 | 758 | m.reply(200, body); 759 | } 760 | 761 | function mockCommit(baseTree, additional) { 762 | additional = additional || {}; 763 | 764 | const expectedBody = { 765 | message: "Your commit message", 766 | tree: "4112258c05f8ce2b0570f1bbb1a330c0f9595ff9", 767 | parents: [baseTree], 768 | ...additional, 769 | }; 770 | 771 | const m = nock("https://api.github.com").post( 772 | `/repos/${owner}/${repo}/git/commits`, 773 | expectedBody, 774 | ); 775 | 776 | const body = { 777 | sha: "ef105a72c03ce2743d90944c2977b1b5563b43c0", 778 | }; 779 | 780 | m.reply(200, body); 781 | } 782 | 783 | function mockCommitSecond(baseTree) { 784 | const expectedBody = { 785 | message: "This is the second commit", 786 | tree: "fffff6bbf5ab983d31b1cca28e204b71ab722764", 787 | parents: [baseTree], 788 | }; 789 | 790 | const m = nock("https://api.github.com").post( 791 | `/repos/${owner}/${repo}/git/commits`, 792 | expectedBody, 793 | ); 794 | 795 | const body = { 796 | sha: "45d77edc93556e3a997bf73d5ed4d9fb57068928", 797 | }; 798 | 799 | m.reply(200, body); 800 | } 801 | 802 | function mockUpdateRef(branch) { 803 | const expectedBody = { 804 | force: true, 805 | sha: "ef105a72c03ce2743d90944c2977b1b5563b43c0", 806 | }; 807 | 808 | const m = nock("https://api.github.com").patch( 809 | `/repos/${owner}/${repo}/git/refs/heads%2F${branch}`, 810 | expectedBody, 811 | ); 812 | 813 | m.reply(200); 814 | } 815 | 816 | function mockCreateRef(branch, sha) { 817 | const expectedBody = { 818 | force: true, 819 | ref: `refs/heads/${branch}`, 820 | sha: sha || "ef105a72c03ce2743d90944c2977b1b5563b43c0", 821 | }; 822 | 823 | const m = nock("https://api.github.com").post( 824 | `/repos/${owner}/${repo}/git/refs`, 825 | expectedBody, 826 | ); 827 | 828 | m.reply(200); 829 | } 830 | 831 | function mockCreateRefSecond(branch, sha) { 832 | const expectedBody = { 833 | force: true, 834 | ref: `refs/heads/${branch}`, 835 | sha: sha || "45d77edc93556e3a997bf73d5ed4d9fb57068928", 836 | }; 837 | 838 | const m = nock("https://api.github.com").post( 839 | `/repos/${owner}/${repo}/git/refs`, 840 | expectedBody, 841 | ); 842 | 843 | m.reply(200); 844 | } 845 | 846 | function mockGetRepo() { 847 | const body = { 848 | default_branch: "master", 849 | }; 850 | 851 | nock("https://api.github.com") 852 | .get(`/repos/${owner}/${repo}`) 853 | .reply(200, body); 854 | } 855 | 856 | function mockGetContents(fileName, branch, success) { 857 | const m = nock("https://api.github.com").head( 858 | `/repos/${owner}/${repo}/contents/${fileName}?ref=${branch}`, 859 | ); 860 | 861 | if (success) { 862 | m.reply(200); 863 | } else { 864 | m.reply(404); 865 | } 866 | return m; 867 | } 868 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const plugin = require("./create-or-update-files"); 2 | 3 | module.exports = function (octokit) { 4 | return { 5 | createOrUpdateFiles: plugin.bind(null, octokit), 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "octokit-commit-multiple-files", 3 | "version": "5.0.3", 4 | "description": "Octokit plugin to create/update multiple files at once", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint *.js", 9 | "lint-fix": "eslint --fix *.js" 10 | }, 11 | "keywords": [ 12 | "github", 13 | "octokit", 14 | "plugin", 15 | "commit" 16 | ], 17 | "author": "Michael Heap ", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@octokit/rest": ">=18.5.0", 21 | "eslint": "^8.47.0", 22 | "eslint-plugin-prettier": "^5.0.0", 23 | "jest": "^29.6.2", 24 | "prettier": "^3.0.2", 25 | "nock": "^13.3.3" 26 | }, 27 | "dependencies": {}, 28 | "peerDependencies": { 29 | "@octokit/rest": ">=20.0.1" 30 | } 31 | } 32 | --------------------------------------------------------------------------------