Skip to content

Commit

Permalink
feat: use git remote for branch related config
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lukekarrys committed Jul 9, 2023
1 parent 102e1ae commit 8dc0546
Show file tree
Hide file tree
Showing 20 changed files with 265 additions and 233 deletions.
13 changes: 1 addition & 12 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,7 @@ updates:
directory: /
schedule:
interval: daily
allow:
- dependency-type: direct
versioning-strategy: increase-if-necessary
commit-message:
prefix: deps
prefix-development: chore
labels:
- "Dependencies"
- package-ecosystem: npm
directory: workspace/test-workspace/
schedule:
interval: daily
target-branch: "main"
allow:
- dependency-type: direct
versioning-strategy: increase-if-necessary
Expand Down
26 changes: 0 additions & 26 deletions .github/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,3 @@ branches:
apps: []
users: []
teams: [ "cli-team" ]
- name: latest
protection:
required_status_checks: null
enforce_admins: true
required_pull_request_reviews:
required_approving_review_count: 1
require_code_owner_reviews: true
require_last_push_approval: true
dismiss_stale_reviews: true
restrictions:
apps: []
users: []
teams: [ "cli-team" ]
- name: release/v*
protection:
required_status_checks: null
enforce_admins: true
required_pull_request_reviews:
required_approving_review_count: 1
require_code_owner_reviews: true
require_last_push_approval: true
dismiss_stale_reviews: true
restrictions:
apps: []
users: []
teams: [ "cli-team" ]
2 changes: 0 additions & 2 deletions .github/workflows/ci-test-workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ on:
push:
branches:
- main
- latest
- release/v*
paths:
- workspace/test-workspace/**
schedule:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ on:
push:
branches:
- main
- latest
- release/v*
paths-ignore:
- workspace/test-workspace/**
schedule:
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ on:
push:
branches:
- main
- latest
- release/v*
pull_request:
branches:
- main
- latest
- release/v*
schedule:
# "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
- cron: "0 10 * * 1"
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
push:
branches:
- main
- latest
- release/v*

permissions:
contents: write
Expand Down
14 changes: 12 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const { relative, dirname, join, extname, posix, win32 } = require('path')
const { defaults, pick, omit, uniq } = require('lodash')
const semver = require('semver')
const parseCIVersions = require('./util/parse-ci-versions.js')
const getGitUrl = require('./util/get-git-url.js')
const parseDependabot = require('./util/dependabot.js')
const git = require('./util/git.js')
const gitignore = require('./util/gitignore.js')
const { mergeWithArrays } = require('./util/merge.js')
const { FILE_KEYS, parseConfig: parseFiles, getAddedFiles, mergeFiles } = require('./util/files.js')
Expand Down Expand Up @@ -153,6 +154,9 @@ const getFullConfig = async ({
const publicPkgs = pkgs.filter(p => !p.pkgJson.private)
const allPrivate = pkgs.every(p => p.pkgJson.private)

const branches = uniq([...pkgConfig.branches ?? [], pkgConfig.releaseBranch]).filter(Boolean)
const gitBranches = await git.getBranches(rootPkg.path, branches)

// all derived keys
const derived = {
isRoot,
Expand All @@ -170,6 +174,12 @@ const getFullConfig = async ({
allPrivate,
// controls whether we are in a monorepo with any public workspaces
isMonoPublic: isMono && !!publicPkgs.filter(p => p.path !== rootPkg.path).length,
// git
defaultBranch: await git.defaultBranch(rootPkg.path),
branches: gitBranches.branches,
branchPatterns: gitBranches.patterns,
// dependabot
dependabot: parseDependabot(pkgConfig, defaultConfig, gitBranches.branches),
// repo
repoDir: rootPkg.path,
repoFiles,
Expand Down Expand Up @@ -261,7 +271,7 @@ const getFullConfig = async ({
}
}

const gitUrl = await getGitUrl(rootPkg.path)
const gitUrl = await git.getUrl(rootPkg.path)
if (gitUrl) {
derived.repository = {
type: 'git',
Expand Down
2 changes: 1 addition & 1 deletion lib/content/_on-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pull_request:
{{/if}}
push:
branches:
{{#each branches}}
{{#each branchPatterns}}
- {{ . }}
{{/each}}
{{#if isWorkspace}}
Expand Down
4 changes: 2 additions & 2 deletions lib/content/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ name: CodeQL
on:
push:
branches:
{{#each branches}}
{{#each branchPatterns}}
- {{ . }}
{{/each}}
pull_request:
branches:
{{#each branches}}
{{#each branchPatterns}}
- {{ . }}
{{/each}}
schedule:
Expand Down
13 changes: 11 additions & 2 deletions lib/content/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
version: 2

updates:
{{#each dependabot}}
- package-ecosystem: npm
directory: {{ pkgDir }}
directory: /
schedule:
interval: daily
target-branch: "{{ branch }}"
allow:
- dependency-type: direct
versioning-strategy: {{ dependabot }}
{{#each allowNames }}
dependency-name: "{{ . }}"
{{/each}}
versioning-strategy: {{ strategy }}
commit-message:
prefix: deps
prefix-development: chore
labels:
- "Dependencies"
{{#each labels }}
- "{{ . }}"
{{/each}}
{{/each}}
12 changes: 2 additions & 10 deletions lib/content/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@ const sharedRootAdd = (name) => ({
'.github/dependabot.yml': {
file: 'dependabot.yml',
filter: (p) => p.config.dependabot,
clean: (p) => p.config.isRoot,
// dependabot takes a single top level config file. this parser
// will run for all configured packages and each one will have
// its item replaced in the updates array based on the directory
parser: (p) => class extends p.YmlMerge {
key = 'updates'
id = 'directory'
},
},
'.github/workflows/post-dependabot.yml': {
file: 'post-dependabot.yml',
Expand Down Expand Up @@ -139,8 +131,8 @@ module.exports = {
workspaceModule,
windowsCI: true,
macCI: true,
branches: ['main', 'latest', 'release/v*'],
defaultBranch: 'main',
branches: ['main', 'latest'],
releaseBranch: 'release/v*',
distPaths: [
'bin/',
'lib/',
Expand Down
2 changes: 1 addition & 1 deletion lib/content/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
type: string
push:
branches:
{{#each branches}}
{{#each branchPatterns}}
- {{ . }}
{{/each}}

Expand Down
28 changes: 28 additions & 0 deletions lib/util/dependabot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { name: NAME } = require('../../package.json')
const { minimatch } = require('minimatch')

const parseDependabotConfig = (v) => typeof v === 'string' ? { strategy: v } : (v ?? {})

module.exports = (config, defaultConfig, branches) => {
const { dependabot } = config
const { dependabot: defaultDependabot } = defaultConfig

if (!dependabot) {
return false
}

return branches
.filter((b) => dependabot[b] !== false)
.map(branch => {
const isRelease = minimatch(branch, config.releaseBranch)

return {
branch,
allowNames: isRelease ? [NAME] : [],
labels: isRelease ? ['Backport', branch] : [],
...parseDependabotConfig(defaultDependabot),
...parseDependabotConfig(dependabot),
...parseDependabotConfig(dependabot[branch]),
}
})
}
26 changes: 0 additions & 26 deletions lib/util/get-git-url.js

This file was deleted.

74 changes: 74 additions & 0 deletions lib/util/git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const hgi = require('hosted-git-info')
const git = require('@npmcli/git')
const { minimatch } = require('minimatch')

const cache = new Map()

const tryGit = async (path, ...args) => {
if (!await git.is({ cwd: path })) {
throw new Error('no git')
}
const key = [path, ...args].join(',')
if (cache.has(key)) {
return cache.get(key)
}
const res = git.spawn(args, { cwd: path }).then(r => r.stdout.trim())
cache.set(key, res)
return res
}

// parse a repo from a git origin into a format
// for a package.json#repository object
const getUrl = async (path) => {
try {
const urlStr = await tryGit(path, 'remote', 'get-url', 'origin')
const { domain, user, project } = hgi.fromUrl(urlStr)
const url = new URL(`https://${domain}`)
url.pathname = `/${user}/${project}.git`
return url.toString()
} catch {
// errors are ignored
}
}

const getBranches = async (path, branchPatterns) => {
let matchingBranches = new Set()
let matchingPatterns = new Set()

try {
const res = await tryGit(path, 'ls-remote', '--heads', 'origin').then(r => r.split('\n'))
const remotes = res.map((h) => h.match(/refs\/heads\/(.*)$/)).filter(Boolean).map(h => h[1])
for (const branch of remotes) {
for (const pattern of branchPatterns) {
if (minimatch(branch, pattern)) {
matchingBranches.add(branch)
matchingPatterns.add(pattern)
}
}
}
} catch {
matchingBranches = new Set(branchPatterns.filter(b => !b.includes('*')))
matchingPatterns = new Set(branchPatterns)
}

return {
branches: [...matchingBranches],
patterns: [...matchingPatterns],
}
}

const defaultBranch = async (path) => {
try {
const remotes = await tryGit(path, 'remote', 'show', 'origin')
const branch = remotes.match(/HEAD branch: (.*)$/m)
return branch[1]
} catch {
return 'main'
}
}

module.exports = {
getUrl,
getBranches,
defaultBranch,
}
Loading

0 comments on commit 8dc0546

Please sign in to comment.