Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/github-settings/1. repository-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ repository:
</td></tr>
<tr><td>
<p><code>default_branch</code><span style="color:gray;">&emsp;<i>boolean</i>&emsp;</span></p>
<p>Updates the default branch for this repository.</p>
<p>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 <code>CREATE_DEFAULT_BRANCH=true</code> environment variable to instead create the configured branch (off the current default) and promote it, without renaming the existing default branch.</p>
Comment thread
decyjphr marked this conversation as resolved.
Outdated
</td><td style="vertical-align:top">

```yaml
Expand Down
1 change: 1 addition & 0 deletions lib/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
50 changes: 48 additions & 2 deletions lib/plugins/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -195,13 +196,58 @@ 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)}`)
})
Comment thread
Copilot marked this conversation as resolved.
}

renameBranch (oldname, newname, resArray) {
this.log.error(`Branch ${newname} does not exist. So renaming the current default branch ${oldname} to ${newname}`)
const parms = {
Expand Down
91 changes: 90 additions & 1 deletion test/unit/lib/plugins/repository.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Repository = require('../../../../lib/plugins/repository')
const env = require('../../../../lib/env')

describe('Repository', () => {
const github = {
Expand All @@ -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
Expand Down Expand Up @@ -58,7 +65,7 @@ describe('Repository', () => {
})
})

it.only('syncs topics', () => {
it('syncs topics', () => {
const plugin = configure({
topics: ['foo', 'bar']
})
Expand All @@ -75,4 +82,86 @@ 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'
})
})
})
})
})
})