summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/ci/github-script/reviewers.js
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:51:45 +0300
commitd3261e64152397db2dca4d691a990c6bc2a6f4dd (patch)
treefac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/ci/github-script/reviewers.js
parent31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff)
downloadProject-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz
Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/ci/github-script/reviewers.js')
-rw-r--r--archived/projt-launcher/ci/github-script/reviewers.js329
1 files changed, 329 insertions, 0 deletions
diff --git a/archived/projt-launcher/ci/github-script/reviewers.js b/archived/projt-launcher/ci/github-script/reviewers.js
new file mode 100644
index 0000000000..9a2fd8df6a
--- /dev/null
+++ b/archived/projt-launcher/ci/github-script/reviewers.js
@@ -0,0 +1,329 @@
+/**
+ * ProjT Launcher - Reviewer Assignment
+ * Automatically assigns reviewers based on changed files and CODEOWNERS
+ */
+
+const fs = require('node:fs')
+const path = require('node:path')
+
+function extractMaintainersBlock(source) {
+ const token = 'maintainers ='
+ const start = source.indexOf(token)
+ if (start === -1) {
+ return ''
+ }
+
+ const braceStart = source.indexOf('{', start)
+ if (braceStart === -1) {
+ return ''
+ }
+
+ let depth = 0
+ for (let i = braceStart; i < source.length; i += 1) {
+ const char = source[i]
+ if (char === '{') {
+ depth += 1
+ } else if (char === '}') {
+ depth -= 1
+ if (depth === 0) {
+ return source.slice(braceStart, i + 1)
+ }
+ }
+ }
+ return ''
+}
+
+function parseAreas(areaBlock) {
+ const matches = areaBlock.match(/"([^"]+)"/g) || []
+ return matches.map(entry => entry.replace(/"/g, ''))
+}
+
+function loadMaintainersFromNix() {
+ const maintainersPath = path.join(__dirname, '..', 'eval', 'compare', 'maintainers.nix')
+ try {
+ const source = fs.readFileSync(maintainersPath, 'utf8')
+ const block = extractMaintainersBlock(source)
+ if (!block) {
+ return []
+ }
+
+ const entryRegex = /(\w+)\s*=\s*{([\s\S]*?)\n\s*};/g
+ const maintainers = []
+ let match
+ while ((match = entryRegex.exec(block)) !== null) {
+ const [, , body] = match
+ const githubMatch = body.match(/github\s*=\s*"([^"]+)"/)
+ if (!githubMatch) continue
+ const areasMatch = body.match(/areas\s*=\s*\[([\s\S]*?)\]/)
+ const areas = areasMatch ? parseAreas(areasMatch[1]) : []
+ maintainers.push({
+ handle: githubMatch[1],
+ areas,
+ })
+ }
+ return maintainers
+ } catch (error) {
+ console.warn(`Could not read maintainers from maintainers.nix: ${error.message}`)
+ return []
+ }
+}
+
+const FALLBACK_MAINTAINERS = [
+ {
+ handle: 'YongDo-Hyun',
+ areas: ['all'],
+ },
+ {
+ handle: 'grxtor',
+ areas: ['all'],
+ },
+]
+
+const MAINTAINERS = (() => {
+ const parsed = loadMaintainersFromNix()
+ return parsed.length > 0 ? parsed : FALLBACK_MAINTAINERS
+})()
+
+// File patterns to components mapping
+const FILE_PATTERNS = [
+ { pattern: /^launcher\/ui\//, component: 'ui' },
+ { pattern: /^launcher\/minecraft\//, component: 'minecraft' },
+ { pattern: /^launcher\/modplatform\//, component: 'modplatform' },
+ { pattern: /^launcher\//, component: 'core' },
+ { pattern: /^libraries\//, component: 'core' },
+ { pattern: /^\.github\//, component: 'ci' },
+ { pattern: /^ci\//, component: 'ci' },
+ { pattern: /CMakeLists\.txt$/, component: 'build' },
+ { pattern: /\.cmake$/, component: 'build' },
+ { pattern: /vcpkg/, component: 'build' },
+ { pattern: /^docs\//, component: 'docs' },
+ { pattern: /\.md$/, component: 'docs' },
+ { pattern: /translations\//, component: 'translations' },
+]
+
+const COMPONENTS = Array.from(new Set(FILE_PATTERNS.map(({ component }) => component)))
+
+const getMaintainersForComponent = component => {
+ const assigned = MAINTAINERS.filter(
+ maintainer =>
+ maintainer.areas.includes(component) || maintainer.areas.includes('all')
+ ).map(maintainer => maintainer.handle)
+
+ return assigned.length > 0 ? assigned : MAINTAINERS.map(maintainer => maintainer.handle)
+}
+
+// Component to reviewer mapping for ProjT Launcher
+const COMPONENT_REVIEWERS = Object.fromEntries(
+ COMPONENTS.map(component => [component, getMaintainersForComponent(component)])
+)
+
+/**
+ * Get components affected by file changes
+ * @param {Array} files - List of changed files
+ * @returns {Set} Affected components
+ */
+function getAffectedComponents(files) {
+ const components = new Set()
+
+ for (const file of files) {
+ const filename = file.filename || file
+ for (const { pattern, component } of FILE_PATTERNS) {
+ if (pattern.test(filename)) {
+ components.add(component)
+ break
+ }
+ }
+ }
+
+ return components
+}
+
+/**
+ * Get reviewers for components
+ * @param {Set} components - Affected components
+ * @returns {Set} Reviewers
+ */
+function getReviewersForComponents(components) {
+ const reviewers = new Set()
+
+ for (const component of components) {
+ const componentReviewers = COMPONENT_REVIEWERS[component] || []
+ for (const reviewer of componentReviewers) {
+ reviewers.add(reviewer.toLowerCase())
+ }
+ }
+
+ return reviewers
+}
+
+/**
+ * Handle reviewer assignment for a PR
+ */
+async function handleReviewers({
+ github,
+ context,
+ core,
+ log,
+ dry,
+ pull_request,
+ reviews,
+ maintainers,
+ owners,
+ getTeamMembers,
+ getUser,
+}) {
+ const pull_number = pull_request.number
+
+ // Get currently requested reviewers
+ const requested_reviewers = new Set(
+ pull_request.requested_reviewers.map(({ login }) => login.toLowerCase()),
+ )
+ log?.(
+ 'reviewers - requested_reviewers',
+ Array.from(requested_reviewers).join(', '),
+ )
+
+ // Get existing reviewers (already reviewed)
+ const existing_reviewers = new Set(
+ reviews.map(({ user }) => user?.login.toLowerCase()).filter(Boolean),
+ )
+ log?.(
+ 'reviewers - existing_reviewers',
+ Array.from(existing_reviewers).join(', '),
+ )
+
+ // Guard against too many reviewers from large PRs
+ if (maintainers && maintainers.length > 16) {
+ core.warning('Too many potential reviewers, skipping automatic assignment.')
+ return existing_reviewers.size === 0 && requested_reviewers.size === 0
+ }
+
+ // Build list of potential reviewers
+ const users = new Set()
+
+ // Add maintainers
+ if (maintainers) {
+ for (const id of maintainers) {
+ try {
+ const user = await getUser(id)
+ users.add(user.login.toLowerCase())
+ } catch (e) {
+ core.warning(`Could not resolve user ID ${id}`)
+ }
+ }
+ }
+
+ // Add owners (from CODEOWNERS)
+ if (owners) {
+ for (const handle of owners) {
+ if (handle && !handle.includes('/')) {
+ users.add(handle.toLowerCase())
+ }
+ }
+ }
+
+ log?.('reviewers - users', Array.from(users).join(', '))
+
+ // Handle team-based owners
+ const teams = new Set()
+ if (owners) {
+ for (const handle of owners) {
+ const parts = handle.split('/')
+ if (parts.length === 2 && parts[0] === context.repo.owner) {
+ teams.add(parts[1])
+ }
+ }
+ }
+ log?.('reviewers - teams', Array.from(teams).join(', '))
+
+ // Get team members
+ const team_members = new Set()
+ if (teams.size > 0 && getTeamMembers) {
+ for (const team of teams) {
+ try {
+ const members = await getTeamMembers(team)
+ for (const member of members) {
+ team_members.add(member.login.toLowerCase())
+ }
+ } catch (e) {
+ core.warning(`Could not fetch team ${team}`)
+ }
+ }
+ }
+ log?.('reviewers - team_members', Array.from(team_members).join(', '))
+
+ // Combine all potential reviewers
+ const all_reviewers = new Set([...users, ...team_members])
+
+ // Remove PR author - can't review own PR
+ const author = pull_request.user?.login.toLowerCase()
+ all_reviewers.delete(author)
+
+ log?.('reviewers - all_reviewers', Array.from(all_reviewers).join(', '))
+
+ // Filter to collaborators only
+ const reviewers = []
+ for (const username of all_reviewers) {
+ try {
+ await github.rest.repos.checkCollaborator({
+ ...context.repo,
+ username,
+ })
+ reviewers.push(username)
+ } catch (e) {
+ if (e.status !== 404) throw e
+ core.warning(
+ `User ${username} cannot be requested for review (not a collaborator)`,
+ )
+ }
+ }
+ log?.('reviewers - filtered_reviewers', reviewers.join(', '))
+
+ // Limit reviewers
+ if (reviewers.length > 10) {
+ core.warning(`Too many reviewers (${reviewers.length}), limiting to 10`)
+ reviewers.length = 10
+ }
+
+ // Determine who needs to be requested
+ const new_reviewers = new Set(reviewers)
+ .difference(requested_reviewers)
+ .difference(existing_reviewers)
+
+ log?.(
+ 'reviewers - new_reviewers',
+ Array.from(new_reviewers).join(', '),
+ )
+
+ if (new_reviewers.size === 0) {
+ log?.('Has reviewer changes', 'false (no new reviewers)')
+ } else if (dry) {
+ core.info(
+ `Would request reviewers for #${pull_number}: ${Array.from(new_reviewers).join(', ')} (dry run)`,
+ )
+ } else {
+ await github.rest.pulls.requestReviewers({
+ ...context.repo,
+ pull_number,
+ reviewers: Array.from(new_reviewers),
+ })
+ core.info(
+ `Requested reviewers for #${pull_number}: ${Array.from(new_reviewers).join(', ')}`,
+ )
+ }
+
+ // Return whether "needs-reviewers" label should be set
+ return (
+ new_reviewers.size === 0 &&
+ existing_reviewers.size === 0 &&
+ requested_reviewers.size === 0
+ )
+}
+
+module.exports = {
+ handleReviewers,
+ getAffectedComponents,
+ getReviewersForComponents,
+ COMPONENT_REVIEWERS,
+ FILE_PATTERNS,
+}