Skip to content

Commit d50eee3

Browse files
committed
feat: use git remote for branch related config
This will check the origin remote if it exists and use that to determine which branches exist. These branches are then used to populate CI branches, branch protections, and dependabot. Using this for dependabot is a new feature which allows old release branches to get dependency updates for template-oss only. This also updates the dependabot config to only update the root directory instead of each workspace directory. The previous way was an attempt to get it to work with workspaces, but wasn't used in any our repos. Dependabot should now be able to update workspaces when configured to use a single root directory. Fixes #329
1 parent 4662ec3 commit d50eee3

21 files changed

+289
-239
lines changed

.github/dependabot.yml

+1-12
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,7 @@ updates:
77
directory: /
88
schedule:
99
interval: daily
10-
allow:
11-
- dependency-type: direct
12-
versioning-strategy: increase-if-necessary
13-
commit-message:
14-
prefix: deps
15-
prefix-development: chore
16-
labels:
17-
- "Dependencies"
18-
- package-ecosystem: npm
19-
directory: workspace/test-workspace/
20-
schedule:
21-
interval: daily
10+
target-branch: "main"
2211
allow:
2312
- dependency-type: direct
2413
versioning-strategy: increase-if-necessary

.github/settings.yml

-28
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,3 @@ branches:
2525
apps: []
2626
users: []
2727
teams: [ "cli-team" ]
28-
- name: latest
29-
protection:
30-
required_status_checks: null
31-
enforce_admins: true
32-
block_creations: true
33-
required_pull_request_reviews:
34-
required_approving_review_count: 1
35-
require_code_owner_reviews: true
36-
require_last_push_approval: true
37-
dismiss_stale_reviews: true
38-
restrictions:
39-
apps: []
40-
users: []
41-
teams: [ "cli-team" ]
42-
- name: release/v*
43-
protection:
44-
required_status_checks: null
45-
enforce_admins: true
46-
block_creations: true
47-
required_pull_request_reviews:
48-
required_approving_review_count: 1
49-
require_code_owner_reviews: true
50-
require_last_push_approval: true
51-
dismiss_stale_reviews: true
52-
restrictions:
53-
apps: []
54-
users: []
55-
teams: [ "cli-team" ]

.github/workflows/ci-test-workspace.yml

-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ on:
1010
push:
1111
branches:
1212
- main
13-
- latest
14-
- release/v*
1513
paths:
1614
- workspace/test-workspace/**
1715
schedule:

.github/workflows/ci.yml

-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ on:
1010
push:
1111
branches:
1212
- main
13-
- latest
14-
- release/v*
1513
paths-ignore:
1614
- workspace/test-workspace/**
1715
schedule:

.github/workflows/codeql-analysis.yml

-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@ on:
66
push:
77
branches:
88
- main
9-
- latest
10-
- release/v*
119
pull_request:
1210
branches:
1311
- main
14-
- latest
15-
- release/v*
1612
schedule:
1713
# "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
1814
- cron: "0 10 * * 1"

.github/workflows/release.yml

-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ on:
1111
push:
1212
branches:
1313
- main
14-
- latest
15-
- release/v*
1614

1715
permissions:
1816
contents: write

lib/config.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ const { relative, dirname, join, extname, posix, win32 } = require('path')
22
const { defaults, pick, omit, uniq } = require('lodash')
33
const semver = require('semver')
44
const parseCIVersions = require('./util/parse-ci-versions.js')
5-
const getGitUrl = require('./util/get-git-url.js')
5+
const parseDependabot = require('./util/dependabot.js')
6+
const git = require('./util/git.js')
67
const gitignore = require('./util/gitignore.js')
78
const { mergeWithArrays } = require('./util/merge.js')
89
const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles, mergeFiles } = require('./util/files.js')
@@ -11,6 +12,7 @@ const CONFIG_KEY = 'templateOSS'
1112
const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {}
1213

1314
const { name: NAME, version: LATEST_VERSION } = require('../package.json')
15+
const { minimatch } = require('minimatch')
1416
const MERGE_KEYS = [...FILE_KEYS, 'defaultContent', 'content']
1517
const DEFAULT_CONTENT = require.resolve(NAME)
1618

@@ -153,6 +155,12 @@ const getFullConfig = async ({
153155
const publicPkgs = pkgs.filter(p => !p.pkgJson.private)
154156
const allPrivate = pkgs.every(p => p.pkgJson.private)
155157

158+
const branches = uniq([...pkgConfig.branches ?? [], pkgConfig.releaseBranch]).filter(Boolean)
159+
const gitBranches = await git.getBranches(rootPkg.path, branches)
160+
const currentBranch = await git.currentBranch(rootPkg.path)
161+
const isReleaseBranch = currentBranch ? minimatch(currentBranch, pkgConfig.releaseBranch) : false
162+
const defaultBranch = await git.defaultBranch(rootPkg.path) ?? pkgConfig.defaultBranch ?? 'main'
163+
156164
// all derived keys
157165
const derived = {
158166
isRoot,
@@ -170,6 +178,14 @@ const getFullConfig = async ({
170178
allPrivate,
171179
// controls whether we are in a monorepo with any public workspaces
172180
isMonoPublic: isMono && !!publicPkgs.filter(p => p.path !== rootPkg.path).length,
181+
// git
182+
defaultBranch,
183+
baseBranch: isReleaseBranch ? currentBranch : defaultBranch,
184+
branches: gitBranches.branches,
185+
branchPatterns: gitBranches.patterns,
186+
isReleaseBranch,
187+
// dependabot
188+
dependabot: parseDependabot(pkgConfig, defaultConfig, gitBranches.branches),
173189
// repo
174190
repoDir: rootPkg.path,
175191
repoFiles,
@@ -261,7 +277,7 @@ const getFullConfig = async ({
261277
}
262278
}
263279

264-
const gitUrl = await getGitUrl(rootPkg.path)
280+
const gitUrl = await git.getUrl(rootPkg.path)
265281
if (gitUrl) {
266282
derived.repository = {
267283
type: 'git',

lib/content/_on-ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pull_request:
1212
{{/if}}
1313
push:
1414
branches:
15-
{{#each branches}}
15+
{{#each branchPatterns}}
1616
- {{ . }}
1717
{{/each}}
1818
{{#if isWorkspace}}

lib/content/ci-release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
ref:
88
required: true
99
type: string
10-
default: {{ defaultBranch }}
10+
default: {{ baseBranch }}
1111
workflow_call:
1212
inputs:
1313
ref:

lib/content/codeql-analysis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ name: CodeQL
33
on:
44
push:
55
branches:
6-
{{#each branches}}
6+
{{#each branchPatterns}}
77
- {{ . }}
88
{{/each}}
99
pull_request:
1010
branches:
11-
{{#each branches}}
11+
{{#each branchPatterns}}
1212
- {{ . }}
1313
{{/each}}
1414
schedule:

lib/content/dependabot.yml

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
version: 2
22

33
updates:
4+
{{#each dependabot}}
45
- package-ecosystem: npm
5-
directory: {{ pkgDir }}
6+
directory: /
67
schedule:
78
interval: daily
9+
target-branch: "{{ branch }}"
810
allow:
911
- dependency-type: direct
10-
versioning-strategy: {{ dependabot }}
12+
{{#each allowNames }}
13+
dependency-name: "{{ . }}"
14+
{{/each}}
15+
versioning-strategy: {{ strategy }}
1116
commit-message:
1217
prefix: deps
1318
prefix-development: chore
1419
labels:
1520
- "Dependencies"
21+
{{#each labels }}
22+
- "{{ . }}"
23+
{{/each}}
24+
{{/each}}

lib/content/index.js

+6-10
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,24 @@ const sharedRootAdd = (name) => ({
3838
'.github/dependabot.yml': {
3939
file: 'dependabot.yml',
4040
filter: (p) => p.config.dependabot,
41-
clean: (p) => p.config.isRoot,
42-
// dependabot takes a single top level config file. this parser
43-
// will run for all configured packages and each one will have
44-
// its item replaced in the updates array based on the directory
45-
parser: (p) => class extends p.YmlMerge {
46-
key = 'updates'
47-
id = 'directory'
48-
},
4941
},
5042
'.github/workflows/post-dependabot.yml': {
5143
file: 'post-dependabot.yml',
5244
filter: (p) => p.config.dependabot,
5345
},
5446
'.github/settings.yml': {
5547
file: 'settings.yml',
48+
filter: (p) => !p.config.isReleaseBranch,
5649
},
5750
})
5851

5952
const sharedRootRm = () => ({
6053
'.github/workflows/pull-request.yml': {
6154
filter: (p) => p.config.allPrivate,
6255
},
56+
'.github/settings.yml': {
57+
filter: (p) => p.config.isReleaseBranch,
58+
},
6359
})
6460

6561
// Changes applied to the root of the repo
@@ -139,8 +135,8 @@ module.exports = {
139135
workspaceModule,
140136
windowsCI: true,
141137
macCI: true,
142-
branches: ['main', 'latest', 'release/v*'],
143-
defaultBranch: 'main',
138+
branches: ['main', 'latest'],
139+
releaseBranch: 'release/v*',
144140
distPaths: [
145141
'bin/',
146142
'lib/',

lib/content/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
type: string
99
push:
1010
branches:
11-
{{#each branches}}
11+
{{#each branchPatterns}}
1212
- {{ . }}
1313
{{/each}}
1414

lib/util/dependabot.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const { name: NAME } = require('../../package.json')
2+
const { minimatch } = require('minimatch')
3+
4+
const parseDependabotConfig = (v) => typeof v === 'string' ? { strategy: v } : (v ?? {})
5+
6+
module.exports = (config, defaultConfig, branches) => {
7+
const { dependabot } = config
8+
const { dependabot: defaultDependabot } = defaultConfig
9+
10+
if (!dependabot) {
11+
return false
12+
}
13+
14+
return branches
15+
.filter((b) => dependabot[b] !== false)
16+
.map(branch => {
17+
const isReleaseBranch = minimatch(branch, config.releaseBranch)
18+
return {
19+
branch,
20+
allowNames: isReleaseBranch ? [NAME] : [],
21+
labels: isReleaseBranch ? ['Backport', branch] : [],
22+
...parseDependabotConfig(defaultDependabot),
23+
...parseDependabotConfig(dependabot),
24+
...parseDependabotConfig(dependabot[branch]),
25+
}
26+
})
27+
}

lib/util/get-git-url.js

-26
This file was deleted.

lib/util/git.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
const hgi = require('hosted-git-info')
2+
const git = require('@npmcli/git')
3+
const { minimatch } = require('minimatch')
4+
5+
const cache = new Map()
6+
7+
const tryGit = async (path, ...args) => {
8+
if (!await git.is({ cwd: path })) {
9+
throw new Error('no git')
10+
}
11+
const key = [path, ...args].join(',')
12+
if (cache.has(key)) {
13+
return cache.get(key)
14+
}
15+
const res = git.spawn(args, { cwd: path }).then(r => r.stdout.trim())
16+
cache.set(key, res)
17+
return res
18+
}
19+
20+
// parse a repo from a git origin into a format
21+
// for a package.json#repository object
22+
const getUrl = async (path) => {
23+
try {
24+
const urlStr = await tryGit(path, 'remote', 'get-url', 'origin')
25+
const { domain, user, project } = hgi.fromUrl(urlStr)
26+
const url = new URL(`https://${domain}`)
27+
url.pathname = `/${user}/${project}.git`
28+
return url.toString()
29+
} catch {
30+
// errors are ignored
31+
}
32+
}
33+
34+
const getBranches = async (path, branchPatterns) => {
35+
let matchingBranches = new Set()
36+
let matchingPatterns = new Set()
37+
38+
try {
39+
const res = await tryGit(path, 'ls-remote', '--heads', 'origin').then(r => r.split('\n'))
40+
const remotes = res.map((h) => h.match(/refs\/heads\/(.*)$/)).filter(Boolean).map(h => h[1])
41+
for (const branch of remotes) {
42+
for (const pattern of branchPatterns) {
43+
if (minimatch(branch, pattern)) {
44+
matchingBranches.add(branch)
45+
matchingPatterns.add(pattern)
46+
}
47+
}
48+
}
49+
} catch {
50+
matchingBranches = new Set(branchPatterns.filter(b => !b.includes('*')))
51+
matchingPatterns = new Set(branchPatterns)
52+
}
53+
54+
return {
55+
branches: [...matchingBranches],
56+
patterns: [...matchingPatterns],
57+
}
58+
}
59+
60+
const defaultBranch = async (path) => {
61+
try {
62+
const remotes = await tryGit(path, 'remote', 'show', 'origin')
63+
const branch = remotes.match(/HEAD branch: (.*)$/m)
64+
return branch[1]
65+
} catch {
66+
return 'main'
67+
}
68+
}
69+
70+
const currentBranch = async (path) => {
71+
try {
72+
return await tryGit(path, 'rev-parse', '--abbrev-ref', 'HEAD')
73+
} catch {
74+
// ignore errors
75+
}
76+
}
77+
78+
module.exports = {
79+
getUrl,
80+
getBranches,
81+
defaultBranch,
82+
currentBranch,
83+
}

0 commit comments

Comments
 (0)