Commit eed9c523 authored by Jérome Perrin's avatar Jérome Perrin

Script to periodically fetch and merge upstream changes

parents
node_modules/
work/
# Script to periodically fetch and merge upstream changes
The use case is to keep a **project** repository up to date with changes from **upstream** repository.
This basically runs in a loop
```
git fetch project
git reset --hard project/master
git fetch upstream
git merge upstream/master
git push project master
```
## Install
Needs nodejs >= 16
```
npm install
```
## Usage
```
cp config-example.yaml config.yaml
$EDITOR config.yaml
npx run zx autogitmerge.mjs
```
\ No newline at end of file
#!/usr/bin/env zx
import 'zx/globals'
const configData = await fs.readFile(argv['config'] || './config.yaml', 'utf-8')
const config = YAML.parse(configData)
const wd = config['working-directory'] || 'work';
const interval = (config.interval || (15 * 60)) * 1000;
if (!fs.existsSync(wd)) {
echo(`Initializing ${wd}`)
await $`mkdir ${wd}`
cd(wd)
await $`git init`
await $`git remote add upstream ${config.upstream.url}`
await $`git remote add project ${config.project.url}`
await $`git fetch --all`
await $`git checkout -b ${config.project.branch} project/${config.project.branch}`
if (config.committer !== undefined) {
await $`git config user.name ${config.committer.name}`
await $`git config user.email ${config.committer.email}`
}
cd('..')
}
cd(wd)
let fetchErrors = 0, reportedConflictHashes
while (true) {
try {
await $`git fetch --all`
} catch (e) {
echo('Error fetching')
fetchErrors++
if (fetchErrors > 10) {
throw e
}
await sleep(interval)
}
fetchErrors = 0
await $`git reset --hard project/${config.project.branch}`
let mergeSuccess = true;
try {
await $`git merge upstream/${config.upstream.branch}`
} catch (e) {
mergeSuccess = false;
const currentHashes = (
(await $`git rev-parse --short project/${config.project.branch}`).stdout.trim()
+ ' and '
+ (await $`git rev-parse --short upstream/${config.upstream.branch}`).stdout.trim()
)
echo(chalk.red(`😱 could not merge master ${currentHashes}`))
if (currentHashes !== reportedConflictHashes) {
/** we saw this conflict for the first time, notify */
reportedConflictHashes = currentHashes
if (config?.notification?.type === 'comment-on-gitlab-merge-request') {
const url = new URL(config.notification['merge-request-url']);
let [projectId, mergeRequestIid] = url.pathname.substring(1).split('/merge_requests/');
url.pathname = `/api/v4/projects/${encodeURIComponent(projectId)}/merge_requests/${mergeRequestIid}/notes`
console.log(url.toString())
let body = config.notification.message
if (body === undefined) {
// merge again capturing the output this time
await $`git reset --hard project/${config.project.branch}`
const gitOutput = (await $`git merge upstream/${config.upstream.branch}`.nothrow()).toString()
body = `Error while merging upstream changes
\`\`\`console
$ git merge upstream/${config.upstream.branch}
${gitOutput}
\`\`\`
`
}
const resp = await fetch(
url,
{
method: 'post',
body: JSON.stringify({ body }),
headers: {
'content-type': 'application/json',
'PRIVATE-TOKEN': config.notification['gitlab-token'],
}
})
echo(JSON.stringify(await resp.json()))
}
} else {
echo(`conflict already notified`)
}
}
if (mergeSuccess) {
await $`git push project ${config.project.branch}`
}
await sleep(interval)
}
# yaml-language-server: $schema=config-schema.json
working-directory: ./erp5-zope4py2
upstream:
url: https://lab.nexedi.com/nexedi/erp5/
branch: master
project:
url: https://lab.nexedi.com/nexedi/erp5/
branch: zope4py2
interval: 2
# optional: committer for the merge commits
committer:
name: Merge Bot
email: gitlab@nexedi.com
# optional: send notification when a conflict occur during merge
notification:
type: comment-on-gitlab-merge-request
merge-request-url: https://lab.nexedi.com/nexedi/erp5/merge_requests/1545
# a user token, comments will be posted as this user
gitlab-token: XXX
{
"$schema": "http://json-schema.org/draft-07/schema",
"additionalProperties": false,
"required": [
"upstream",
"project"
],
"definitions": {
"repository": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"description": "URL of the repository",
"format": "uri"
},
"branch": {
"type": "string",
"description": "Branch in the repository",
"default": "master"
}
}
}
},
"properties": {
"working-directory": {
"type": "string",
"description": "Directory to use for the working copy"
},
"upstream": {
"$ref": "#/definitions/repository",
"examples": [
{
"url": "https://lab.nexedi.com/nexedi/erp5/",
"branch": "master"
}
]
},
"project": {
"$ref": "#/definitions/repository",
"examples": [
{
"url": "https://lab.nexedi.com/nexedi/project-erp5/",
"branch": "next"
}
]
},
"notification": {
"oneOf": [
{
"additionalProperties": false,
"type": "object",
"required": [
"type",
"merge-request-url",
"gitlab-token"
],
"properties": {
"type": {
"type": "string",
"const": "comment-on-gitlab-merge-request",
"default": "comment-on-gitlab-merge-request"
},
"merge-request-url": {
"type": "string",
"description": "URL of a gitlab merge request to send notifications when not able to merge automatically",
"format": "uri"
},
"gitlab-token": {
"type": "string",
"description": "gitlab token"
},
"message":{
"type": "string",
"description": "Message to body to post instead of the git command output"
}
}
}
]
},
"committer": {
"type": "object",
"additionalProperties": false,
"default": "taken from global git config",
"examples": [
{
"name": "Merge Bot",
"email": "gitlab@nexedi.com"
}
],
"properties": {
"name": {
"type": "string",
"description": "Name of the author of the merge commits"
},
"email": {
"type": "string",
"description": "Email of the author of the merge commits"
}
}
},
"interval": {
"type": "number",
"description": "Interval in seconds between each fetch",
"default": 900
}
}
}
\ No newline at end of file
This diff is collapsed.
{
"dependencies": {},
"devDependencies": {
"zx": "^7.0.0"
}
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment