diff --git a/.github/members.json b/.github/members.json new file mode 100644 index 00000000000..2692f76656e --- /dev/null +++ b/.github/members.json @@ -0,0 +1,282 @@ +{ + "maintainers": [ + { + "login": "dyladan", + "name": "Daniel Dyla", + "affiliation": "Dynatrace" + }, + { + "login": "pichlermarc", + "name": "Marc Pichler", + "affiliation": "Dynatrace" + }, + { + "login": "trentm", + "name": "Trent Mick", + "affiliation": "Elastic" + }, + { + "login": "david-luna", + "name": "David Luna", + "affiliation": "Elastic" + }, + { + "login": "JamieDanielson", + "name": "Jamie Danielson", + "affiliation": "Honeycomb" + }, + { + "login": "legendecas", + "name": "Chengzhong Wu", + "affiliation": "Bloomberg" + } + ], + "approvers": [ + { + "login": "blumamir", + "name": "Amir Blum", + "affiliation": "Odigos" + }, + { + "login": "david-luna", + "name": "David Luna", + "affiliation": "Elastic" + }, + { + "login": "hectorhdzg", + "name": "Hector Hernandez", + "affiliation": "Microsoft" + }, + { + "login": "martinkuba", + "name": "Martin Kuba", + "affiliation": "Lightstep" + }, + { + "login": "maryliag", + "name": "Marylia Gutierrez", + "affiliation": "Grafana Labs" + }, + { + "login": "mwear", + "name": "Matthew Wear", + "affiliation": "LightStep" + }, + { + "login": "MSNev", + "name": "Neville Wylie", + "affiliation": "Microsoft" + }, + { + "login": "pkanal", + "name": "Purvi Kanal", + "affiliation": "Honeycomb" + }, + { + "login": "svetlanabrennan", + "name": "Svetlana Brennan", + "affiliation": "New Relic" + } + ], + "triagers": [ + { + "login": "JacksonWeber", + "name": "Jackson Weber", + "affiliation": "Microsoft" + } + ], + "contrib-triagers": [ + { + "login": "aabmass", + "name": "Aaron Abbott", + "affiliation": "Google" + }, + { + "login": "abhee11", + "name": "Abhinav Mathur", + "affiliation": "AppDynamics" + }, + { + "login": "obecny", + "name": "Bartlomiej Obecny" + }, + { + "login": "d4nyll", + "name": "Daniel Li" + }, + { + "login": "facostaembrace", + "name": "Florencia Acosta", + "affiliation": "Embrace" + }, + { + "login": "JacksonWeber", + "name": "Jackson Weber", + "affiliation": "Microsoft" + }, + { + "login": "Ugzuzg", + "name": "Jaryk", + "affiliation": "Volvo Cars" + }, + { + "login": "jj22ee", + "name": "Jonathan Lee" + }, + { + "login": "jpmunz", + "name": "Jonathan Munz", + "affiliation": "Embrace" + }, + { + "login": "kirrg001", + "name": "kirrg001", + "affiliation": "Instana" + }, + { + "login": "mhennoch", + "name": "MartenH", + "affiliation": "Splunk" + }, + { + "login": "MikeGoldsmith", + "name": "Mike Goldsmith", + "affiliation": "Honeycomb" + }, + { + "login": "mottibec", + "name": "Motti" + }, + { + "login": "punya", + "name": "Punya Biswal", + "affiliation": "Google" + }, + { + "login": "seemk", + "name": "Siim Kallas", + "affiliation": "Splunk" + }, + { + "login": "t2t2", + "name": "t2t2", + "affiliation": "Splunk" + }, + { + "login": "trivikr", + "name": "Trivikram Kamat", + "affiliation": "AWS" + }, + { + "login": "psx95", + "name": "psx95" + }, + { + "login": "dylanrussell", + "name": "dylanrussell" + }, + { + "login": "dashpole", + "name": "dashpole" + }, + { + "login": "yiyuan-he", + "name": "yiyuan-he" + }, + { + "login": "henrinormak", + "name": "henrinormak" + }, + { + "login": "weyert", + "name": "weyert" + }, + { + "login": "raphael-theriault-swi", + "name": "raphael-theriault-swi" + }, + { + "login": "naseemkullah", + "name": "naseemkullah" + }, + { + "login": "onurtemizkan", + "name": "onurtemizkan" + }, + { + "login": "sudarshan12s", + "name": "sudarshan12s" + }, + { + "login": "sharadraju", + "name": "sharadraju" + } + ], + "emeriti": [ + { + "login": "obecny", + "name": "Bartlomiej Obecny", + "role": "Maintainer" + }, + { + "login": "bg451", + "name": "Brandon Gonzalez", + "role": "Approver" + }, + { + "login": "dkhan", + "name": "Daniel Khan", + "role": "Maintainer" + }, + { + "login": "Flarna", + "name": "Gerhard Stöbich", + "role": "Approver" + }, + { + "login": "haddasbronfman", + "name": "Haddas Bronfman", + "role": "Approver" + }, + { + "login": "johnbley", + "name": "John Bley", + "role": "Approver" + }, + { + "login": "markwolff", + "name": "Mark Wolff", + "role": "Approver" + }, + { + "login": "mayurkale22", + "name": "Mayur Kale", + "role": "Maintainer" + }, + { + "login": "naseemkullah", + "name": "Naseem K. Ullah", + "role": "Approver" + }, + { + "login": "OlivierAlbertini", + "name": "Olivier Albertini", + "role": "Approver" + }, + { + "login": "rauno56", + "name": "Rauno Viskus", + "role": "Maintainer" + }, + { + "login": "rochdev", + "name": "Roch Devost", + "role": "Approver" + }, + { + "login": "vmarchaud", + "name": "Valentin Marchaud", + "role": "Maintainer" + } + ] +} diff --git a/README.md b/README.md index 4f6bf479bb8..fa229728ea0 100644 --- a/README.md +++ b/README.md @@ -246,12 +246,11 @@ We have a weekly SIG meeting! See the [community page](https://github.com/open-t - [Chengzhong Wu](https://github.com/legendecas), Bloomberg - [Daniel Dyla](https://github.com/dyladan), Dynatrace +- [David Luna](https://github.com/david-luna), Elastic - [Jamie Danielson](https://github.com/JamieDanielson), Honeycomb - [Marc Pichler](https://github.com/pichlermarc), Dynatrace - [Trent Mick](https://github.com/trentm), Elastic -For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#maintainer). - ### Approvers - [Amir Blum](https://github.com/blumamir), Odigos @@ -283,7 +282,10 @@ Typically, members of this are [component owners](https://github.com/open-teleme - [Abhinav Mathur](https://github.com/abhee11), AppDynamics - [Bartlomiej Obecny](https://github.com/obecny) - [Daniel Li](https://github.com/d4nyll) +- [dashpole](https://github.com/dashpole) +- [dylanrussell](https://github.com/dylanrussell) - [Florencia Acosta](https://github.com/facostaembrace), Embrace +- [henrinormak](https://github.com/henrinormak) - [Jackson Weber](https://github.com/JacksonWeber), Microsoft - [Jaryk](https://github.com/Ugzuzg), Volvo Cars - [Jonathan Lee](https://github.com/jj22ee) @@ -292,10 +294,18 @@ Typically, members of this are [component owners](https://github.com/open-teleme - [MartenH](https://github.com/mhennoch), Splunk - [Mike Goldsmith](https://github.com/MikeGoldsmith), Honeycomb - [Motti](https://github.com/mottibec) +- [naseemkullah](https://github.com/naseemkullah) +- [onurtemizkan](https://github.com/onurtemizkan) +- [psx95](https://github.com/psx95) - [Punya Biswal](https://github.com/punya), Google +- [raphael-theriault-swi](https://github.com/raphael-theriault-swi) +- [sharadraju](https://github.com/sharadraju) - [Siim Kallas](https://github.com/seemk), Splunk +- [sudarshan12s](https://github.com/sudarshan12s) - [t2t2](https://github.com/t2t2), Splunk - [Trivikram Kamat](https://github.com/trivikr), AWS +- [weyert](https://github.com/weyert) +- [yiyuan-he](https://github.com/yiyuan-he) For more information about the triager role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager). diff --git a/scripts/check-membership-status.mjs b/scripts/check-membership-status.mjs new file mode 100644 index 00000000000..37a7770589c --- /dev/null +++ b/scripts/check-membership-status.mjs @@ -0,0 +1,60 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const ORG = 'open-telemetry'; +const TEAM_MAP = { + maintainers: 'javascript-maintainers', + approvers: 'javascript-approvers', + triagers: 'javascript-triagers', + 'contrib-triagers': 'javascript-contrib-triagers' +}; +const MEMBERS_JSON_PATH = path.resolve(process.cwd(), '.github/members.json'); +const OUTPUT_PATH = path.resolve(process.cwd(), '.tmp/membership-report.md'); + +// Requires: GITHUB_TOKEN in env +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + +async function fetchTeamMembers(teamSlug) { + const url = `https://api.github.com/orgs/${ORG}/teams/${teamSlug}/members?per_page=100`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${GITHUB_TOKEN}`, 'User-Agent': 'membership-checker' } + }); + if (!res.ok) throw new Error(`Failed to fetch team ${teamSlug}: ${res.statusText}`); + const data = await res.json(); + return new Set(data.map(user => user.login.toLowerCase())); +} + +async function main() { + if (!GITHUB_TOKEN) { + console.error('GITHUB_TOKEN env variable required'); + process.exit(1); + } + + const members = JSON.parse(await fs.readFile(MEMBERS_JSON_PATH, 'utf8')); + const errors = [] + for (const [group, teamSlug] of Object.entries(TEAM_MAP)) { + const groupMembers = members[group] || []; + if (groupMembers.length === 0) { + continue; + } + const teamSet = await fetchTeamMembers(teamSlug); + for (const m of groupMembers) { + if (!teamSet.has(m.login.toLowerCase())) { + const membershipError = `- ${m.login} is missing from GitHub team @${ORG}/${teamSlug}`; + console.info(membershipError); + errors.push(membershipError); + } + } + } + + if (errors.length === 0) { + return; + } + + // Create report and write to file. + const report = "Membership discrepancies found:\n" + errors.join('\n') + '\n' + "@open-telemetry/javascript-maintainers, please verify and fix the membership issues.\n"; + await fs.mkdir(path.dirname(OUTPUT_PATH), { recursive: true }); + await fs.writeFile(OUTPUT_PATH, report); +} + +await main(); diff --git a/scripts/sync-contrib-triagers.mjs b/scripts/sync-contrib-triagers.mjs new file mode 100644 index 00000000000..9ff42c2a1a5 --- /dev/null +++ b/scripts/sync-contrib-triagers.mjs @@ -0,0 +1,64 @@ +import fs from 'fs/promises'; +import path from 'path'; +import yaml from 'yaml'; + +const COMPONENT_OWNERS_URL = 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js-contrib/refs/heads/main/.github/component_owners.yml'; +const MEMBERS_JSON_PATH = path.resolve(process.cwd(), '.github/members.json'); + +function getLoginsByRole(members, role) { + if (members[role] == null) { + return []; + } + return members[role].map(m => m.login.toLowerCase()); +} + +async function main() { + // Fetch and parse component_owners.yml + const res = await fetch(COMPONENT_OWNERS_URL); + if (!res.ok) throw new Error(`Failed to fetch component_owners.yml: ${res.statusText}`); + const ymlText = await res.text(); + const ymlData = yaml.parse(ymlText); + + // Collect all unique owners + const contribTriagers = new Set(); + for (const component of Object.keys(ymlData.components)) { + const componentOwners = ymlData.components[component]; + if (componentOwners && Array.isArray(componentOwners)) { + componentOwners.forEach(owner => contribTriagers.add(owner.toLowerCase())); + } + } + + // Read and parse members.json + const members = JSON.parse(await fs.readFile(MEMBERS_JSON_PATH, 'utf8')); + + // Collect existing logins - if any of these roles already have the owner, we do not need to add them. + const existingLogins = new Set([ + ...getLoginsByRole(members, 'contrib-triagers'), + ...getLoginsByRole(members, 'maintainers'), + ...getLoginsByRole(members, 'approvers') + ]); + + // Add missing owners + let added = false; + for (const login of contribTriagers) { + if (!existingLogins.has(login)) { + console.info(`Adding ${login} to contrib-triagers`); + members['contrib-triagers'].push({ + login: login, + name: login // we may not know their name, so just use login as a placeholder until it's updated manually + }); + added = true; + } + } + + // Write back if changes were made + if (added) { + await fs.writeFile(MEMBERS_JSON_PATH, JSON.stringify(members, null, 2) + '\n'); + console.log('Updated contrib-triagers with new owners.'); + } else { + console.log('No new owners to add.'); + } +} + +await main(); + diff --git a/scripts/update-members.mjs b/scripts/update-members.mjs new file mode 100644 index 00000000000..c54da759c97 --- /dev/null +++ b/scripts/update-members.mjs @@ -0,0 +1,70 @@ +import fs from 'fs/promises'; +import path from 'path'; + +const rootDir = path.resolve(process.cwd(), '.'); +const membersPath = path.join(rootDir, '.github', 'members.json'); +const readmePath = path.join(rootDir, 'README.md'); + +const explainers = { + 'Triagers': `Members of this team have triager permissions for opentelemetry-js.git and opentelemetry-js-contrib.git.`, + 'Contrib Triagers': `Members of this team have triager permissions for opentelemetry-js-contrib.git. +Typically, members of this are [component owners](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/.github/component_owners.yml) of one or more packages in the contrib repo.`, +}; + +const moreInfos = { + 'Approvers': `For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver).`, + 'Triagers': `For more information about the triager role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager).`, + 'Contrib Triagers': `For more information about the triager role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager).`, + 'Emeriti': `For more information about the emeritus role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager).` +}; + +function memberToMarkdown(member) { + let md = `- [${member.name}](https://github.com/${member.login})`; + if (member.role) md += `, ${member.role}`; + if (member.affiliation) md += `, ${member.affiliation}`; + return md; +} + +function replaceSection(readme, sectionTitle, members) { + // Match from the heading to just before the next h3 or end of file, excluding the next heading + const sectionRegex = new RegExp( + `^### ${sectionTitle}\\n[\\s\\S]*?(?=^### |\\Z)`, + 'gm' + ); + + const explainer = explainers[sectionTitle]; + const moreInfo = moreInfos[sectionTitle]; + const memberLines = members.map(memberToMarkdown).join('\n'); + + // Build the new section + let section = `### ${sectionTitle}\n\n`; + if (explainer) section += explainer + '\n\n'; + section += memberLines ? memberLines + '\n\n' : '- N/A\n\n'; + if (moreInfo) section += moreInfo + '\n\n'; + + return readme.replace(sectionRegex, section); +} + +function sortMembersByName(members) { + return [...members].sort((a, b) => a.name.localeCompare(b.name)); +} + +async function main() { + const [membersRaw, readmeRaw] = await Promise.all([ + fs.readFile(membersPath, 'utf8'), + fs.readFile(readmePath, 'utf8'), + ]); + const members = JSON.parse(membersRaw); + + let readme = readmeRaw; + + readme = replaceSection(readme, 'Maintainers', sortMembersByName(members.maintainers)); + readme = replaceSection(readme, 'Approvers', sortMembersByName(members.approvers)); + readme = replaceSection(readme, 'Triagers', sortMembersByName(members.triagers)); + readme = replaceSection(readme, 'Contrib Triagers', sortMembersByName(members['contrib-triagers'])); + readme = replaceSection(readme, 'Emeriti', sortMembersByName(members.emeriti)); + + await fs.writeFile(readmePath, readme); +} + +await main();