diff --git a/README.md b/README.md
index 4419722c8..29cf6aea0 100644
--- a/README.md
+++ b/README.md
@@ -859,6 +859,10 @@ You can pass environment variables; the easiest way to do it is via a `.env` fil
```
BLOCK_REPO_RENAME_BY_HUMAN=true
```
+1. Create the configured `default_branch` instead of renaming the current one using `CREATE_DEFAULT_BRANCH` (default is `false`). By default, when `repository.default_branch` is configured and differs from the repo's current default branch, `safe-settings` renames the current default branch to the configured name. When `CREATE_DEFAULT_BRANCH=true`, if the configured `default_branch` does not exist, `safe-settings` instead creates a new branch off the current default branch and sets it as the default, leaving the existing default branch untouched (no rename). If the configured branch already exists, it is simply set as the default. For e.g.
+ ```
+ CREATE_DEFAULT_BRANCH=true
+ ```
### Runtime Settings
diff --git a/docs/github-settings/1. repository-settings.md b/docs/github-settings/1. repository-settings.md
index 4d7fc0785..c0cdbc1e5 100644
--- a/docs/github-settings/1. repository-settings.md
+++ b/docs/github-settings/1. repository-settings.md
@@ -225,8 +225,8 @@ repository:
|
- default_branch boolean
-Updates the default branch for this repository.
+default_branch string
+Updates the default branch for this repository. By default, if the configured branch differs from the current default branch, safe-settings renames the current default branch. Set the CREATE_DEFAULT_BRANCH=true environment variable to instead create the configured branch (off the current default) and promote it, without renaming the existing default branch.
|
```yaml
diff --git a/lib/env.js b/lib/env.js
index 94c0ea742..2c861486d 100644
--- a/lib/env.js
+++ b/lib/env.js
@@ -6,5 +6,6 @@ module.exports = {
CREATE_PR_COMMENT: process.env.CREATE_PR_COMMENT || 'true',
CREATE_ERROR_ISSUE: process.env.CREATE_ERROR_ISSUE || 'true',
BLOCK_REPO_RENAME_BY_HUMAN: process.env.BLOCK_REPO_RENAME_BY_HUMAN || 'false',
+ CREATE_DEFAULT_BRANCH: process.env.CREATE_DEFAULT_BRANCH === 'true',
FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true'
}
diff --git a/lib/plugins/repository.js b/lib/plugins/repository.js
index 3333225d2..9f7cee759 100644
--- a/lib/plugins/repository.js
+++ b/lib/plugins/repository.js
@@ -3,6 +3,7 @@
const ErrorStash = require('./errorStash')
const NopCommand = require('../nopcommand')
const MergeDeep = require('../mergeDeep')
+const env = require('../env')
const ignorableFields = [
'id',
'node_id',
@@ -179,7 +180,7 @@ module.exports = class Repository extends ErrorStash {
// If the old branch was renamed github will find the branch with the oldname the branch but the ref doesn't exist
// So we'd have to rename it back to the oldname
if (res.data.name !== newname) {
- return this.renameBranch(oldname, newname, resArray)
+ return this.createOrRenameBranch(oldname, newname, resArray)
} else {
const parms = {
owner: this.settings.owner,
@@ -195,13 +196,59 @@ module.exports = class Repository extends ErrorStash {
}
}).catch(e => {
if (e.status === 404) {
- return this.renameBranch(oldname, newname, resArray)
+ return this.createOrRenameBranch(oldname, newname, resArray)
} else {
this.logError(`Error ${JSON.stringify(e)}`)
}
})
}
+ // Decide how to reconcile a missing default branch based on the
+ // CREATE_DEFAULT_BRANCH feature flag. When enabled, a new branch is created
+ // off the current default branch and promoted to default, leaving the existing
+ // default branch untouched. When disabled (default), the current default branch
+ // is renamed (the historical behavior).
+ createOrRenameBranch (oldname, newname, resArray) {
+ if (env.CREATE_DEFAULT_BRANCH) {
+ return this.createDefaultBranch(oldname, newname, resArray)
+ }
+ return this.renameBranch(oldname, newname, resArray)
+ }
+
+ createDefaultBranch (oldname, newname, resArray) {
+ this.log.debug(`Branch ${newname} does not exist. Creating it from the current default branch ${oldname} and making it the default`)
+ return this.github.repos.getBranch({
+ owner: this.settings.owner,
+ repo: this.settings.repo,
+ branch: oldname
+ }).then((res) => {
+ const sha = res.data.commit.sha
+ const createRefParms = {
+ owner: this.settings.owner,
+ repo: this.settings.repo,
+ ref: `refs/heads/${newname}`,
+ sha
+ }
+ const updateParms = {
+ owner: this.settings.owner,
+ repo: this.settings.repo,
+ default_branch: newname
+ }
+ if (this.nop) {
+ resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.git.createRef.endpoint(createRefParms), `Create default branch ${newname} from ${oldname}`))
+ resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(updateParms), `Set default branch to ${newname}`))
+ return Promise.resolve(resArray)
+ }
+ this.log.info(`Creating branch ${newname} from ${oldname} (${sha}) and setting it as the default branch`)
+ return this.github.git.createRef(createRefParms).then(() => {
+ return this.github.repos.update(updateParms)
+ })
+ }).catch(e => {
+ this.logError(`Error creating default branch ${newname} from ${oldname}: ${JSON.stringify(e)}`)
+ throw e
+ })
+ }
+
renameBranch (oldname, newname, resArray) {
this.log.error(`Branch ${newname} does not exist. So renaming the current default branch ${oldname} to ${newname}`)
const parms = {
diff --git a/test/unit/lib/plugins/repository.test.js b/test/unit/lib/plugins/repository.test.js
index fc4c453b0..75587fed1 100644
--- a/test/unit/lib/plugins/repository.test.js
+++ b/test/unit/lib/plugins/repository.test.js
@@ -1,4 +1,5 @@
const Repository = require('../../../../lib/plugins/repository')
+const env = require('../../../../lib/env')
describe('Repository', () => {
const github = {
@@ -8,13 +9,19 @@ describe('Repository', () => {
topics: []
}
}),
+ getBranch: jest.fn().mockResolvedValue({ data: { name: 'main', commit: { sha: 'abc123' } } }),
update: jest.fn().mockResolvedValue(),
+ renameBranch: jest.fn().mockResolvedValue(),
replaceAllTopics: jest.fn().mockResolvedValue()
+ },
+ git: {
+ createRef: jest.fn().mockResolvedValue()
}
}
const log = jest.fn()
log.debug = jest.fn()
log.error = jest.fn()
+ log.info = jest.fn()
function configure (config) {
const nop = false
@@ -58,7 +65,7 @@ describe('Repository', () => {
})
})
- it.only('syncs topics', () => {
+ it('syncs topics', () => {
const plugin = configure({
topics: ['foo', 'bar']
})
@@ -75,4 +82,99 @@ describe('Repository', () => {
})
})
})
+
+ describe('default_branch reconciliation', () => {
+ const originalFlag = env.CREATE_DEFAULT_BRANCH
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ github.repos.get.mockResolvedValue({
+ data: {
+ name: 'test',
+ default_branch: 'master',
+ topics: []
+ }
+ })
+ })
+
+ afterEach(() => {
+ env.CREATE_DEFAULT_BRANCH = originalFlag
+ })
+
+ describe('when CREATE_DEFAULT_BRANCH is disabled (default)', () => {
+ beforeEach(() => {
+ env.CREATE_DEFAULT_BRANCH = false
+ })
+
+ it('renames the current default branch when the configured branch is missing', () => {
+ github.repos.getBranch.mockRejectedValueOnce({ status: 404 })
+ const plugin = configure({ default_branch: 'main' })
+ return plugin.sync().then(() => {
+ expect(github.repos.renameBranch).toHaveBeenCalledWith({
+ owner: 'bkeepers',
+ repo: 'test',
+ branch: 'master',
+ new_name: 'main'
+ })
+ expect(github.git.createRef).not.toHaveBeenCalled()
+ })
+ })
+ })
+
+ describe('when CREATE_DEFAULT_BRANCH is enabled', () => {
+ beforeEach(() => {
+ env.CREATE_DEFAULT_BRANCH = true
+ })
+
+ it('creates the branch off the current default and promotes it without renaming', () => {
+ // First getBranch call (for the configured branch) 404s; the second
+ // (for the current default) returns its SHA.
+ github.repos.getBranch
+ .mockRejectedValueOnce({ status: 404 })
+ .mockResolvedValueOnce({ data: { name: 'master', commit: { sha: 'deadbeef' } } })
+ const plugin = configure({ default_branch: 'main' })
+ return plugin.sync().then(() => {
+ expect(github.git.createRef).toHaveBeenCalledWith({
+ owner: 'bkeepers',
+ repo: 'test',
+ ref: 'refs/heads/main',
+ sha: 'deadbeef'
+ })
+ expect(github.repos.update).toHaveBeenCalledWith({
+ owner: 'bkeepers',
+ repo: 'test',
+ default_branch: 'main'
+ })
+ expect(github.repos.renameBranch).not.toHaveBeenCalled()
+ })
+ })
+
+ it('only sets the default branch when the configured branch already exists', () => {
+ github.repos.getBranch.mockResolvedValueOnce({ data: { name: 'main', commit: { sha: 'abc123' } } })
+ const plugin = configure({ default_branch: 'main' })
+ return plugin.sync().then(() => {
+ expect(github.git.createRef).not.toHaveBeenCalled()
+ expect(github.repos.renameBranch).not.toHaveBeenCalled()
+ expect(github.repos.update).toHaveBeenCalledWith({
+ owner: 'bkeepers',
+ repo: 'test',
+ default_branch: 'main'
+ })
+ })
+ })
+
+ it('propagates errors when branch creation fails instead of swallowing them', () => {
+ github.git.createRef.mockRejectedValueOnce({ status: 422, message: 'boom' })
+ const errors = []
+ const plugin = new Repository(false, github, { owner: 'bkeepers', repo: 'test' }, { default_branch: 'main' }, 1, log, errors)
+ const resArray = []
+ return expect(plugin.createDefaultBranch('master', 'main', resArray)).rejects.toEqual({ status: 422, message: 'boom' }).then(() => {
+ // The failure is recorded rather than silently swallowed, and the
+ // default branch is never updated when branch creation fails.
+ expect(errors).toHaveLength(1)
+ expect(github.repos.update).not.toHaveBeenCalled()
+ })
+ })
+ })
+ })
})
|