Skip to content

Commit

Permalink
🌱 Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
re-fort committed Sep 17, 2017
0 parents commit 75004c4
Show file tree
Hide file tree
Showing 21 changed files with 2,035 additions and 0 deletions.
43 changes: 43 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
version: 2
jobs:
build:
docker:
- image: circleci/node:8.5.0

working_directory: ~/tmp

branches:
only:
- master

steps:
- checkout

# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# Fallback to using the latest cache if no exact match is found
- v1-dependencies-

# Install dependencies
- run:
name: Install
command: yarn install

- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}

# Lint
- run:
name: Lint
command: yarn run lint

# Test
- run:
name: Test
command: |
touch .env
yarn test
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
24 changes: 24 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Note: if you set env vars on Heroku, the following vars are overwritten
USER_NAME=vuejs-jp-bot
EMAIL=vuejs-jp-bot@users.noreply.github.com

ORIGIN_REPO_URL=git@github.com:vuejs-jp-bot/jp.vuejs.org.git
ORIGIN_REPO_DEFAULT_BRANCH=lang-ja

UPSTREAM_REPO_URL=https://github.com/re-fort/jp.vuejs.org.git
UPSTREAM_REPO_DEFAULT_BRANCH=lang-ja

HEAD_REPO_URL=https://github.com/vuejs/vuejs.org.git
HEAD_REPO_DEFAULT_BRANCH=master

FEED_URL=https://github.com/vuejs/vuejs.org/commits/master.atom
FEED_REFRESH=60000

GITHUB_ACCESS_TOKEN=XXXXXXXX
SLACK_TOKEN=XXXXXXXX
SLACK_CHANNEL=XXXXXXXX

# For test
GITHUB_TEST_ACCESS_TOKEN=XXXXXXXX
SLACK_TEST_TOKEN=XXXXXXXX
SLACK_TEST_CHANNEL=XXXXXXXX
20 changes: 20 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"env": {
"es6": true,
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module"
},
"rules": {
"comma-dangle": ["warn", "always-multiline"],
"no-extra-semi": "warn",
"no-undef": "error",
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
"quotes": ["warn", "single"],
"semi": "off",
"space-before-blocks": ["warn", { "functions": "always", "keywords": "always", "classes": "always" }]
}
}
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
.idea
.env
node_modules/
repo/*
npm-debug.log
yarn-error.log
!.gitkeep
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
worker: touch .env && yarn run watch
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[![CircleCI](https://circleci.com/gh/re-fort/che-tsumi.svg?style=shield&circle-token=6e0d820d5783c1d12c06aa65afa447463f470467)](https://circleci.com/gh/re-fort/che-tsumi)

che-tsumi
======================

## Flow
1. Watch RSS feed(e.g. https://github.com/vuejs/vuejs.org/commits/master.atom)
1. Try to cherry-pick each commit when new feed items are detected
1. Create a new pull request if succeed
1. Add a reaction on Slack channel
96 changes: 96 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const RssFeedEmitter = require('rss-feed-emitter')
const Github = require('./lib/github')
const Repo = require('./lib/repository')
const Slack = require('./lib/slack')
const Utility = require('./lib/utility')

let feeder = new RssFeedEmitter()
let github = new Github()
let slack = new Slack({ token: process.env.SLACK_TOKEN })

let remote = {
origin: {
url: process.env.ORIGIN_REPO_URL,
owner: Utility.extractRepoOwner(process.env.ORIGIN_REPO_URL),
name: Utility.extractRepoName(process.env.ORIGIN_REPO_URL),
defaultBranch: process.env.ORIGIN_REPO_DEFAULT_BRANCH,
},
upstream: {
url: process.env.UPSTREAM_REPO_URL,
owner: Utility.extractRepoOwner(process.env.UPSTREAM_REPO_URL),
name: Utility.extractRepoName(process.env.UPSTREAM_REPO_URL),
defaultBranch: process.env.UPSTREAM_REPO_DEFAULT_BRANCH,
},
head: {
url: process.env.HEAD_REPO_URL,
name: Utility.extractRepoName(process.env.HEAD_REPO_URL),
defaultBranch: process.env.HEAD_REPO_DEFAULT_BRANCH,
},
}

let repo = new Repo(
{
path: 'repo',
remote,
user: {
name: process.env.USER_NAME,
email: process.env.EMAIL,
},
}
)

function setup() {
repo.setup()
github.authenticate({ type: 'token', token: process.env.GITHUB_ACCESS_TOKEN })
setupFeeder()
}

function setupFeeder() {
feeder.add({
url: process.env.FEED_URL,
refresh: Number(process.env.FEED_REFRESH),
})

feeder.on('new-item', function(item) {
Utility.log('I', `New commit: ${item.title}`)
let hash = Utility.extractBasename(item.link)
// branch names consisting of 40 hex characters are not allowed
let shortHash = hash.substr(0, 8)

if (repo.existsRemoteBranch(shortHash)) {
Utility.log('W', `Remote branch already exists: ${shortHash}`)
return
}

repo.fetchAllRemotes()
repo.updateLocal()
repo.createNewBranch(shortHash)

if (repo.hasConflicts('cherry-pick', hash)) {
Utility.log('W', 'Conflicts occurred. Please make a pull request by yourself')
repo.resetChanges()
} else {
Utility.log('S', `Fully merged: ${shortHash}`)
repo.updateRemote()
after(item, hash, shortHash)
}
})
}

async function after(item, hash, shortHash) {
const { data: pullRequest } = await github.createPullRequest(remote, { title: item.title, body: `Cherry picked from ${item.link}`, branch: shortHash })
Utility.log('S', `Created new pull request: ${pullRequest.html_url}`)

const { messages } = await slack.searchMessages({ query: hash })
if (!messages) return
let message = messages.matches.find(extractMatchedMessage)
if (!message) return
const { ok: result } = await slack.addReactions({ name: 'raising_hand', channel: process.env.SLACK_CHANNEL, timestamp: message.ts })
if (result) Utility.log('S', `Add reaction: ${message.permalink}`)
}

function extractMatchedMessage(message) {
return message.text !== '' && message.channel.id === process.env.SLACK_CHANNEL && message.username === 'recent commits to vuejs.org:master'
}

setup()
33 changes: 33 additions & 0 deletions lib/git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const shell = require('shelljs')
shell.config.silent = true

class Git {
constructor() {
}

static exec(command, execOptions) {
return shell.exec(`git ${command}`, execOptions)
}

static checkout(branch, option = '', execOptions) {
return this.exec(`checkout ${option} ${branch}`, execOptions)
}

static fetch(repo, branch, option = '', execOptions) {
return this.exec(`fetch ${option} ${repo} ${branch}`, execOptions)
}

static merge(branch, option = '', execOptions) {
return this.exec(`merge ${option} ${branch}`, execOptions)
}

static push(repo, branch, option = '', execOptions) {
return this.exec(`push ${option} ${repo} ${branch}`, execOptions)
}

static cherryPick(hash, option = '', execOptions) {
return this.exec(`cherry-pick ${option} ${hash}`, execOptions)
}
}

module.exports = Git
45 changes: 45 additions & 0 deletions lib/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const GitHubApi = require('github')
const Promise = require('bluebird')

class GitHub {
constructor() {
this.github = new GitHubApi({ Promise: Promise })
}

authenticate(options = {}) {
this.github.authenticate({
type: options.type,
token: options.token,
})
}

createPullRequest(remote, params = {}) {
return new Promise((resolve, reject) => {
this.github.pullRequests.create({
owner: remote.upstream.owner,
repo: remote.upstream.name,
title: params.title,
body: params.body,
head: `${remote.origin.owner}:${params.branch}`,
base: remote.upstream.defaultBranch,
})
.then(res => resolve(res))
.catch(err => reject(err))
})
}

closePullRequest(remote, params = {}) {
return new Promise((resolve, reject) => {
this.github.pullRequests.update({
owner: remote.upstream.owner,
repo: remote.upstream.name,
number: params.number,
state: 'closed',
})
.then(res => resolve(res))
.catch(err => reject(err))
})
}
}

module.exports = GitHub
76 changes: 76 additions & 0 deletions lib/repository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const shell = require('shelljs')
const Git = require('./git')
shell.config.silent = true

class Repository {
constructor({ path = '.', remote, user }) {
this.path = path
this.remote = remote
this.user = user
}

setup() {
shell.cd(this.path)
if (shell.cd(this.remote.origin.name).code !== 0) {
Git.exec(`clone ${this.remote.origin.url} ${this.remote.origin.name}`)
shell.cd(this.remote.origin.name)
Git.exec(`remote add ${this.remote.upstream.name} ${this.remote.upstream.url}`)
Git.exec(`remote add ${this.remote.head.name} ${this.remote.head.url}`)
Git.exec(`config user.name "${this.user.name}"`)
Git.exec(`config user.email "${this.user.email}"`)
}
this.resetChanges()
this.checkputDefaultBranch()
Git.exec('branch | grep -v "*" | xargs git branch -D')
}

fetchAllRemotes() {
this.fetchUpstream()
this.fetchHead()
}

updateLocal() {
this.mergeUpstream()
}

fetchUpstream() {
Git.fetch(this.remote.upstream.name, this.remote.upstream.defaultBranch)
}

mergeUpstream() {
this.checkputDefaultBranch()
Git.merge(`${this.remote.upstream.name}/${this.remote.upstream.defaultBranch}`)
this.updateRemote()
}

fetchHead() {
Git.fetch(this.remote.head.name, this.remote.head.defaultBranch)
}

checkputDefaultBranch() {
Git.checkout(this.remote.origin.defaultBranch)
}

updateRemote() {
Git.push('origin', 'HEAD')
}

resetChanges() {
Git.exec('reset --hard')
}

existsRemoteBranch(branchName) {
return Git.exec(`branch -a | grep -c remotes/origin/${branchName}`).stdout.replace('\n', '') === '1'
}

createNewBranch(branchName) {
return Git.checkout(branchName, '-b')
}

hasConflicts(command, target, option) {
if (command === 'cherry-pick') return Git.cherryPick(target, option).code !== 0
else return Git.merge(target).code !== 0
}
}

module.exports = Repository
Loading

0 comments on commit 75004c4

Please sign in to comment.