├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cli.js
├── env.js
├── fixtures
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
└── js
│ └── .travis.yml
├── lib
├── githubHelpers.js
├── helpers.js
├── lintPackageJson.js
└── robo.js
├── package-lock.json
├── package.json
├── src
├── checkers
│ ├── community.js
│ ├── javascript.js
│ └── travis.js
├── index.js
├── messages.js
└── utilities
│ ├── find-section.js
│ └── get-section.js
└── test
├── fixtures
├── CODE_OF_CONDUCT.md
└── readme-with-contributing.md
├── lint.js
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | config.json
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache:
3 | directories:
4 | - node_modules
5 | notifications:
6 | email: false
7 | node_js:
8 | - '8'
9 | before_script:
10 | - npm prune
11 | - greenkeeper-lockfile-update
12 | after_success:
13 | - npm run semantic-release
14 | branches:
15 | except:
16 | - /^v\d+\.\d+\.\d+$/
17 | before_install:
18 | - npm install -g npm@5
19 | - npm install -g greenkeeper-lockfile@1
20 | after_script: greenkeeper-lockfile-upload
21 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at richard.littauer@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribute
2 |
3 | Please contribute! Here are some things that would be great:
4 | - Open an issue!
5 | - Open a pull request!
6 | - Say hi! 👋
7 |
8 | Please abide by the [code of conduct](CODE_OF_CONDUCT.md).
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Richard Littauer
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
13 | all 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
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build A Space
2 |
3 | [](https://travis-ci.org/mntnr/build-a-space) [](https://greenkeeper.io/)
4 |
5 | > Automatically add community documentation to your repository
6 |
7 | ## Background
8 |
9 | I often clean up repositories, and have ideas for what I want to add to them. However, just as often, I manually add files to repositories. [@gr2m](https://github.com/gr2m) got me thinking - wouldn't it be better if I could automatically add files to a repository, using a GitHub bot?
10 |
11 | This GitHub bot does _just that_. It adds a Contributing guide, a Code of Conduct, a README if you don't have one stubbed out, a License, and more to a repository. It lints your `package.json`. It tells you if you don't have an email specified in your CoC. It does a lot of stuff.
12 |
13 | The point is to make building a space for community to grow _easier_. This stuff isn't rocket science, but doing it manually day after day is the hard part. Let's make it easier to build a space.
14 |
15 | ## Install
16 |
17 | You can install this globally with npm:
18 |
19 | ```
20 | $ npm install -g build-a-space
21 | ```
22 |
23 | You'll need a [GitHub token](https://github.com/settings/tokens). Put it in the `env.js` file, or in a `$BUILD_A_SPACE` token in your environment.
24 |
25 | You _may_ be able to install this locally and include it, but I have no idea how it would react.
26 |
27 | ## Usage
28 |
29 | ```
30 | Usage
31 | $ build-a-space [opts]
32 |
33 | Options
34 | -f, --fork Create and use a fork instead of pushing to a branch
35 | -t, --test Don't open issues or create pull requests
36 | -c, --config The path to a configuration file
37 | -b, --branch The default branch to use instead of 'master'
38 | --email The email for the Code of Conduct
39 | --licensee The person to license the repository to
40 | --travis Edit the Travis file
41 | --open Open the PR url afterwards
42 |
43 | Examples
44 | $ build-a-space mntnr/build-a-space
45 | ```
46 |
47 | Substitute another repo as needed. It drives itself from there.
48 |
49 | ### Configuration
50 |
51 | You can specify a configuration file to stop having to type lots and lots of flags for multiple repositories. This will overwrite any flags you send in.
52 |
53 | ```
54 | $ build-a-space -c=config.json
55 | ```
56 |
57 | And, in `config.json`:
58 |
59 | ```
60 | {
61 | "email": "richard@maintainer.io",
62 | "licensee": "Richard Littauer",
63 | "contributing": "./contributing.md"
64 | }
65 | ```
66 |
67 | The contributing file path needs to be in the `build-a-space` directory.
68 |
69 | ## Maintainers
70 |
71 | [Me](https://burntfen.com).
72 |
73 | And [you](https://github.com/mntnr/build-a-space/issues/new?title=I%20want%20to%20be%20a%20maintainer!)?
74 |
75 | ## Contribute
76 |
77 | I would love for this to be a community effort. For now, I am hacking away at it because I want to be able to use it quickly get various documents into place as needed for different organizations I work with. However, if would be great if others would start using it, as well.
78 |
79 | Check out the [Contributing guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) for more.
80 |
81 | ## License
82 |
83 | [MIT](LICENSE) © 2017 Richard Littauer
84 |
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const meow = require('meow')
4 | const fs = require('mz/fs')
5 | const path = require('path')
6 | const fn = require('./src/index')
7 | const gitRemoteOriginUrl = require('git-remote-origin-url')
8 | const gitRemoteUpstreamUrl = require('git-remote-upstream-url')
9 |
10 | const cli = meow({
11 | 'help': `
12 | Usage
13 | $ build-a-space [opts]
14 |
15 | Options
16 | -f, --fork Create and use a fork instead of pushing to a branch
17 | -t, --test Don't open issues or create pull requests
18 | -c, --config The path to a configuration file
19 | -b, --branch The default branch to use instead of 'master'
20 | --email The email for the Code of Conduct
21 | --licensee The person to license the repository to
22 | --travis Edit the travis file
23 | --open Open the PR url afterwards
24 |
25 | Examples
26 | $ build-a-space mntnr/build-a-space
27 | `,
28 | 'flags': {
29 | 'help': {
30 | type: 'boolean',
31 | alias: 'h'
32 | },
33 | 'fork': {
34 | type: 'boolean',
35 | alias: 'f'
36 | },
37 | 'test': {
38 | type: 'boolean',
39 | alias: 't'
40 | },
41 | 'config': {
42 | type: 'string',
43 | alias: 'c'
44 | },
45 | 'branch': {
46 | type: 'string',
47 | alias: 'b'
48 | },
49 | 'email': {
50 | type: 'string'
51 | },
52 | 'licensee': {
53 | type: 'string'
54 | },
55 | 'travis': {
56 | type: 'boolean',
57 | default: false
58 | },
59 | 'open': {
60 | type: 'boolean',
61 | defaul: false
62 | },
63 | 'version': {
64 | type: 'boolean',
65 | alias: 'v'
66 | }
67 | }
68 | })
69 |
70 | // TODO Make this into it's own module, gh-get-shortname
71 | function getRepoFromConfig () {
72 | return gitRemoteUpstreamUrl()
73 | .catch(() => gitRemoteOriginUrl())
74 | .then(res => res.match(/([^/:]+\/[^/.]+)(\.git)?$/)[1])
75 | }
76 |
77 | async function getConfig (configPath) {
78 | if (configPath) {
79 | let config = await fs.readFileSync(path.join(__dirname, configPath))
80 | .toString('utf8')
81 | return JSON.parse(config)
82 | }
83 | }
84 |
85 | async function letsGo () {
86 | console.log('')
87 | const repoName = cli.input[0] || await getRepoFromConfig()
88 | Object.assign(cli.flags, await getConfig(cli.flags.config))
89 | await fn(repoName, cli.flags)
90 | console.log('')
91 | }
92 |
93 | letsGo()
94 |
--------------------------------------------------------------------------------
/env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | BUILD_A_SPACE: process.env.BUILD_A_SPACE,
3 | BASE_URL: process.env.BASE_URL || 'https://api.github.com'
4 | }
5 |
--------------------------------------------------------------------------------
/fixtures/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/fixtures/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribute
2 |
3 | Please contribute! Here are some things that would be great:
4 | - [Open an issue!](https://github.com/[GITHUB REPONAME]/issues/new)
5 | - Open a pull request!
6 | - Say hi! :wave:
7 |
8 | Please abide by the [code of conduct](CODE_OF_CONDUCT.md).
9 |
--------------------------------------------------------------------------------
/fixtures/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2017 by [INSERT LICENSEE]
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/fixtures/js/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache:
3 | directories:
4 | - node_modules
5 | notifications:
6 | email: false
7 | node_js:
8 | - 'node'
9 | - '8'
10 | - '7'
11 | before_script:
12 | - npm prune
13 | - greenkeeper-lockfile-update
14 | after_success:
15 | - npm run semantic-release
16 | branches:
17 | except:
18 | - /^v\d+\.\d+\.\d+$/
19 | before_install:
20 | - npm install -g npm@5
21 | - npm install -g greenkeeper-lockfile@1
22 | after_script: greenkeeper-lockfile-upload
23 |
--------------------------------------------------------------------------------
/lib/githubHelpers.js:
--------------------------------------------------------------------------------
1 | const atob = require('atob')
2 | const btoa = require('btoa')
3 | const console = require('./robo')
4 |
5 | module.exports = {
6 | // getCurrentSha,
7 | // getFileBlob,
8 | bunchFiles,
9 | sameOnBranch,
10 | commitFile,
11 | checkStatus
12 | }
13 |
14 | async function checkStatus (github, file) {
15 | let {status} = await github.get(`/repos/${github.repoName}/contents/${file.path}?branch=${github.branchName}`)
16 | .catch(err => {
17 | console.robolog(`Unable to find ${file.name}.`)
18 | return err.response
19 | })
20 | return status
21 | }
22 |
23 | async function sameOnBranch (github, file) {
24 | // Check if the file is any different on the branch
25 | const {data} = await github.get(`/repos/${github.targetRepo}/contents/${file.path}?branch=${github.branchName}`)
26 | .catch(err => {
27 | if (err) {
28 | // console.log('File does not exist, so cannot check sameOnBranch.')
29 | }
30 | return false
31 | })
32 |
33 | // If the content is the same, don't add a new commit
34 | // Shim them in and out to make sure that there's no formatting differences
35 | return (data) ? btoa(atob(file.content)) === btoa(atob(data.content)) : false
36 | }
37 |
38 | async function commitFile (github, file) {
39 | const commit = {
40 | branch: github.branchName,
41 | content: file.content,
42 | message: file.message,
43 | path: file.path
44 | }
45 |
46 | if (!file.newFile) {
47 | commit.sha = await getCurrentSha(github, file.path)
48 | }
49 |
50 | await github.put(`/repos/${github.targetRepo}/contents/${file.path}?branch=${github.branchName}`, commit)
51 | .catch(err => {
52 | if (err) {
53 | console.robowarn(`Unable to add ${file.name}!`)
54 | }
55 | })
56 | }
57 |
58 | // This function bunches up multiple file changes into the same commit.
59 | // It depends on a files object: { name, filepath, content, note }.
60 | async function bunchFiles (github, filesToCheck, opts) {
61 | opts = opts || {}
62 |
63 | // Always work off of master, for now. TODO Enable other dev branches
64 | // get the current commit object and current tree
65 | const {
66 | data: {commit: {sha: currentCommitSha}},
67 | data: {commit: {commit: {tree: {sha: treeSha}}}}
68 | } = await github.get(`/repos/${github.repoName}/branches/${github.defaultBranch}`)
69 | .catch(err => {
70 | if (err) {
71 | console.robowarn('Unable to get commit information to bunch files')
72 | }
73 | })
74 |
75 | // retrieve the content of the blob object that tree has for that particular file path
76 | const {data: {tree}} = await github.get(`/repos/${github.repoName}/git/trees/${treeSha}`)
77 | .catch(err => {
78 | if (err) {
79 | console.robowarn('Unable to get tree')
80 | }
81 | })
82 |
83 | // change the content somehow and post a new blob object with that new content, getting a blob SHA back
84 | const newBlobs = await Promise.all(filesToCheck.map(async file => getFileBlob(github, file)))
85 |
86 | if (newBlobs.length !== 0 && !opts.test) {
87 | const newTree = tree.concat(newBlobs)
88 |
89 | // post a new tree object with that file path pointer replaced with your new blob SHA getting a tree SHA back
90 | const {data: {sha: newTreeSha}} = await github.post(`/repos/${github.targetRepo}/git/trees`, {
91 | tree: newTree,
92 | base_tree: treeSha
93 | })
94 |
95 | // create a new commit object with the current commit SHA as the parent and the new tree SHA, getting a commit SHA back
96 | const {data: {sha: newCommitSha}} = await github.post(`/repos/${github.targetRepo}/git/commits`, {
97 | message: opts.message,
98 | tree: newTreeSha,
99 | parents: [currentCommitSha]
100 | }).catch(err => {
101 | if (err) {
102 | console.log('Unable to create new commit obj')
103 | }
104 | })
105 |
106 | // update the reference of your branch to point to the new commit SHA
107 | await github.post(`/repos/${github.targetRepo}/git/refs/heads/${github.branchName}`, {
108 | sha: newCommitSha
109 | }).catch(err => {
110 | if (err) {
111 | console.log('Unable to update refs with new commit')
112 | }
113 | })
114 | }
115 | return filesToCheck
116 | }
117 |
118 | async function getFileBlob (github, file) {
119 | console.robolog(`Adding ${file.name} file`)
120 |
121 | // Create a blob
122 | const {data: blob} = await github.post(`/repos/${github.targetRepo}/git/blobs`, {
123 | content: file.content,
124 | encoding: 'base64'
125 | }).catch(err => {
126 | if (err) {}
127 | console.robofire(`I can't post to a foreign repo! Do you have access?`)
128 | console.log('')
129 | process.exit(1)
130 | })
131 |
132 | return {
133 | mode: '100644',
134 | type: 'blob',
135 | path: file.filePath, // Puts them all in the base directory for now
136 | sha: blob.sha,
137 | url: blob.url
138 | }
139 | }
140 |
141 | async function getCurrentSha (github, filename) {
142 | const {data: {sha: currentSha}} = await github.get(`/repos/${github.targetRepo}/contents/${filename}?branch=${github.branchName}`)
143 | .catch(err => {
144 | if (err) {}
145 | console.robofire('Unable to get current sha, most likely due to undefined branch.')
146 | })
147 | return currentSha
148 | }
149 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | function joinNotes (arr) {
2 | return arr.map(file => {
3 | if (file) {
4 | return file.note.map(note => `- [ ] ${note}`).join('\n')
5 | }
6 | }).join('\n')
7 | }
8 |
9 | module.exports = {
10 | joinNotes
11 | }
12 |
--------------------------------------------------------------------------------
/lib/lintPackageJson.js:
--------------------------------------------------------------------------------
1 | const deepEqual = require('deep-equal')
2 |
3 | const githubPreviewHeaders = [
4 | `application/vnd.github.mercy-preview+json`, // Topics https://developer.github.com/v3/repos/#replace-all-topics-for-a-repository
5 | `application/vnd.github.drax-preview+json` // Licenses https://developer.github.com/v3/licenses/
6 | ]
7 |
8 | // TODO Return to this if relevant
9 | // Set field in package
10 | // Apply note to notes
11 | // function check (field, expected, note) {
12 | // if (!field) {
13 | // field = expected
14 | // push note.
15 | // }
16 | // }
17 |
18 | // Set the package description to match the GitHub description
19 | async function checkDescription (github, pkg, ghDescription, notesForUser) {
20 | if (ghDescription !== pkg.description) {
21 | if (ghDescription && !pkg.description) {
22 | pkg.description = ghDescription
23 | notesForUser.push(`We've added "${ghDescription}" as the description in the \`package.json\`. We got this from the GitHub repo description.`)
24 | } else if (!ghDescription && pkg.description) {
25 | const {data} = await github.patch(`/repos/${github.repoName}`, {
26 | description: pkg.description,
27 | name: github.repoName
28 | }).catch(err => {
29 | if (err) {}
30 | console.robowarn(`Unable to set GitHub description using \`package.json\` description. Probably a permissions error.`)
31 | notesForUser.push(`Add a GitHub description. Your \`package.json\` description should work.`)
32 | })
33 | if (data.description === pkg.description) {
34 | console.robolog(`I set the GitHub description to match the \`package.json\` description.`)
35 | }
36 | } else {
37 | notesForUser.push(`Check the \`package.json\` description. It didn't match the GitHub description for the repository.`)
38 | }
39 | }
40 | }
41 |
42 | function checkRepository (github, pkg, notesForUser) {
43 | // TODO Enable this, too: JSON.parse({'type': 'git', 'url': `https://github.com/${github.repoName}.git`}).toString()
44 | const expectedRepo = {
45 | 'type': 'git',
46 | // TODO Allow "url": "git://github.com/simonv3/covenant-generator.git"
47 | 'url': `https://github.com/${github.repoName}.git`
48 | }
49 | if (!pkg.repository) {
50 | pkg.repository = expectedRepo // username/repo is also valid https://docs.npmjs.com/files/package.json#repository
51 | } else if (!deepEqual(pkg.repository, expectedRepo)) {
52 | notesForUser.push(`We expected the repository url in the \`package.json\` to be ${expectedRepo.url.split('.git')[0]}, and it wasn't. Is this intentional?`)
53 | }
54 | }
55 |
56 | // Check that the homepage is valid
57 | function checkHomepage (github, pkg, notesForUser) {
58 | // TODO Use the GitHub homepage if it exists
59 | if (!pkg.homepage) {
60 | pkg.homepage = `https://github.com/${github.repoName}`
61 | notesForUser.push(`Check that the homepage in the \`package.json\` is OK. Another one besides your GitHub repo might work. We've set it to https://github.com/${github.repoName}.`)
62 | }
63 | }
64 |
65 | function checkBugs (github, pkg, notesForUser) {
66 | const validBugString = `https://github.com/${github.repoName}/issues`
67 | if (!pkg.bugs || pkg.bugs === validBugString.replace('https', 'http') || pkg.bugs.url === validBugString.replace('https', 'http')) {
68 | pkg.bugs = {
69 | 'url': `https://github.com/${github.repoName}/issues`
70 | }
71 | } else if ((pkg.bugs.url && pkg.bugs.url !== validBugString) || pkg.bugs !== validBugString) {
72 | notesForUser.push(`Check that the bugs field in the package.json is OK. It doesn't match what we'd expect, which would be https://github.com/${github.repoName}/issues`)
73 | }
74 | }
75 |
76 | function arrToQuotedArr (arr) {
77 | return arr.map(k => `"${k}"`).join(', ')
78 | }
79 |
80 | async function checkKeywords (github, pkg, topics, notesForUser) {
81 | // Note: This uses a GitHub preview header, and may break.
82 | // Uniquify and filter out null and undefined
83 | const allKeywords = [ ...new Set(topics.concat(pkg.keywords)) ].filter(x => x)
84 | if (!deepEqual(allKeywords, pkg.keywords)) {
85 | pkg.keywords = allKeywords // Add this even if allKeywords is empty, because the field is worth having
86 | if (allKeywords.length === 0) {
87 | notesForUser.push(`Add some keywords to your package.json. We've added an empty \`keywords\` field for now.`)
88 | } else {
89 | notesForUser.push(`Check the \`package.json\` keywords. We added these from your GitHub topics: ${arrToQuotedArr(pkg.keywords)}.`)
90 | }
91 | }
92 | if (allKeywords.length !== 0 && !deepEqual(allKeywords, topics)) {
93 | const {data} = await github.put(`/repos/${github.repoName}/topics`, {
94 | names: allKeywords
95 | }).catch(err => {
96 | console.robowarn('Unable to set GitHub topics using `package.json` keywords. Probably a permissions error.')
97 | notesForUser.push(`Add these keywords (from your \`package.json\`) as GitHub topics to your repo: ${arrToQuotedArr(allKeywords)}.`)
98 | if (err) {
99 | return false
100 | }
101 | })
102 | if (data && data.topics === allKeywords) {
103 | console.robolog('I set the GitHub topics to include all `package.json` keywords.')
104 | notesForUser.push(`Check your GitHub topics. I added some from your \`package.json\` keywords.`)
105 | }
106 | }
107 | }
108 |
109 | module.exports = {
110 | lint,
111 | checkHomepage,
112 | checkRepository,
113 | checkKeywords,
114 | checkDescription
115 | }
116 |
117 | async function lint (github, pkg) {
118 | const notesForUser = []
119 |
120 | // Add in the headers we need for these calls
121 | githubPreviewHeaders.map(header => github.defaults.headers.common.accept.push(header))
122 |
123 | const {data: {description, topics, license}} = await github.get(`/repos/${github.repoName}`)
124 |
125 | // Check the GitHub and npm descriptions
126 | await checkDescription(github, pkg, description, notesForUser)
127 |
128 | // Check that the keywords match GitHub topics
129 | await checkKeywords(github, pkg, topics, notesForUser)
130 |
131 | // Check that the homepage exists
132 | checkHomepage(github, pkg, notesForUser)
133 |
134 | // Check that `bugs` matches GitHub URL
135 | checkBugs(github, pkg, notesForUser)
136 |
137 | if (!pkg.license) {
138 | pkg.license = 'MIT'
139 | notesForUser.push(`Check the license in your \`package.json\`. We added "MIT" for now.`)
140 | } else if (license && pkg.license !== license.spdx_id) {
141 | // Check that the license matches
142 | notesForUser.push(`Update the license in your \`package.json\`. It did not match what we found on GitHub, and we were unable to resolve this.`)
143 | }
144 |
145 | // Check that the repository matches
146 | checkRepository(github, pkg, notesForUser)
147 |
148 | if (!pkg.contributors) {
149 | pkg.contributors = [pkg.author]
150 | notesForUser.push(`If there are more contributors, add them to the Contributors field in the \`package.json\`.`)
151 | }
152 |
153 | // Tests
154 | if (!pkg.scripts || !pkg.scripts.test || pkg.scripts.test.indexOf('no test specified') !== -1) {
155 | notesForUser.push(`Add some tests! There aren't any currently set. [Use this link to stub out an issue.](https://github.com/${github.repoName}/issues/new?title=Add%20Tests&body=Tests%20are%20useful%20for%20ensuring%20code%20quality.%20No%20tests%20were%20found%20in%20the%20package%20manifest.)`)
156 | // TODO Open an issue suggesting that they add tests
157 | // const query = querystring.stringify({
158 | // type: 'issue',
159 | // is: 'open',
160 | // repo: github.repoName
161 | // }, 'tests', ':')
162 |
163 | // const testIssueResult = await github.get(`/search/issues?q=${query}`).catch(err => err)
164 | // console.log('Here', testIssueResult)
165 |
166 | // const result = await github.post(`/repos/${github.repoName}/issues`, {
167 | // title: 'Add tests',
168 | // body: `No tests are specified in the npm manifest. Do you have tests for this repo yet?`
169 | // })
170 | //
171 | // const {data: {html_url: issueUrl}} = result
172 | // console.log(`🤖🙏 issue opened as a reminder to add tests: ${issueUrl}`)
173 | }
174 |
175 | return {pkg, notesForUser}
176 | }
177 |
--------------------------------------------------------------------------------
/lib/robo.js:
--------------------------------------------------------------------------------
1 | console.robolog = function (message) {
2 | return console.log('🤖 ' + message)
3 | }
4 |
5 | console.robofire = function (message) {
6 | return console.log('🔥 ' + message)
7 | }
8 |
9 | console.robowarn = function (message) {
10 | return console.log('⚠️ ' + message)
11 | }
12 |
13 | module.exports = console
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "build-a-space",
3 | "version": "1.0.1",
4 | "description": "Automatically add community documentation to your repository",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "ava test/*.js"
8 | },
9 | "bin": "./cli.js",
10 | "keywords": [
11 | "api",
12 | "automatic",
13 | "bot",
14 | "community",
15 | "github"
16 | ],
17 | "author": "Richard Littauer (https://burntfen.com)",
18 | "license": "MIT",
19 | "dependencies": {
20 | "atob": "^2.1.2",
21 | "axios": "^0.17.0",
22 | "btoa": "^1.1.2",
23 | "deep-equal": "^1.0.1",
24 | "git-remote-origin-url": "^2.0.0",
25 | "git-remote-upstream-url": "^2.0.0",
26 | "json-diff": "^0.5.2",
27 | "mdast-util-heading-range": "^2.1.0",
28 | "meow": "^4.0.0",
29 | "mz": "^2.7.0",
30 | "opn": "^5.1.0",
31 | "querystring": "^0.2.0",
32 | "remark-parse": "^4.0.0",
33 | "standard": "^13.0.1",
34 | "unified": "^6.1.5",
35 | "unist-util-position": "^3.0.0",
36 | "vfile": "^2.2.0"
37 | },
38 | "devDependencies": {
39 | "ava": "^0.24.0"
40 | },
41 | "homepage": "https://github.com/mntnr/build-a-space",
42 | "bugs": "https://github.com/mntnr/build-a-space/issues",
43 | "repository": {
44 | "type": "git",
45 | "url": "https://github.com/mntnr/build-a-space.git"
46 | },
47 | "contributors": [
48 | "Richard Littauer (https://burntfen.com)"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/src/checkers/community.js:
--------------------------------------------------------------------------------
1 | const btoa = require('btoa')
2 | const fs = require('mz/fs')
3 | const path = require('path')
4 | const {bunchFiles} = require('../../lib/githubHelpers.js')
5 | const getSection = require('../utilities/get-section')
6 |
7 | module.exports = async function wrap (github, opts) {
8 | const files = await community(github, opts)
9 | return bunchFiles(github, files, {
10 | test: opts.test,
11 | message: `docs: adding community docs`
12 | })
13 | }
14 |
15 | async function addToBranch (github, file, toCheck, opts) {
16 | let email = opts.email || github.user.email || '[INSERT EMAIL ADDRESS]'
17 | let licensee = opts.licensee || '[INSERT LICENSEE]'
18 |
19 | // Check if file exists already in the branch
20 | const {status} = await github.get(`/repos/${github.targetRepo}/contents/${file.filePath}?branch=${github.branchName}`)
21 | .catch(err => err)
22 | if (status !== 200) {
23 | let fileContent
24 |
25 | if (file.name === 'readme') {
26 | fileContent = Buffer.from(`# ${github.repoName.split('/')[1]}
27 |
28 | TODO This needs to be filled out!`)
29 |
30 | console.robowarn('You need to fill out the README manually!')
31 |
32 | file.content = fileContent.toString('base64')
33 | } else {
34 | if (file.name === 'contributing' && opts.contributing) {
35 | fileContent = await fs.readFileSync(path.join(__dirname, '../../', opts.contributing)).toString('utf8')
36 | } else {
37 | const {data} = await github.get(`/repos/${github.repoName}/readme`)
38 | const readme = Buffer.from(data.content, 'base64')
39 | const filePath = path.join(__dirname, '..', '..', 'fixtures', file.filePath)
40 | fileContent = getSection(readme, 'contribute') || await fs.readFileSync(filePath, 'utf8')
41 | }
42 |
43 | // Text Replacements
44 | if (file.name === 'code_of_conduct') {
45 | fileContent = fileContent.replace('[INSERT EMAIL ADDRESS]', email)
46 | file.note.push(`Check the email in the Code of Conduct. We've added in "${email}".`)
47 | } else if (file.name === 'license') {
48 | fileContent = fileContent.replace('[INSERT LICENSEE]', licensee)
49 | file.note.push(`Check the licensee in the License. We've licensed this to "${licensee}".`)
50 | } else if (file.name === 'contributing') {
51 | fileContent = fileContent.replace('[GITHUB REPONAME]', github.repoName)
52 | }
53 |
54 | file.content = btoa(fileContent)
55 | }
56 |
57 | toCheck.push(file)
58 | }
59 | }
60 |
61 | async function community (github, opts) {
62 | // what is the community vitality like?
63 | const {data: community} = await github.get(`/repos/${github.repoName}/community/profile`)
64 | .catch(err => {
65 | console.robofire('Unable to get community profile. Check your headers.')
66 | return err
67 | })
68 |
69 | const defaultChecks = [
70 | {
71 | name: 'readme',
72 | filePath: 'README.md',
73 | note: ['Update your README. It is only specced out, and will need more work. I suggest [standard-readme](https://github.com/RichardLitt/standard-readme) for this.']
74 | },
75 | {
76 | name: 'license',
77 | // TODO You don't need all caps for License, and it doesn't need to be a markdown file
78 | filePath: 'LICENSE',
79 | note: [`Check that the license name and year is correct. I've added the MIT license, which should suit most purposes.`]
80 | },
81 | // TODO Parse in the Contributing section from the README
82 | {
83 | name: 'contributing',
84 | filePath: 'CONTRIBUTING.md',
85 | note: ['Update the Contributing guide to include any repository-specific requests, or to point to a global Contributing document.']
86 | },
87 | {
88 | name: 'code_of_conduct',
89 | filePath: 'CODE_OF_CONDUCT.md',
90 | note: []
91 | }
92 | ]
93 |
94 | let toCheck = []
95 |
96 | await Promise.all(defaultChecks.map(async file => {
97 | if (community && !community.files[file.name]) {
98 | await addToBranch(github, file, toCheck, opts)
99 | }
100 | }))
101 |
102 | return toCheck
103 | }
104 |
--------------------------------------------------------------------------------
/src/checkers/javascript.js:
--------------------------------------------------------------------------------
1 | const btoa = require('btoa')
2 | const lintPackageJson = require('../../lib/lintPackageJson.js')
3 | const {checkStatus, sameOnBranch, commitFile} = require('../../lib/githubHelpers.js')
4 |
5 | module.exports = async function addJavascriptFiles (github) {
6 | // Is this a JS repo?
7 | const {data: {language}} = await github.get(`/repos/${github.repoName}`)
8 | if (language !== 'JavaScript') return
9 |
10 | console.robolog('Assuming this is a JavaScript repository, checking...')
11 |
12 | const toCheck = []
13 |
14 | const packageFile = {
15 | name: 'package.json',
16 | path: 'package.json',
17 | note: [`Check that nothing drastic happened in the \`package.json\`.`]
18 | }
19 |
20 | // Abandon if there is no package.json
21 | const npmStatus = await checkStatus(github, packageFile)
22 | if (npmStatus === 404) {
23 | console.robowarn('There is no package.json. Is this not checked into npm?')
24 | packageFile.note = [`You have no package.json file checked in that I can find. I am assuming this isn't published on npm. Did I miss something?`]
25 | toCheck.push(packageFile)
26 | } else {
27 | const {data: npm} = await github.get(`/repos/${github.repoName}/contents/package.json?branch=${github.branchName}`)
28 | const pkg = JSON.parse(Buffer.from(npm.content, 'base64'))
29 | // Do the heavylifting in the lintPackage file
30 | const {pkg: newPkg, notesForUser} = await lintPackageJson.lint(github, pkg)
31 | packageFile.note = packageFile.note.concat(notesForUser)
32 | packageFile.content = btoa(JSON.stringify(newPkg, null, 2))
33 |
34 | if (!await sameOnBranch(github, packageFile)) {
35 | await commitFile(github, {
36 | content: packageFile.content,
37 | message: `chore: updated fields in the package.json
38 |
39 | ${packageFile.note.map(note => `- ${note}`).join('\n')}
40 | `,
41 | path: packageFile.path,
42 | name: packageFile.name
43 | })
44 |
45 | toCheck.push(packageFile)
46 | }
47 | }
48 |
49 | // TODO Open issue to enable greenkeeper
50 | return toCheck
51 | }
52 |
--------------------------------------------------------------------------------
/src/checkers/travis.js:
--------------------------------------------------------------------------------
1 | const fs = require('mz/fs')
2 | const path = require('path')
3 | const { checkStatus, sameOnBranch, commitFile } = require('../../lib/githubHelpers.js')
4 |
5 | module.exports = async function checkTravis (github, opts) {
6 | if (!opts.travis) {
7 | console.robolog('Ignoring Travis file')
8 | return []
9 | }
10 |
11 | const toCheck = []
12 |
13 | // If travis file exists
14 | const travisFile = {
15 | name: 'travis',
16 | path: '.travis.yml',
17 | note: ['Check if .travis.yml was overwritten or not.']
18 | }
19 |
20 | // Only commit if there is already a travis file
21 | const travisStatus = await checkStatus(github, travisFile)
22 | if (travisStatus === 404) {
23 | travisFile.note = [`Consider adding Travis. Travis is useful not only for tests, but also for [greenkeeper](https://greenkeeper.io/) and [semantic-release](https://github.com/semantic-release/semantic-release).`]
24 | toCheck.push(travisFile)
25 | } else {
26 | // Get the content of the template
27 | travisFile.content = await fs.readFileSync(path.join(__dirname, `../../fixtures/js/${travisFile.path}`)).toString('base64')
28 | // If it isn't the same on the branch
29 | if (!await sameOnBranch(github, travisFile)) {
30 | // Commit
31 | await commitFile(github, {
32 | name: travisFile.name,
33 | path: travisFile.path,
34 | message: 'ci: adding travis file with Greenkeeper and semantic-release enabled',
35 | content: travisFile.content
36 | })
37 |
38 | // And return the final object for travis
39 | toCheck.push(travisFile)
40 | }
41 | }
42 |
43 | return toCheck
44 | }
45 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const env = require('../env')
3 | const console = require('../lib/robo')
4 | const querystring = require('querystring')
5 | const checkCommunity = require('./checkers/community')
6 | const checkJavascript = require('./checkers/javascript')
7 | const checkTravis = require('./checkers/travis')
8 | const messages = require('./messages')
9 | const github = axios.create({
10 | baseURL: env.BASE_URL,
11 | headers: {
12 | common: {
13 | authorization: `token ${env.BUILD_A_SPACE}`,
14 | accept: [
15 | `application/vnd.github.black-panther-preview+json`, // Community https://developer.github.com/v3/community
16 | `application/vnd.github.scarlet-witch-preview+json` // CoC https://developer.github.com/v3/codes_of_conduct/
17 | ]
18 | }
19 | }
20 | })
21 | const opn = require('opn')
22 |
23 | module.exports = async function index (repoName, opts) {
24 | // https://docs.travis-ci.com/user/environment-variables/
25 | // TODO Set a sensible repo env
26 | github.repoName = repoName || env.TRAVIS_REPO_SLUG
27 |
28 | // Validate that the repository is actually a repo
29 | await github.get(`/repos/${github.repoName}`)
30 | .catch(err => {
31 | if (err) {
32 | console.robofire('That is not a valid GitHub repository!')
33 | console.log('')
34 | process.exit(1)
35 | }
36 | })
37 |
38 | console.robolog(`Starting process...`)
39 |
40 | // who am I?
41 | const {data: user} = await github.get('/user')
42 | console.robolog(`Authenticated as ${user.login}. Looking if I already created a pull request.`)
43 | github.user = user
44 |
45 | github.defaultBranch = opts.branch || 'master'
46 |
47 | // Start a pull request
48 | const currentSha = await initPR(github, opts)
49 |
50 | // Create a new fork if we need to
51 | if (opts.fork) {
52 | github.targetRepo = await getOrCreateFork(github, opts)
53 | } else {
54 | github.targetRepo = github.repoName
55 | }
56 |
57 | // Create or use an existing branch
58 | await getOrCreateBranch(github, currentSha)
59 |
60 | // Check the community files
61 | const communityFiles = await checkCommunity(github, opts)
62 | // Check the JavaScript files
63 | const jsFiles = await checkJavascript(github, opts)
64 | const travisFile = await checkTravis(github, opts)
65 |
66 | // Create a pullrequest, and combine notes for the enduser
67 | await createPullRequest(github, communityFiles.concat(jsFiles, travisFile), opts)
68 | }
69 |
70 | async function initPR (github, opts) {
71 | // Do I have a pending pull request?
72 | const query = querystring.stringify({
73 | type: 'pr',
74 | author: github.user.login,
75 | is: 'open',
76 | repo: github.repoName
77 | }, ' ', ':')
78 |
79 | const {data: pullRequestsResult} = await github.get(`/search/issues?q=${query}`)
80 | const pullRequestNumbers = pullRequestsResult.items.map(pr => pr.number)
81 |
82 | // if there are more than a single pull request, then we have a problem, because
83 | // I don’t know which one to update. So I’ll ask you for help :)
84 | if (pullRequestsResult.total_count > 1) {
85 | console.robolog('🤖🆘 I don’t know how to handle more than one pull requests. Creating an issue.')
86 | if (!opts.test) {
87 | const result = await github.post(`/repos/${github.repoName}/issues`, {
88 | title: messages.issue.title,
89 | body: messages.issue.body(pullRequestNumbers)
90 | })
91 | const {data: {html_url: issueUrl}} = result
92 | console.robolog(`🤖🙏 issue created: ${issueUrl}`)
93 | } else {
94 | console.robolog(`🤖🙏 issue not created, because test.`)
95 | }
96 | process.exit(1)
97 | }
98 |
99 | if (pullRequestsResult.total_count === 1) {
100 | const pullRequest = pullRequestsResult.items[0]
101 | console.robolog(`Existing pull-request found: ${pullRequest.html_url}`)
102 |
103 | const {data} = await github.get(`/repos/${github.repoName}/pulls/${pullRequest.number}`)
104 |
105 | if (data.head.ref.indexOf('docs') === -1) {
106 | console.robofire(`Existing branch doesn't look like it was made by this tool! Abort!`)
107 | console.log()
108 | process.exit(1)
109 | }
110 |
111 | github.branchName = data.head.ref // as branchName
112 | // TODO Enable existing pull request to be fixed and added to
113 | // await updateFixtures({diffs, github, github.repoName, branchName})
114 | // console.robolog(`pull-request updated: ${pullRequest.html_url}`)
115 | } else {
116 | console.robolog('No existing pull request found')
117 | github.branchName = `docs/${new Date().toISOString().substr(0, 10)}`
118 | }
119 |
120 | console.robolog(`Looking for last commit sha of ${github.repoName}/git/refs/heads/${github.defaultBranch}`)
121 | const {data: {object: {sha}}} = await github.get(`/repos/${github.repoName}/git/refs/heads/${github.defaultBranch}`)
122 |
123 | return sha
124 | }
125 |
126 | async function getOrCreateFork (github, opts) {
127 | // List forks
128 | const repoOnly = github.repoName.split('/')[1]
129 |
130 | const {data: forks} = await github.get(`/repos/${github.repoName}/forks`)
131 | // Filter forks owner - if it matches github.owner, use that fork
132 | const ownFork = forks.filter(fork => fork.owner.login === github.user.login)
133 | // Return if it does exist
134 | if (ownFork.length !== 0) {
135 | console.robolog(`Using existing fork: ${github.user.login}/${repoOnly}.`)
136 | } else {
137 | var error
138 | // Create it if it don't exist ya'll
139 | // This doesn't seem to be working at all
140 | if (opts.test) {
141 | console.robofire(`Refusing to create fork, because tests.`)
142 | } else {
143 | console.log(github.user.login, repoOnly)
144 | await github.post(`/repos/${github.repoName}/forks`)
145 | .catch(err => {
146 | if (err) {
147 | error = true
148 | console.robofire(`Unable to create a new fork for ${github.user.login}!`)
149 | console.log(err)
150 | }
151 | })
152 | if (!error) {
153 | console.robolog(`Created new fork: ${github.user.login}/${repoOnly}.`)
154 | }
155 | }
156 | }
157 | return `${github.user.login}/${repoOnly}`
158 | }
159 |
160 | async function getOrCreateBranch (github, sha) {
161 | // Gets a 422 sometimes
162 | const branchExists = await github.get(`/repos/${github.targetRepo}/branches/${github.branchName}`)
163 | .catch(err => {
164 | if (err) {
165 | console.robolog(`Creating new branch on ${github.targetRepo}: ${github.branchName} using last sha ${sha.slice(0, 7)}`)
166 | } // do nothing
167 | })
168 | if (!branchExists) {
169 | await github.post(`/repos/${github.targetRepo}/git/refs`, {
170 | ref: `refs/heads/${github.branchName}`,
171 | sha
172 | }).catch(err => {
173 | if (err) {}
174 | console.robofire('Unable to create a new branch. Do you have access?')
175 | console.log('')
176 | process.exit(1)
177 | })
178 | } else {
179 | console.robolog(`Using existing branch on ${github.targetRepo}: ${github.branchName} using last sha ${sha.slice(0, 7)}`)
180 | }
181 | }
182 |
183 | async function createPullRequest (github, files, opts) {
184 | if (github.branchName === github.defaultBranch) {
185 | console.robolog(`No changes (you've run this already), or there is some other issue.`)
186 | console.log()
187 | return
188 | }
189 |
190 | // Are there any commits?
191 | const {data: {commit: {sha: oldBranch}}} = await github.get(`/repos/${github.repoName}/branches/${github.defaultBranch}`)
192 | const {data: {commit: {sha: newBranch}}} = await github.get(`/repos/${github.targetRepo}/branches/${github.branchName}`)
193 | if (oldBranch === newBranch) {
194 | console.robofire(`Unable to create PR because there is no content.`)
195 | console.log()
196 | return
197 | }
198 |
199 | console.robolog(`Creating pull request`)
200 |
201 | if (opts.test) {
202 | console.robolog(`Pull request not created, because tests.`)
203 | } else {
204 | const res = await github.post(`/repos/${github.repoName}/pulls`, {
205 | title: messages.pr.title,
206 | // Where changes are implemented. Format: `username:branch`.
207 | head: `${github.targetRepo.split('/')[0]}:${github.branchName}`,
208 | // TODO Use default_branch across tool, not just `master` branch
209 | base: github.defaultBranch,
210 | body: messages.pr.body(files)
211 | }).catch(async err => {
212 | console.robofire(`Unable to create PR inexplicably.`, err)
213 | })
214 | if (res) {
215 | console.robolog(`Pull request created: ${res.data.html_url}`)
216 | if (opts.open) {
217 | await opn(res.data.html_url, {wait: false})
218 | }
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/messages.js:
--------------------------------------------------------------------------------
1 | const {joinNotes} = require('../lib/helpers')
2 |
3 | module.exports = {
4 | 'issue': {
5 | 'title': '🤖🆘 Too many PRs',
6 | 'body': function (prNumbers) {
7 | return `Dearest humans,
8 |
9 | I've run into a problem here. I am trying to update the community docs and to get this repo up-to-scratch. I would usually create a new pull request to let you know about it, or update an existing one. But now there more than one: ${prNumbers.map(number => `#${number}`).join(', ')}
10 |
11 | I don’t know how that happened, did I short-circuit again?
12 |
13 | You could really help me by closing all pull requests or by leaving open the one you want me to keep updating.
14 |
15 | Hope you can fix it (and my circuits) soon. 🙏
16 |
17 | Note: This pull request was automatically generated by https://github.com/mntnr/build-a-space. Found an issue? [Let me know!](https://github.com/mntnr/build-a-space/issues)
18 | `
19 | }
20 | },
21 | 'pr': {
22 | 'title': 'Add community documentation',
23 | 'body': function (files) {
24 | return `Dearest humans,
25 |
26 | You are missing some important community files. I am adding them here for you!
27 |
28 | Here are some things you should do manually before merging this Pull Request:
29 |
30 | ${joinNotes(files)}
31 |
32 | Note: This pull request was automatically generated by https://github.com/mntnr/build-a-space. Found an issue? [Let me know!](https://github.com/mntnr/build-a-space/issues)
33 | `
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/utilities/find-section.js:
--------------------------------------------------------------------------------
1 | const position = require('unist-util-position')
2 | const range = require('mdast-util-heading-range')
3 |
4 | module.exports = find
5 |
6 | function find (heading, opts, callback) {
7 | if (callback === undefined) {
8 | callback = opts
9 | opts = undefined
10 | }
11 |
12 | return attacher
13 |
14 | function attacher () {
15 | return transformer
16 | }
17 |
18 | function transformer (tree, file) {
19 | var found = false
20 |
21 | /* Search for `heading`, call `onfind` if found. */
22 | range(tree, heading, onfind)
23 |
24 | /* Call `callback` if not found. */
25 | if (!found) {
26 | callback()
27 | }
28 |
29 | function onfind (start, nodes) {
30 | /* Include the heading if given `includeHeading`. */
31 | var heading = (opts || {}).includeHeading === true
32 | var begin = heading ? start : nodes[0]
33 | var initial = position.start(begin).offset
34 | var final = position.end(nodes[nodes.length - 1]).offset
35 |
36 | /* If there’s content, call `callback` with the doc. */
37 | if (initial !== undefined && final !== undefined) {
38 | found = true
39 | callback(file.toString('utf8').slice(initial, final))
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/utilities/get-section.js:
--------------------------------------------------------------------------------
1 | const vfile = require('vfile')
2 | const unified = require('unified')
3 | const parse = require('remark-parse')
4 | const find = require('./find-section')
5 |
6 | module.exports = getSection
7 |
8 | function getSection (buf, title) {
9 | const file = vfile(buf)
10 | let section
11 |
12 | // Use the section from the readme, if there is one.
13 | const processor = unified()
14 | .use(parse)
15 | .use(find(title, {includeHeading: true}, found))
16 |
17 | // Parse and process the readme
18 | processor.runSync(processor.parse(file), file)
19 |
20 | return section
21 |
22 | function found (doc) {
23 | section = doc
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/test/fixtures/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at test@example.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/test/fixtures/readme-with-contributing.md:
--------------------------------------------------------------------------------
1 | # Some title
2 |
3 | > Some description
4 |
5 | ## Contribute
6 |
7 | Some longer description.
8 |
9 | ## License
10 |
11 | MIT © Richard McRichface
12 |
--------------------------------------------------------------------------------
/test/lint.js:
--------------------------------------------------------------------------------
1 | const test = require('ava').test
2 | const lint = require('../lib/lintPackageJson.js')
3 | const axios = require('axios')
4 | const console = require('../lib/robo')
5 |
6 | // Silence all consoles. Brittle, but works.
7 | if (process.env.npm_config_argv.indexOf('--verbose') === -1) {
8 | console.robolog = console.robofire = console.robowarn = () => {}
9 | }
10 |
11 | const github = axios.create({
12 | baseURL: 'https://api.example.com',
13 | headers: {
14 | common: {}
15 | }
16 | })
17 |
18 | // checkHomepage
19 | test('homepage will update if it does not exist', async t => {
20 | const notes = []
21 | const pkg = {}
22 | github.repoName = 'test/test'
23 | await lint.checkHomepage(github, pkg, notes)
24 | t.is(pkg.homepage, 'https://github.com/test/test')
25 | t.is(notes[0], 'Check that the homepage in the `package.json` is OK. Another one besides your GitHub repo might work. We\'ve set it to https://github.com/test/test.')
26 | })
27 |
28 | test('homepage will not update if it does exist', async t => {
29 | const pkg = {homepage: 'test.com'}
30 | await lint.checkHomepage(github, pkg, [])
31 | t.is(pkg.homepage, 'test.com')
32 | })
33 |
34 | // checkRepository
35 | test('repository will update if it does not exist', async t => {
36 | const pkg = {}
37 | await lint.checkRepository(github, pkg, [])
38 | t.deepEqual(pkg.repository, {
39 | 'type': 'git',
40 | 'url': `https://github.com/test/test.git`
41 | })
42 | })
43 |
44 | test('repository will not update if it does exist', async t => {
45 | const pkg = {repository: true}
46 | await lint.checkRepository(github, pkg, [])
47 | t.is(pkg.repository, true)
48 | })
49 |
50 | test('repository will add a note it does exist and doesnt match', async t => {
51 | const pkg = {repository: true}
52 | const notes = []
53 | await lint.checkRepository(github, pkg, notes)
54 | t.is(notes[0], 'We expected the repository url in the `package.json` to be https://github.com/test/test, and it wasn\'t. Is this intentional?')
55 | })
56 |
57 | test('homepage will not add a note if does exist and is expected', async t => {
58 | const pkg = {
59 | 'repository': {
60 | 'type': 'git',
61 | 'url': `https://github.com/test/test.git`
62 | }
63 | }
64 | const notes = []
65 | await lint.checkRepository(github, pkg, notes)
66 | t.deepEqual(notes, [])
67 | })
68 |
69 | // checkKeywords
70 | // TODO Find ways of hitting actual API and testing these
71 | // TODO Finish testing checkKeywords
72 | test('checkKeywords will do nothing if there are no topics', async t => {
73 | const github = null
74 | const pkg = {keywords: []}
75 | const notesForUser = []
76 | const topics = []
77 | await lint.checkKeywords(github, pkg, topics, notesForUser)
78 | t.deepEqual(pkg.keywords, [])
79 | t.deepEqual(notesForUser, [])
80 | })
81 |
82 | test('checkKeywords will create a keywords field if it doesn\'t exist', async t => {
83 | const github = null
84 | const pkg = {}
85 | const notesForUser = []
86 | const topics = []
87 | await lint.checkKeywords(github, pkg, topics, notesForUser)
88 | t.deepEqual(pkg.keywords, [])
89 | })
90 |
91 | test('checkKeywords will return nothing if github is not working', async t => {
92 | github.repoName = 'test'
93 | const pkg = {keywords: ['fail']}
94 | const topics = ['test']
95 | const notesForUser = []
96 | const err = await lint.checkKeywords(github, pkg, topics, notesForUser)
97 | t.is(notesForUser.includes('Add these keywords (from your `package.json`) as GitHub topics to your repo: "test", "fail".'), true)
98 | t.is(err, undefined)
99 | })
100 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const test = require('ava').test
2 | const fs = require('fs')
3 | const path = require('path')
4 | const vfile = require('vfile')
5 | const unified = require('unified')
6 | const parse = require('remark-parse')
7 | const getSection = require('../src/utilities/get-section')
8 | const findSection = require('../src/utilities/find-section')
9 |
10 | test('utilities/find-section', t => {
11 | const buf = fs.readFileSync(path.join(__dirname, 'fixtures', 'readme-with-contributing.md'))
12 |
13 | t.is(
14 | getSection(buf, 'missing'),
15 | undefined,
16 | 'should return `undefined` if a section is missing'
17 | )
18 |
19 | t.is(
20 | getSection(buf, 'contribute'),
21 | [
22 | '## Contribute',
23 | '',
24 | 'Some longer description.'
25 | ].join('\n'),
26 | 'should find a section in a buffer'
27 | )
28 |
29 | t.is(
30 | getSection(buf, 'license'),
31 | [
32 | '## License',
33 | '',
34 | 'MIT © Richard McRichface'
35 | ].join('\n'),
36 | 'should find other sections'
37 | )
38 |
39 | t.is(
40 | getSection(String(buf), 'contribute'),
41 | [
42 | '## Contribute',
43 | '',
44 | 'Some longer description.'
45 | ].join('\n'),
46 | 'should find a section in a string'
47 | )
48 | })
49 |
50 | test('utilities/find-section', t => {
51 | const filePath = path.join(__dirname, 'fixtures', 'readme-with-contributing.md')
52 | const buf = fs.readFileSync(filePath)
53 | const file = vfile({path: filePath, contents: buf})
54 | const tree = unified().use(parse).parse(file)
55 |
56 | t.plan(3)
57 |
58 | findSection('missing', function (section) {
59 | t.is(
60 | section,
61 | undefined,
62 | 'should invoke with `undefined` if a section is missing'
63 | )
64 | })()(tree, file)
65 |
66 | findSection('contribute', function (section) {
67 | t.is(
68 | section,
69 | 'Some longer description.',
70 | 'should find a section'
71 | )
72 | })()(tree, file)
73 |
74 | findSection('contribute', {includeHeading: true}, function (section) {
75 | t.is(
76 | section,
77 | [
78 | '## Contribute',
79 | '',
80 | 'Some longer description.'
81 | ].join('\n'),
82 | 'should include the heading if given `includeHeading: true`'
83 | )
84 | })()(tree, file)
85 | })
86 |
--------------------------------------------------------------------------------