workflows/team-sync: init
Creates a team sync workflow that pushes the current state of teams to a JSON file, which can then be ingested by `lib.teams` to expose member lists. Co-Authored-By: Alexander Bantyev <alexander.bantyev@tweag.io>
This commit is contained in:
81
.github/workflows/team.yml
vendored
Normal file
81
.github/workflows/team.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
name: Teams
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Every Tuesday at 19:42 (randomly chosen)
|
||||||
|
- cron: '42 19 * * 1'
|
||||||
|
|
||||||
|
# Allows manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||||
|
id: team-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.OWNER_APP_ID }}
|
||||||
|
private-key: ${{ secrets.OWNER_APP_PRIVATE_KEY }}
|
||||||
|
permission-administration: read
|
||||||
|
permission-members: read
|
||||||
|
- name: Fetch source
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
sparse-checkout: |
|
||||||
|
ci/github-script
|
||||||
|
maintainers/github-teams.json
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install bottleneck
|
||||||
|
- name: Synchronise teams
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.team-token.outputs.token }}
|
||||||
|
script: |
|
||||||
|
require('./ci/github-script/get-teams.js')({
|
||||||
|
github,
|
||||||
|
context,
|
||||||
|
core,
|
||||||
|
outFile: "maintainers/github-teams.json"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Use a GitHub App to create the PR so that CI gets triggered
|
||||||
|
- uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||||
|
id: sync-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.NIXPKGS_CI_APP_ID }}
|
||||||
|
private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }}
|
||||||
|
permission-contents: write
|
||||||
|
permission-pull-requests: write
|
||||||
|
- name: Get GitHub App User Git String
|
||||||
|
id: user
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.sync-token.outputs.token }}
|
||||||
|
APP_SLUG: ${{ steps.sync-token.outputs.app-slug }}
|
||||||
|
run: |
|
||||||
|
name="${APP_SLUG}[bot]"
|
||||||
|
userId=$(gh api "/users/$name" --jq .id)
|
||||||
|
email="$userId+$name@users.noreply.github.com"
|
||||||
|
echo "git-string=$name <$email>" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||||
|
with:
|
||||||
|
token: ${{ steps.sync-token.outputs.token }}
|
||||||
|
add-paths: maintainers/github-teams.json
|
||||||
|
author: ${{ steps.user.outputs.git-string }}
|
||||||
|
committer: ${{ steps.user.outputs.git-string }}
|
||||||
|
commit-message: "maintainers/github-teams.json: Automated sync"
|
||||||
|
branch: github-team-sync
|
||||||
|
title: "maintainers/github-teams.json: Automated sync"
|
||||||
|
body: |
|
||||||
|
This is an automated PR to sync the GitHub teams with access to this repository to the `lib.teams` list.
|
||||||
|
|
||||||
|
This PR can be merged without taking any further action.
|
||||||
|
|
||||||
82
ci/github-script/get-teams.js
Executable file
82
ci/github-script/get-teams.js
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
const excludeTeams = [
|
||||||
|
/^voters.*$/,
|
||||||
|
/^nixpkgs-maintainers$/,
|
||||||
|
/^nixpkgs-committers$/,
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = async ({ github, context, core, outFile }) => {
|
||||||
|
const withRateLimit = require('./withRateLimit.js')
|
||||||
|
const { writeFileSync } = require('node:fs')
|
||||||
|
const result = {}
|
||||||
|
await withRateLimit({ github, core }, async (_stats) => {
|
||||||
|
/// Turn an Array of users into an Object, mapping user.login -> user.id
|
||||||
|
function makeUserSet(users) {
|
||||||
|
// Sort in-place and build result by mutation
|
||||||
|
users.sort((a, b) => (a.login > b.login ? 1 : -1))
|
||||||
|
|
||||||
|
return users.reduce((acc, user) => {
|
||||||
|
acc[user.login] = user.id
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a list of teams and append to the result variable
|
||||||
|
async function processTeams(teams) {
|
||||||
|
for (const team of teams) {
|
||||||
|
core.notice(`Processing team ${team.slug}`)
|
||||||
|
if (!excludeTeams.some((regex) => team.slug.match(regex))) {
|
||||||
|
const members = makeUserSet(
|
||||||
|
await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||||
|
org: context.repo.owner,
|
||||||
|
team_slug: team.slug,
|
||||||
|
role: 'member',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const maintainers = makeUserSet(
|
||||||
|
await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||||
|
org: context.repo.owner,
|
||||||
|
team_slug: team.slug,
|
||||||
|
role: 'maintainer',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
result[team.slug] = {
|
||||||
|
description: team.description,
|
||||||
|
id: team.id,
|
||||||
|
maintainers,
|
||||||
|
members,
|
||||||
|
name: team.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await processTeams(
|
||||||
|
await github.paginate(github.rest.teams.listChildInOrg, {
|
||||||
|
org: context.repo.owner,
|
||||||
|
team_slug: team.slug,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const teams = await github.paginate(github.rest.repos.listTeams, {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
})
|
||||||
|
|
||||||
|
await processTeams(teams)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort the teams by team name
|
||||||
|
const sorted = Object.keys(result)
|
||||||
|
.sort()
|
||||||
|
.reduce((acc, key) => {
|
||||||
|
acc[key] = result[key]
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const json = `${JSON.stringify(sorted, null, 2)}\n`
|
||||||
|
|
||||||
|
if (outFile) {
|
||||||
|
writeFileSync(outFile, json)
|
||||||
|
} else {
|
||||||
|
console.log(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,4 +83,16 @@ program
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('get-teams')
|
||||||
|
.description('Fetch the list of teams with GitHub and output it to a file')
|
||||||
|
.argument('<owner>', 'Owner of the GitHub repository to label (Example: NixOS)')
|
||||||
|
.argument('<repo>', 'Name of the GitHub repository to label (Example: nixpkgs)')
|
||||||
|
.argument('[outFile]', 'Path to the output file (Example: github-teams.json). If not set, prints to stdout')
|
||||||
|
.action(async (owner, repo, outFile, options) => {
|
||||||
|
const getTeams = (await import('./get-teams.js')).default
|
||||||
|
// TODO: Refactor this file so we don't need to pass a PR
|
||||||
|
await run(getTeams, owner, repo, undefined, { ...options, outFile })
|
||||||
|
})
|
||||||
|
|
||||||
await program.parse()
|
await program.parse()
|
||||||
|
|||||||
Reference in New Issue
Block a user