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 10, 2023
1 parent 4662ec3 commit f04a76d
Show file tree
Hide file tree
Showing 21 changed files with 288 additions and 239 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
28 changes: 0 additions & 28 deletions .github/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,3 @@ branches:
apps: []
users: []
teams: [ "cli-team" ]
- name: latest
protection:
required_status_checks: null
enforce_admins: true
block_creations: 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
block_creations: 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
20 changes: 18 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 All @@ -11,6 +12,7 @@ const CONFIG_KEY = 'templateOSS'
const getPkgConfig = (pkg) => pkg[CONFIG_KEY] || {}

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

Expand Down Expand Up @@ -153,6 +155,12 @@ 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)
const currentBranch = await git.currentBranch(rootPkg.path)
const isReleaseBranch = currentBranch ? minimatch(currentBranch, pkgConfig.releaseBranch) : false
const defaultBranch = await git.defaultBranch(rootPkg.path) ?? 'main'

// all derived keys
const derived = {
isRoot,
Expand All @@ -170,6 +178,14 @@ 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,
baseBranch: isReleaseBranch ? currentBranch : defaultBranch,
branches: gitBranches.branches,
branchPatterns: gitBranches.patterns,
isReleaseBranch,
// dependabot
dependabot: parseDependabot(pkgConfig, defaultConfig, gitBranches.branches),
// repo
repoDir: rootPkg.path,
repoFiles,
Expand Down Expand Up @@ -261,7 +277,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
2 changes: 1 addition & 1 deletion lib/content/ci-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
ref:
required: true
type: string
default: {{ defaultBranch }}
default: {{ baseBranch }}
workflow_call:
inputs:
ref:
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}}
16 changes: 6 additions & 10 deletions lib/content/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,24 @@ 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',
filter: (p) => p.config.dependabot,
},
'.github/settings.yml': {
file: 'settings.yml',
filter: (p) => !p.config.isReleaseBranch,
},
})

const sharedRootRm = () => ({
'.github/workflows/pull-request.yml': {
filter: (p) => p.config.allPrivate,
},
'.github/settings.yml': {
filter: (p) => p.config.isReleaseBranch,
},
})

// Changes applied to the root of the repo
Expand Down Expand Up @@ -139,8 +135,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
27 changes: 27 additions & 0 deletions lib/util/dependabot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 isReleaseBranch = minimatch(branch, config.releaseBranch)
return {
branch,
allowNames: isReleaseBranch ? [NAME] : [],
labels: isReleaseBranch ? ['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.

82 changes: 82 additions & 0 deletions lib/util/git.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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')
return remotes.match(/HEAD branch: (.*)$/m)?.[1]
} catch {
// ignore errors
}
}

const currentBranch = async (path) => {
try {
return await tryGit(path, 'rev-parse', '--abbrev-ref', 'HEAD')
} catch {
// ignore errors
}
}

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

0 comments on commit f04a76d

Please sign in to comment.