Skip to content

Commit 2e9c115

Browse files
authored
feat: when a repo isn't setup, automatically set it up (#41)
* feat: when a repo isn't setup, automatically set it up * ensure error for non 404s is thrown
1 parent 7f9e3f8 commit 2e9c115

File tree

16 files changed

+394
-84
lines changed

16 files changed

+394
-84
lines changed

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,14 @@ A bot for automatically adding all-contributors.
1010
<a href="#Usage"><img alt="Example usage screenshot" src="docs/usage.png" width="500px"></a>
1111

1212
## Installation
13-
1. [Install the GitHub App](https://github.com/apps/allcontributors)
14-
2. Setup your `README.md` and `.all-contributorsrc` using the [all-contributors-cli tool](https://github.com/all-contributors/all-contributors-cli)
15-
> In the future we want to remove the need for this setup with the CLI tool, if you want to help out [see issue #3](https://github.com/all-contributors/all-contributors-bot/issues/3)
16-
13+
[Install the GitHub App](https://github.com/apps/allcontributors) and [configure your repositories](https://github.com/apps/allcontributors/installations/new)
1714

1815
## Usage
1916

2017
### Adding contributions
21-
1. Comment on Issue/PR etc with text: `@all-contributors please add jakebolam for infra, test and code` (Full words coming soon, [see issue #30](https://github.com/all-contributors/all-contributors-bot/issues/30))
22-
2. Bot will look for `.all-contributorsrc` if not found, comments on pr to run setup
23-
3. If user exists, add new contribution, if not add user and add contribution
18+
Comment on Issue/PR etc with text: `@all-contributors please add @jakebolam for infrastructure, test and code`
19+
20+
@all-contributors will then creates a PR to add the contributor and respond to you
2421

2522

2623
## Contributing

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525
"dependencies": {
2626
"@probot/serverless-lambda": "^0.2.0",
27-
"all-contributors-cli": "^5.7.0",
27+
"all-contributors-cli": "^5.9.1",
2828
"compromise": "^11.13.0",
2929
"probot": "^8.0.0-octokit-16-preview"
3030
},

src/ContentFiles/index.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
const { generate: generateContentFile } = require('all-contributors-cli')
2+
const { initBadge, initContributorsList } = require('all-contributors-cli')
23

34
const AllContributorBotError = require('../utils/errors')
45

6+
function modifyFiles({ contentFilesByPath, fileContentModifierFunction }) {
7+
const newFilesByPath = {}
8+
Object.entries(contentFilesByPath).forEach(
9+
([filePath, { content, sha }]) => {
10+
const newFileContents = fileContentModifierFunction(content)
11+
newFilesByPath[filePath] = {
12+
content: newFileContents,
13+
originalSha: sha,
14+
}
15+
},
16+
)
17+
return newFilesByPath
18+
}
19+
520
/*
621
* Fetches, stores, generates, and updates the readme content files for the contributors list
722
*/
@@ -23,22 +38,30 @@ class ContentFiles {
2338
)
2439
}
2540

26-
async generate(optionsConfig) {
41+
init() {
42+
const newFilesByPath = modifyFiles({
43+
contentFilesByPath: this.contentFilesByPath,
44+
fileContentModifierFunction: function(content) {
45+
const contentWithBadge = initBadge(content)
46+
const contentWithList = initContributorsList(contentWithBadge)
47+
return contentWithList
48+
},
49+
})
50+
this.contentFilesByPath = newFilesByPath
51+
}
52+
53+
generate(optionsConfig) {
2754
const options = optionsConfig.get()
28-
const newFilesByPath = {}
29-
Object.entries(this.contentFilesByPath).forEach(
30-
([filePath, { content, sha }]) => {
31-
const newFileContents = generateContentFile(
55+
const newFilesByPath = modifyFiles({
56+
contentFilesByPath: this.contentFilesByPath,
57+
fileContentModifierFunction: function(content) {
58+
return generateContentFile(
3259
options,
3360
options.contributors,
3461
content,
3562
)
36-
newFilesByPath[filePath] = {
37-
content: newFileContents,
38-
originalSha: sha,
39-
}
4063
},
41-
)
64+
})
4265
this.contentFilesByPath = newFilesByPath
4366
}
4467

src/OptionsConfig/index.js

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ const ALL_CONTRIBUTORS_RC = '.all-contributorsrc'
22

33
const { addContributorWithDetails } = require('all-contributors-cli')
44

5-
const { ResourceNotFoundError } = require('../utils/errors')
6-
75
class OptionsConfig {
86
constructor({ repository, commentReply }) {
97
this.repository = repository
@@ -13,40 +11,43 @@ class OptionsConfig {
1311
}
1412

1513
async fetch() {
14+
const {
15+
content: rawOptionsFileContent,
16+
sha,
17+
} = await this.repository.getFile(ALL_CONTRIBUTORS_RC)
18+
this.originalOptionsSha = sha
1619
try {
17-
const {
18-
content: rawOptionsFileContent,
19-
sha,
20-
} = await this.repository.getFile(ALL_CONTRIBUTORS_RC)
21-
this.originalOptionsSha = sha
22-
try {
23-
const optionsConfig = JSON.parse(rawOptionsFileContent)
24-
this.options = optionsConfig
25-
return optionsConfig
26-
} catch (error) {
27-
if (error instanceof SyntaxError) {
28-
this.commentReply.reply(
29-
`This project's configuration file has malformed JSON: ${ALL_CONTRIBUTORS_RC}. Error:: ${
30-
error.message
31-
}`,
32-
)
33-
error.handled = true
34-
}
35-
throw error
36-
}
20+
const optionsConfig = JSON.parse(rawOptionsFileContent)
21+
this.options = optionsConfig
22+
return optionsConfig
3723
} catch (error) {
38-
if (error instanceof ResourceNotFoundError) {
39-
this.commentReply
40-
.reply(`This project is not yet setup for [all-contributors](https://github.com/all-contributors/all-contributors).\n
41-
You will need to first setup [${
42-
this.repository.repo
43-
}](https://github.com/${this.repository.getFullname()}) using the [all-contributors-cli](https://github.com/all-contributors/all-contributors-cli) tool.`)
24+
if (error instanceof SyntaxError) {
25+
this.commentReply.reply(
26+
`This project's configuration file has malformed JSON: ${ALL_CONTRIBUTORS_RC}. Error:: ${
27+
error.message
28+
}`,
29+
)
4430
error.handled = true
4531
}
4632
throw error
4733
}
4834
}
4935

36+
init() {
37+
const { repo, owner } = this.repository
38+
this.options = {
39+
projectName: repo,
40+
projectOwner: owner,
41+
repoType: 'github',
42+
repoHost: 'https://github.com',
43+
files: ['README.md'],
44+
imageSize: 100,
45+
commit: false,
46+
contributors: [],
47+
contributorsPerLine: 7,
48+
}
49+
}
50+
5051
get() {
5152
const options = this.options
5253
if (!Array.isArray(options.files)) {

src/Repository/index.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,38 @@ class Repository {
9797
})
9898
}
9999

100-
async updateFiles({ filesByPath, branchName }) {
101-
// TODO: can probably optimise this instead of sending a request per file
100+
async createFile({ filePath, content, branchName }) {
101+
const contentBinary = Buffer.from(content).toString('base64')
102+
103+
//octokit.github.io/rest.js/#api-Repos-createFile
104+
await this.github.repos.createFile({
105+
owner: this.owner,
106+
repo: this.repo,
107+
path: filePath,
108+
message: `docs: create ${filePath}`,
109+
content: contentBinary,
110+
branch: branchName,
111+
})
112+
}
113+
114+
async createOrUpdateFile({ filePath, content, branchName, originalSha }) {
115+
if (originalSha === undefined) {
116+
await this.createFile({ filePath, content, branchName })
117+
} else {
118+
await this.updateFile({
119+
filePath,
120+
content,
121+
branchName,
122+
originalSha,
123+
})
124+
}
125+
}
126+
127+
async createOrUpdateFiles({ filesByPath, branchName }) {
102128
const repository = this
103-
const updateFilesMultiple = Object.entries(filesByPath).map(
129+
const createOrUpdateFilesMultiple = Object.entries(filesByPath).map(
104130
([filePath, { content, originalSha }]) => {
105-
return repository.updateFile({
131+
return repository.createOrUpdateFile({
106132
filePath,
107133
content,
108134
branchName,
@@ -111,7 +137,7 @@ class Repository {
111137
},
112138
)
113139

114-
await Promise.all(updateFilesMultiple)
140+
await Promise.all(createOrUpdateFilesMultiple)
115141
}
116142

117143
async createPullRequest({ title, body, branchName }) {
@@ -130,7 +156,7 @@ class Repository {
130156
async createPullRequestFromFiles({ title, body, filesByPath, branchName }) {
131157
await this.createBranch(branchName)
132158

133-
await this.updateFiles({
159+
await this.createOrUpdateFiles({
134160
filesByPath,
135161
branchName,
136162
})

src/processIssueComment.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ const parseComment = require('./utils/parse-comment')
88

99
const isMessageForBot = require('./utils/isMessageForBot')
1010
const { GIHUB_BOT_NAME } = require('./utils/settings')
11-
const { AllContributorBotError } = require('./utils/errors')
11+
const {
12+
AllContributorBotError,
13+
ResourceNotFoundError,
14+
} = require('./utils/errors')
1215

1316
async function processAddContributor({
1417
context,
@@ -35,7 +38,10 @@ async function processAddContributor({
3538
repository,
3639
})
3740
await contentFiles.fetch(optionsConfig)
38-
await contentFiles.generate(optionsConfig)
41+
if (optionsConfig.getOriginalSha() === undefined) {
42+
contentFiles.init()
43+
}
44+
contentFiles.generate(optionsConfig)
3945
const filesByPathToUpdate = contentFiles.get()
4046
filesByPathToUpdate[optionsConfig.getPath()] = {
4147
content: optionsConfig.getRaw(),
@@ -65,7 +71,15 @@ async function processIssueComment({ context, commentReply }) {
6571
repository,
6672
commentReply,
6773
})
68-
await optionsConfig.fetch()
74+
try {
75+
await optionsConfig.fetch()
76+
} catch (error) {
77+
if (error instanceof ResourceNotFoundError) {
78+
optionsConfig.init()
79+
} else {
80+
throw error
81+
}
82+
}
6983

7084
const commentBody = context.payload.comment.body
7185
const parsedComment = parseComment(commentBody)
@@ -111,7 +125,7 @@ async function processIssueCommentSafe({ context }) {
111125
if (error.handled) {
112126
context.log.debug(error)
113127
} else if (error instanceof AllContributorBotError) {
114-
context.log.error(error)
128+
context.log.info(error)
115129
commentReply.reply(error.message)
116130
} else {
117131
context.log.error(error)

test/ContentFiles/__snapshots__/index.test.js.snap

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/all-contri
1818
1919
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
2020
<!-- prettier-ignore -->
21-
| [<img src=\\"https://avatars2.githubusercontent.com/u/3534236?v=4\\" width=\\"100px;\\"/><br /><sub><b>Jake Bolam</b></sub>](https://jakebolam.com)<br />[💻](#code-jakebolam \\"Code\\") [🤔](#ideas-jakebolam \\"Ideas, Planning, & Feedback\\") [🚇](#infra-jakebolam \\"Infrastructure (Hosting, Build-Tools, etc)\\") [⚠️](#test-jakebolam \\"Tests\\") | [<img src=\\"https://avatars2.githubusercontent.com/u/7265547?v=4\\" width=\\"100px;\\"/><br /><sub><b>tbenning</b></sub>](https://github.com/tbenning)<br />[🎨](#design-tbenning \\"Design\\") |
21+
| [<img src=\\"https://avatars2.githubusercontent.com/u/3534236?v=4\\" width=\\"100px;\\" alt=\\"Jake Bolam\\"/><br /><sub><b>Jake Bolam</b></sub>](https://jakebolam.com)<br />[💻](#code-jakebolam \\"Code\\") [🤔](#ideas-jakebolam \\"Ideas, Planning, & Feedback\\") [🚇](#infra-jakebolam \\"Infrastructure (Hosting, Build-Tools, etc)\\") [⚠️](#test-jakebolam \\"Tests\\") | [<img src=\\"https://avatars2.githubusercontent.com/u/7265547?v=4\\" width=\\"100px;\\" alt=\\"tbenning\\"/><br /><sub><b>tbenning</b></sub>](https://github.com/tbenning)<br />[🎨](#design-tbenning \\"Design\\") |
2222
| :---: | :---: |
2323
<!-- ALL-CONTRIBUTORS-LIST:END -->
2424
@@ -29,3 +29,28 @@ This project follows the [all-contributors](https://github.com/all-contributors/
2929
[MIT](LICENSE)
3030
"
3131
`;
32+
33+
exports[`ContentFiles Init content 1`] = `
34+
"# TestContentFIles
35+
[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)
36+
37+
A test for content files generation
38+
39+
### Content
40+
1. Stuff here
41+
2. Yeah boy
42+
43+
## LICENSE
44+
45+
[MIT](LICENSE)
46+
47+
## Contributors
48+
49+
Thanks goes to these wonderful people ([emoji key](https://github.com/all-contributors/all-contributors#emoji-key)):
50+
51+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
52+
<!-- prettier-ignore -->
53+
<!-- ALL-CONTRIBUTORS-LIST:END -->
54+
55+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!"
56+
`;

test/ContentFiles/index.test.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ describe('ContentFiles', () => {
99
path.join(__dirname, 'test-readme-file.md'),
1010
'utf8',
1111
)
12+
const mockTestFileContentNoTable = fs.readFileSync(
13+
path.join(__dirname, 'test-readme-file-no-table.md'),
14+
'utf8',
15+
)
1216

13-
test(`Add's new contributor`, async () => {
17+
test(`Add's new contributor`, () => {
1418
const contentFiles = new ContentFiles({
1519
repository: mockRepository,
1620
})
@@ -45,7 +49,29 @@ describe('ContentFiles', () => {
4549
}
4650
},
4751
}
48-
await contentFiles.generate(mockOptionsConfig)
52+
contentFiles.generate(mockOptionsConfig)
53+
54+
expect(contentFiles.get()['README.md'].content).toMatchSnapshot()
55+
})
56+
57+
test(`Init content`, () => {
58+
const contentFiles = new ContentFiles({
59+
repository: mockRepository,
60+
})
61+
contentFiles.contentFilesByPath = {
62+
'README.md': {
63+
content: mockTestFileContentNoTable,
64+
},
65+
}
66+
67+
const mockOptionsConfig = {
68+
get: function() {
69+
return {
70+
contributorsPerLine: 7,
71+
}
72+
},
73+
}
74+
contentFiles.init(mockOptionsConfig)
4975

5076
expect(contentFiles.get()['README.md'].content).toMatchSnapshot()
5177
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# TestContentFIles
2+
3+
A test for content files generation
4+
5+
### Content
6+
1. Stuff here
7+
2. Yeah boy
8+
9+
## LICENSE
10+
11+
[MIT](LICENSE)

0 commit comments

Comments
 (0)