diff options
Diffstat (limited to 'archived/projt-launcher/ci/github-script')
17 files changed, 4319 insertions, 0 deletions
diff --git a/archived/projt-launcher/ci/github-script/.editorconfig b/archived/projt-launcher/ci/github-script/.editorconfig new file mode 100644 index 0000000000..67d678ef17 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/.editorconfig @@ -0,0 +1,3 @@ +[run] +indent_style = space +indent_size = 2 diff --git a/archived/projt-launcher/ci/github-script/.gitignore b/archived/projt-launcher/ci/github-script/.gitignore new file mode 100644 index 0000000000..6b8a37657b --- /dev/null +++ b/archived/projt-launcher/ci/github-script/.gitignore @@ -0,0 +1,2 @@ +node_modules +step-summary.md diff --git a/archived/projt-launcher/ci/github-script/.npmrc b/archived/projt-launcher/ci/github-script/.npmrc new file mode 100644 index 0000000000..fb41d64f46 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/.npmrc @@ -0,0 +1,2 @@ +package-lock-only = true +save-exact = true diff --git a/archived/projt-launcher/ci/github-script/backport.js b/archived/projt-launcher/ci/github-script/backport.js new file mode 100644 index 0000000000..4d63a38875 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/backport.js @@ -0,0 +1,688 @@ +/** + * ProjT Launcher - Backport Handler + * Handles backport requests via PR comments. + * + * Command (single line): + * @projt-launcher-bot backport <target...> [--force] [--no-pr] + * + * Targets: + * - release-* branch name (e.g. release-1.2.3) + * - latest (highest versioned release-*) + * - all (all release-* branches) + * + * If no targets are provided, it falls back to PR labels: + * backport/<branch>, backport/latest, backport/all + */ + +const { execFile } = require('node:child_process') +const { promisify } = require('node:util') + +const execFileAsync = promisify(execFile) + +function stripNoise(body = '') { + return String(body) + .replace(/\r/g, '') + .replace(/<!--.*?-->/gms, '') + .replace(/(^`{3,})[^`].*?\1/gms, '') +} + +function tokenize(argString) { + const tokens = [] + let i = 0 + let current = '' + let quote = null + + const push = () => { + if (current.length > 0) tokens.push(current) + current = '' + } + + while (i < argString.length) { + const ch = argString[i] + + if (quote) { + if (ch === quote) { + quote = null + } else if (ch === '\\' && i + 1 < argString.length) { + i++ + current += argString[i] + } else { + current += ch + } + i++ + continue + } + + if (ch === '"' || ch === "'") { + quote = ch + i++ + continue + } + + if (/\s/.test(ch)) { + push() + i++ + while (i < argString.length && /\s/.test(argString[i])) i++ + continue + } + + current += ch + i++ + } + + push() + return tokens +} + +function parseBackportCommand(body) { + const cleaned = stripNoise(body) + const match = cleaned.match(/^@projt-launcher-bot\s+backport\b(.*)$/im) + if (!match) return null + + const tokens = tokenize(match[1] ?? '') + const targets = [] + const options = { + force: false, + noPr: false, + } + + for (let idx = 0; idx < tokens.length; idx++) { + const t = tokens[idx] + if (!t) continue + + if (t === '--force') { + options.force = true + continue + } + + if (t === '--no-pr') { + options.noPr = true + continue + } + + if (t === '--to') { + const next = tokens[idx + 1] + if (next) { + targets.push(next) + idx++ + } + continue + } + + if (t.startsWith('--to=')) { + targets.push(t.slice('--to='.length)) + continue + } + + if (t.startsWith('-')) { + continue + } + + targets.push(t) + } + + return { targets, options } +} + +function parseReleaseVersionTuple(branch) { + const m = String(branch).match(/^release-(v?\d+(?:\.\d+){1,2})(?:$|[-_].*)$/i) + if (!m) return null + const parts = m[1].replace(/^v/i, '').split('.').map((p) => Number(p)) + while (parts.length < 3) parts.push(0) + if (parts.some((n) => Number.isNaN(n))) return null + return parts +} + +function compareVersionTuples(a, b) { + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const av = a[i] ?? 0 + const bv = b[i] ?? 0 + if (av !== bv) return av - bv + } + return 0 +} + +async function addReaction({ github, node_id, reaction }) { + await github.graphql( + `mutation($node_id: ID!, $reaction: ReactionContent!) { + addReaction(input: { content: $reaction, subjectId: $node_id }) { + clientMutationId + } + }`, + { node_id, reaction }, + ) +} + +async function listReleaseBranches({ github, context }) { + const branches = await github.paginate(github.rest.repos.listBranches, { + ...context.repo, + per_page: 100, + }) + return branches.map((b) => b.name).filter((n) => /^release-/.test(n)) +} + +async function resolveTargets({ github, context, core, pull_request, requestedTargets }) { + const releaseBranches = await listReleaseBranches({ github, context }) + const releaseSet = new Set(releaseBranches) + + const normalized = (requestedTargets ?? []) + .map((t) => String(t).trim()) + .filter(Boolean) + + const wantsAll = normalized.includes('all') + const wantsLatest = normalized.includes('latest') + + const explicit = normalized.filter((t) => t !== 'all' && t !== 'latest') + + const resolved = new Set() + + if (wantsAll) { + for (const b of releaseBranches) resolved.add(b) + } + + if (wantsLatest) { + const candidates = releaseBranches + .map((b) => ({ b, v: parseReleaseVersionTuple(b) })) + .filter((x) => x.v) + .sort((x, y) => compareVersionTuples(x.v, y.v)) + + if (candidates.length > 0) { + resolved.add(candidates[candidates.length - 1].b) + } else { + core.warning('No versioned release-* branches found for target "latest"') + } + } + + for (const t of explicit) { + if (releaseSet.has(t)) { + resolved.add(t) + } else { + core.warning(`Ignoring unknown target branch: ${t}`) + } + } + + // Fallback to PR labels if comment had no targets. + if (resolved.size === 0) { + const labels = (pull_request.labels ?? []).map((l) => l.name) + const labelTargets = [] + for (const label of labels) { + if (!label.startsWith('backport/')) continue + labelTargets.push(label.slice('backport/'.length)) + } + if (labelTargets.length > 0) { + return resolveTargets({ + github, + context, + core, + pull_request, + requestedTargets: labelTargets, + }) + } + } + + return [...resolved] +} + +async function git(args, opts = {}) { + const { cwd, core, allowFailure } = opts + try { + const { stdout, stderr } = await execFileAsync('git', args, { cwd }) + if (stderr && core) core.info(stderr.trim()) + return stdout.trim() + } catch (e) { + if (allowFailure) return null + throw e + } +} + +async function remoteBranchExists({ cwd, branch }) { + try { + await execFileAsync('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch], { cwd }) + return true + } catch { + return false + } +} + +async function getCommitParentCount({ cwd, sha }) { + const raw = await git(['cat-file', '-p', sha], { cwd }) + return raw.split('\n').filter((l) => l.startsWith('parent ')).length +} + +async function createOrReuseBackportPR({ + github, + context, + core, + targetBranch, + backportBranch, + originalPR, + originalTitle, + cherryPickedSha, + requestedVia = 'bot comment', +}) { + const head = `${context.repo.owner}:${backportBranch}` + + const { data: prs } = await github.rest.pulls.list({ + ...context.repo, + state: 'all', + head, + base: targetBranch, + per_page: 10, + }) + + if (prs.length > 0) { + return { number: prs[0].number, url: prs[0].html_url, state: prs[0].state, reused: true } + } + + const { data: created } = await github.rest.pulls.create({ + ...context.repo, + title: `[Backport ${targetBranch}] ${originalTitle}`, + body: [ + `Automated backport of #${originalPR} to \`${targetBranch}\`.`, + ``, + `- Original PR: #${originalPR}`, + `- Cherry-picked: \`${cherryPickedSha}\``, + `- Requested via ${requestedVia}`, + ].join('\n'), + head: backportBranch, + base: targetBranch, + maintainer_can_modify: true, + }) + + try { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: created.number, + labels: ['automated-backport'], + }) + } catch (e) { + core.warning(`Failed to add label "automated-backport" to #${created.number}: ${e.message}`) + } + + return { number: created.number, url: created.html_url, state: created.state, reused: false } +} + +async function performBackport({ + github, + context, + core, + cwd, + pull_request, + targetBranch, + backportBranch, + mergeSha, + options, + requestedVia, +}) { + const baseBranch = pull_request.base.ref + + if (!options.force) { + const exists = await remoteBranchExists({ cwd, branch: backportBranch }) + if (exists) { + return { + targetBranch, + backportBranch, + status: 'skipped', + message: `Branch \`${backportBranch}\` already exists (use \`--force\` to rewrite)`, + } + } + } + + await git(['config', 'user.name', 'github-actions[bot]'], { cwd }) + await git(['config', 'user.email', 'github-actions[bot]@users.noreply.github.com'], { cwd }) + + await git(['fetch', 'origin', targetBranch, baseBranch], { cwd }) + await git(['checkout', '-B', backportBranch, `origin/${targetBranch}`], { cwd }) + + const parentCount = await getCommitParentCount({ cwd, sha: mergeSha }) + const cherryPickArgs = parentCount > 1 ? ['cherry-pick', '-m', '1', mergeSha] : ['cherry-pick', mergeSha] + + try { + await git(cherryPickArgs, { cwd }) + } catch (e) { + await git(['cherry-pick', '--abort'], { cwd, allowFailure: true }) + return { + targetBranch, + backportBranch, + status: 'conflict', + message: `Cherry-pick failed with conflicts for \`${targetBranch}\``, + } + } + + await git(['push', '--force-with-lease', 'origin', backportBranch], { cwd }) + + if (options.noPr) { + return { + targetBranch, + backportBranch, + status: 'pushed', + message: `Pushed \`${backportBranch}\` (PR creation disabled via --no-pr)`, + } + } + + const pr = await createOrReuseBackportPR({ + github, + context, + core, + targetBranch, + backportBranch, + originalPR: pull_request.number, + originalTitle: pull_request.title, + cherryPickedSha: mergeSha, + requestedVia, + }) + + return { + targetBranch, + backportBranch, + status: 'pr', + pr, + message: pr.reused + ? `Reused backport PR #${pr.number} (${pr.url})` + : `Created backport PR #${pr.number} (${pr.url})`, + } +} + +async function handleBackportComment({ github, context, core }) { + const payload = context.payload + const commentBody = payload.comment?.body ?? '' + const command = parseBackportCommand(commentBody) + if (!command) return false + + if (!payload.issue?.pull_request) { + core.info('Backport command ignored: not a pull request') + return false + } + + const association = payload.comment?.author_association + const allowed = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']) + if (!allowed.has(String(association))) { + core.info(`Backport command ignored: insufficient permissions (${association})`) + return false + } + + const prNumber = payload.issue.number + const { data: pull_request } = await github.rest.pulls.get({ + ...context.repo, + pull_number: prNumber, + }) + + if (!pull_request.merged) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: 'Backport request ignored: PR is not merged.', + }) + return true + } + + const nodeId = payload.comment?.node_id + if (nodeId) { + try { + await addReaction({ github, node_id: nodeId, reaction: 'EYES' }) + } catch { + // ignore + } + } + + const targets = await resolveTargets({ + github, + context, + core, + pull_request, + requestedTargets: command.targets, + }) + + if (targets.length === 0) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: [ + 'Backport failed: no valid targets resolved.', + '', + 'Use one of:', + '- `@projt-launcher-bot backport latest`', + '- `@projt-launcher-bot backport all`', + '- `@projt-launcher-bot backport release-1.2.3`', + ].join('\n'), + }) + if (nodeId) { + try { + await addReaction({ github, node_id: nodeId, reaction: 'CONFUSED' }) + } catch { + // ignore + } + } + return true + } + + const cwd = process.env.GITHUB_WORKSPACE || process.cwd() + const mergeSha = pull_request.merge_commit_sha + if (!mergeSha) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: 'Backport failed: merge commit SHA is missing for this PR.', + }) + if (nodeId) { + try { + await addReaction({ github, node_id: nodeId, reaction: 'CONFUSED' }) + } catch { + // ignore + } + } + return true + } + + const results = [] + for (const targetBranch of targets) { + const backportBranch = `backport/${targetBranch}/pr-${pull_request.number}` + const res = await performBackport({ + github, + context, + core, + cwd, + pull_request, + targetBranch, + backportBranch, + mergeSha, + options: command.options, + requestedVia: 'bot comment', + }) + results.push(res) + } + + const lines = [] + lines.push('## Backport results') + lines.push('') + lines.push(`Original PR: #${pull_request.number}`) + lines.push(`Cherry-picked: \`${mergeSha}\``) + lines.push('') + for (const r of results) { + if (r.status === 'pr') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'pushed') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'skipped') { + lines.push(`- SKIP \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'conflict') { + lines.push(`- FAIL \`${r.targetBranch}\`: ${r.message}`) + } else { + lines.push(`- WARN \`${r.targetBranch}\`: ${r.message ?? 'unknown status'}`) + } + } + + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body: lines.join('\n'), + }) + + const anyConflict = results.some((r) => r.status === 'conflict') + if (nodeId) { + try { + await addReaction({ + github, + node_id: nodeId, + reaction: anyConflict ? 'CONFUSED' : 'ROCKET', + }) + } catch { + // ignore + } + } + + return true +} + +function getBackportLabelTargets(labels = []) { + return labels + .filter((l) => typeof l === 'string' && l.startsWith('backport/')) + .map((l) => l.slice('backport/'.length)) +} + +function optionsFromLabels(labelTargets = []) { + return { + force: labelTargets.includes('force'), + noPr: labelTargets.includes('no-pr'), + skip: labelTargets.includes('skip'), + } +} + +async function upsertBackportSummaryComment({ github, context, pull_number, body }) { + const marker = '<!-- projt-bot:backport-summary -->' + const fullBody = [marker, body].join('\n') + + const comments = await github.paginate(github.rest.issues.listComments, { + ...context.repo, + issue_number: pull_number, + per_page: 100, + }) + + const existing = comments.find( + (c) => c.user?.login === 'github-actions[bot]' && typeof c.body === 'string' && c.body.includes(marker), + ) + + if (existing) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body: fullBody, + }) + } else { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pull_number, + body: fullBody, + }) + } +} + +async function handleBackportOnClose({ github, context, core }) { + const payload = context.payload + const pr = payload.pull_request + if (!pr) return false + + // Only act when a PR is merged and has backport/* labels. + if (!pr.merged) return false + + const labelNames = (pr.labels ?? []).map((l) => l.name) + const labelTargets = getBackportLabelTargets(labelNames) + if (labelTargets.length === 0) return false + + const opts = optionsFromLabels(labelTargets) + if (opts.skip) { + core.info('Backport skipped via backport/skip label') + return true + } + + const requestedTargets = labelTargets.filter((t) => !['force', 'no-pr', 'skip'].includes(t)) + + const targets = await resolveTargets({ + github, + context, + core, + pull_request: pr, + requestedTargets, + }) + + if (targets.length === 0) { + await upsertBackportSummaryComment({ + github, + context, + pull_number: pr.number, + body: [ + '## Backport results', + '', + 'No valid targets resolved from backport labels.', + '', + `Labels: ${labelNames.filter((n) => n.startsWith('backport/')).join(', ')}`, + ].join('\n'), + }) + return true + } + + const mergeSha = pr.merge_commit_sha + if (!mergeSha) { + await upsertBackportSummaryComment({ + github, + context, + pull_number: pr.number, + body: 'Backport failed: merge commit SHA is missing for this PR.', + }) + return true + } + + const cwd = process.env.GITHUB_WORKSPACE || process.cwd() + const results = [] + for (const targetBranch of targets) { + const backportBranch = `backport/${targetBranch}/pr-${pr.number}` + const res = await performBackport({ + github, + context, + core, + cwd, + pull_request: pr, + targetBranch, + backportBranch, + mergeSha, + options: { force: opts.force, noPr: opts.noPr }, + requestedVia: 'labels', + }) + results.push(res) + } + + const lines = [] + lines.push('## Backport results') + lines.push('') + lines.push(`Original PR: #${pr.number}`) + lines.push(`Cherry-picked: \`${mergeSha}\``) + lines.push('') + for (const r of results) { + if (r.status === 'pr') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'pushed') { + lines.push(`- OK \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'skipped') { + lines.push(`- SKIP \`${r.targetBranch}\`: ${r.message}`) + } else if (r.status === 'conflict') { + lines.push(`- FAIL \`${r.targetBranch}\`: ${r.message}`) + } else { + lines.push(`- WARN \`${r.targetBranch}\`: ${r.message ?? 'unknown status'}`) + } + } + + await upsertBackportSummaryComment({ + github, + context, + pull_number: pr.number, + body: lines.join('\n'), + }) + + return true +} + +module.exports = { + parseBackportCommand, + handleBackportComment, + handleBackportOnClose, +} diff --git a/archived/projt-launcher/ci/github-script/commit-types.json b/archived/projt-launcher/ci/github-script/commit-types.json new file mode 100644 index 0000000000..e6b206b1bf --- /dev/null +++ b/archived/projt-launcher/ci/github-script/commit-types.json @@ -0,0 +1,70 @@ +{ + "types": [ + "deps", + "dependencies", + "dep", + "upgrade", + "downgrade", + "bump", + "release", + "hotfix", + "security", + "vulnerability", + "localization", + "translation", + "i18n", + "l10n", + "internationalization", + "localisation", + "config", + "configuration", + "cleanup", + "clean", + "maintenance", + "infra", + "infrastructure", + "ops", + "operations", + "devops", + "qa", + "ux", + "ui", + "api", + "backend", + "frontend", + "data", + "database", + "schema", + "samples", + "examples", + "assets", + "content", + "docs-build", + "docs-ci", + "docs-config", + "docs-deps", + "docs-release", + "meta", + "init", + "prototype", + "experiment", + "hotpath", + "breaking", + "deprecate", + "compat", + "migration", + "interop", + "benchmark", + "profiles", + "telemetry", + "analytics", + "observability", + "state", + "sync", + "validation", + "lint", + "formatter", + "package", + "vendor" + ] +} diff --git a/archived/projt-launcher/ci/github-script/commits.js b/archived/projt-launcher/ci/github-script/commits.js new file mode 100644 index 0000000000..27dbd61eb6 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/commits.js @@ -0,0 +1,336 @@ +/** + * ProjT Launcher - Commit Validation for Pull Requests + * Validates commit messages, structure, and conventions + */ + +const { classify } = require('../supportedBranches.js') +const withRateLimit = require('./withRateLimit.js') +const { dismissReviews, postReview } = require('./reviews.js') + +const commitTypeConfig = (() => { + try { + return require('./commit-types.json') + } catch (error) { + console.warn(`commit validator: could not load commit-types.json (${error.message})`) + return {} + } +})() + +const parseCommitTypeList = (value) => { + if (!value) { + return [] + } + return value + .split(',') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean) +} + +const DEFAULT_COMMIT_TYPES = [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + 'deps', +] + +const EXTENDED_COMMIT_TYPES = [ + ...(commitTypeConfig.types ?? []), +] + +const ENV_COMMIT_TYPES = parseCommitTypeList( + process.env.COMMIT_TYPES ?? process.env.ADDITIONAL_COMMIT_TYPES ?? '' +) + +const COMMIT_TYPES = Array.from( + new Set([...DEFAULT_COMMIT_TYPES, ...EXTENDED_COMMIT_TYPES, ...ENV_COMMIT_TYPES]) +) + +const COMMIT_TYPE_SET = new Set(COMMIT_TYPES) + +// Component scopes for ProjT Launcher +const VALID_SCOPES = [ + 'core', + 'ui', + 'minecraft', + 'modplatform', + 'modrinth', + 'curseforge', + 'ftb', + 'technic', + 'atlauncher', + 'auth', + 'java', + 'news', + 'settings', + 'skins', + 'translations', + 'build', + 'ci', + 'nix', + 'vcpkg', + 'deps', +] + +/** + * Validate commit message format + * Expected format: type(scope): description + * @param {string} message - Commit message + * @returns {object} Validation result + */ +function normalizeCommitType(type) { + if (!type) { + return '' + } + const trimmed = type.toLowerCase() + const legacyMatch = trimmed.match(/^\d+\.(.+)$/) + return legacyMatch ? legacyMatch[1] : trimmed +} + +function validateCommitMessage(message) { + const firstLine = message.split('\n')[0] + + // Check for conventional commit format + const conventionalMatch = firstLine.match( + /^(?<type>[\w.-]+)(?:\((?<scope>[\w-]+)\))?!?:\s*(?<description>.+)$/ + ) + + if (!conventionalMatch) { + return { + valid: false, + severity: 'warning', + message: `Commit message doesn't follow conventional format: "${firstLine.substring(0, 50)}..."`, + } + } + + const { type, scope, description } = conventionalMatch.groups + const normalizedType = normalizeCommitType(type) + + // Validate type + if (!COMMIT_TYPE_SET.has(normalizedType)) { + return { + valid: false, + severity: 'warning', + message: `Unknown commit type "${type}". Valid types: ${COMMIT_TYPES.join(', ')}`, + } + } + + // Validate scope if present + if (scope && !VALID_SCOPES.includes(scope.toLowerCase())) { + return { + valid: false, + severity: 'info', + message: `Unknown scope "${scope}". Consider using: ${VALID_SCOPES.slice(0, 5).join(', ')}...`, + } + } + + // Check description length + if (description.length < 10) { + return { + valid: false, + severity: 'warning', + message: 'Commit description too short (min 10 chars)', + } + } + + if (firstLine.length > 140) { + return { + valid: false, + severity: 'info', + message: 'First line exceeds 140 characters', + } + } + + return { valid: true } +} + +/** + * Check commit for specific patterns + * @param {object} commit - Commit object + * @returns {object} Check result + */ +function checkCommitPatterns(commit) { + const message = commit.message + const issues = [] + + // Check for WIP markers + if (message.match(/\bWIP\b/i)) { + issues.push({ + severity: 'warning', + message: 'Commit contains WIP marker', + }) + } + + // Check for fixup/squash commits + if (message.match(/^(fixup|squash)!/i)) { + issues.push({ + severity: 'info', + message: 'Commit is a fixup/squash commit - remember to rebase before merge', + }) + } + + // Check for merge commits + if (message.startsWith('Merge ')) { + issues.push({ + severity: 'info', + message: 'Merge commit detected - consider rebasing instead', + }) + } + + // Check for large descriptions without body + if (message.split('\n').length === 1 && message.length > 100) { + issues.push({ + severity: 'info', + message: 'Long commit message without body - consider adding details in commit body', + }) + } + + return issues +} + +/** + * Validate all commits in a PR + */ +async function run({ github, context, core, dry }) { + await withRateLimit({ github, core }, async (stats) => { + stats.prs = 1 + + const pull_number = context.payload.pull_request.number + const base = context.payload.pull_request.base.ref + const baseClassification = classify(base) + + // Get all commits in the PR + const commits = await github.paginate(github.rest.pulls.listCommits, { + ...context.repo, + pull_number, + }) + + core.info(`Validating ${commits.length} commits for PR #${pull_number}`) + + const results = [] + + for (const { sha, commit } of commits) { + const commitResults = { + sha: sha.substring(0, 7), + fullSha: sha, + author: commit.author.name, + message: commit.message.split('\n')[0], + issues: [], + } + + // Validate commit message format + const formatValidation = validateCommitMessage(commit.message) + if (!formatValidation.valid) { + commitResults.issues.push({ + severity: formatValidation.severity, + message: formatValidation.message, + }) + } + + // Check for commit patterns + const patternIssues = checkCommitPatterns(commit) + commitResults.issues.push(...patternIssues) + + results.push(commitResults) + } + + // Log results + let hasErrors = false + let hasWarnings = false + + for (const result of results) { + core.startGroup(`Commit ${result.sha}`) + core.info(`Author: ${result.author}`) + core.info(`Message: ${result.message}`) + + if (result.issues.length === 0) { + core.info('✓ No issues found') + } else { + for (const issue of result.issues) { + switch (issue.severity) { + case 'error': + core.error(issue.message) + hasErrors = true + break + case 'warning': + core.warning(issue.message) + hasWarnings = true + break + default: + core.info(`ℹ ${issue.message}`) + } + } + } + core.endGroup() + } + + // If all commits are valid, dismiss any previous reviews + if (!hasErrors && !hasWarnings) { + await dismissReviews({ github, context, dry }) + core.info('✓ All commits passed validation') + return + } + + // Generate summary for issues + const issueCommits = results.filter(r => r.issues.length > 0) + + if (issueCommits.length > 0) { + const body = [ + '## Commit Validation Issues', + '', + 'The following commits have issues that should be addressed:', + '', + ...issueCommits.flatMap(commit => [ + `### \`${commit.sha}\`: ${commit.message}`, + '', + ...commit.issues.map(issue => `- **${issue.severity}**: ${issue.message}`), + '', + ]), + '---', + '', + '### Commit Message Guidelines', + '', + 'ProjT Launcher uses [Conventional Commits](https://www.conventionalcommits.org/):', + '', + '```', + 'type(scope): description', + '', + '[optional body]', + '', + '[optional footer]', + '```', + '', + `**Types**: ${COMMIT_TYPES.join(', ')}`, + '', + `**Scopes**: ${VALID_SCOPES.slice(0, 8).join(', ')}, ...`, + ].join('\n') + + // Post review only for errors/warnings, not info + if (hasErrors || hasWarnings) { + await postReview({ github, context, core, dry, body }) + } + + // Write step summary + const fs = require('node:fs') + if (process.env.GITHUB_STEP_SUMMARY) { + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, body) + } + } + + if (hasErrors) { + throw new Error('Commit validation failed with errors') + } + }) +} + +module.exports = run +module.exports.validateCommitMessage = validateCommitMessage +module.exports.checkCommitPatterns = checkCommitPatterns +module.exports.normalizeCommitType = normalizeCommitType diff --git a/archived/projt-launcher/ci/github-script/get-teams.js b/archived/projt-launcher/ci/github-script/get-teams.js new file mode 100644 index 0000000000..c547d5ac62 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/get-teams.js @@ -0,0 +1,134 @@ +/** + * ProjT Launcher - Team Information Fetcher + * Fetches team information from GitHub organization for CI purposes + */ + +// Teams to exclude from processing (bots, voters, etc.) +const excludeTeams = [ + /^voters.*$/, + /^bots?$/, +] + +/** + * Main function to fetch team information + */ +module.exports = async ({ github, context, core, outFile }) => { + const withRateLimit = require('./withRateLimit.js') + const { writeFileSync } = require('node:fs') + + const org = context.repo.owner + const result = {} + + await withRateLimit({ github, core }, async () => { + /** + * Convert array of users to object mapping login -> id + */ + function makeUserSet(users) { + users.sort((a, b) => (a.login > b.login ? 1 : -1)) + return users.reduce((acc, user) => { + acc[user.login] = user.id + return acc + }, {}) + } + + /** + * Process teams recursively + */ + async function processTeams(teams) { + for (const team of teams) { + // Skip excluded teams + if (excludeTeams.some((regex) => team.slug.match(regex))) { + core.info(`Skipping excluded team: ${team.slug}`) + continue + } + + core.notice(`Processing team ${team.slug}`) + + try { + // Get team members + const members = makeUserSet( + await github.paginate(github.rest.teams.listMembersInOrg, { + org, + team_slug: team.slug, + role: 'member', + }), + ) + + // Get team maintainers + const maintainers = makeUserSet( + await github.paginate(github.rest.teams.listMembersInOrg, { + org, + team_slug: team.slug, + role: 'maintainer', + }), + ) + + result[team.slug] = { + description: team.description, + id: team.id, + maintainers, + members, + name: team.name, + } + } catch (e) { + core.warning(`Failed to fetch team ${team.slug}: ${e.message}`) + } + + // Process child teams + try { + const childTeams = await github.paginate( + github.rest.teams.listChildInOrg, + { + org, + team_slug: team.slug, + }, + ) + await processTeams(childTeams) + } catch (e) { + // Child teams might not exist or be accessible + core.info(`No child teams for ${team.slug}`) + } + } + } + + // Get all teams with access to the repository + try { + const teams = await github.paginate(github.rest.repos.listTeams, { + ...context.repo, + }) + + core.info(`Found ${teams.length} teams with repository access`) + await processTeams(teams) + } catch (e) { + core.warning(`Could not fetch repository teams: ${e.message}`) + + // Fallback: create minimal team structure + result['projt-maintainers'] = { + description: 'ProjT Launcher Maintainers', + id: 0, + maintainers: {}, + members: {}, + name: 'ProjT Maintainers', + } + } + }) + + // Sort teams alphabetically + 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) + core.info(`Team information written to ${outFile}`) + } else { + console.log(json) + } + + return sorted +} diff --git a/archived/projt-launcher/ci/github-script/merge.js b/archived/projt-launcher/ci/github-script/merge.js new file mode 100644 index 0000000000..536af0f056 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/merge.js @@ -0,0 +1,308 @@ +/** + * ProjT Launcher - Merge Handler + * Handles PR merge operations with validation and queue management + */ + +const { classify } = require('../supportedBranches.js') + +// Component definitions for ProjT Launcher +const COMPONENTS = { + core: ['launcher/', 'systeminfo/', 'katabasis/', 'libnbtplusplus/', 'launcherjava/'], + ui: ['launcher/ui/', 'launcher/resources/', 'launcher/ui/'], + minecraft: ['launcher/minecraft/', 'tomlplusplus/', 'qdcss/'], + modplatform: ['launcher/modplatform/'], + build: ['CMakeLists.txt', 'cmake/', 'vcpkg.json', 'CMakePresets.json'], + docs: ['docs/', 'README.md', 'CONTRIBUTING.md'], + ci: ['.github/', 'ci/'], +} + +/** + * Get component owners for changed files + * @param {Array} files - Changed files + * @returns {Set} Component owners + */ +function getComponentOwners(files) { + const owners = new Set() + + for (const { filename } of files) { + for (const [component, paths] of Object.entries(COMPONENTS)) { + if (paths.some(path => filename.startsWith(path) || filename === path)) { + owners.add(component) + } + } + } + + return owners +} + +/** + * Run merge checklist for ProjT Launcher PRs + */ +function runChecklist({ + committers, + events, + files, + pull_request, + log, + maintainers, + user, + userIsMaintainer, +}) { + // Check what components are touched + const components = getComponentOwners(files) + + // Get eligible reviewers from maintainers + const eligible = maintainers && maintainers.length > 0 + ? new Set(maintainers) + : new Set() + + // Get current approvals + const approvals = new Set( + events + .filter( + ({ event, state, commit_id }) => + event === 'reviewed' && + state === 'approved' && + // Only approvals for the current head SHA count + commit_id === pull_request.head.sha, + ) + .map(({ user }) => user?.id) + .filter(Boolean), + ) + + const checklist = { + 'PR targets a development branch (develop, master)': + classify(pull_request.base.ref).type.includes('development'), + + 'PR has passing CI checks': + pull_request.mergeable_state !== 'blocked', + + 'PR is at least one of:': { + 'Approved by a maintainer': committers.intersection(approvals).size > 0, + 'Opened by a maintainer': committers.has(pull_request.user.id), + 'Part of a backport': + pull_request.head.ref.startsWith('backport-') || + pull_request.labels?.some(l => l.name === 'backport'), + }, + + 'PR has no merge conflicts': + pull_request.mergeable === true, + } + + if (user) { + checklist[`${user.login} is a project maintainer`] = userIsMaintainer + if (components.size > 0) { + checklist[`${user.login} owns touched components (${Array.from(components).join(', ')})`] = + eligible.has(user.id) + } + } else { + checklist['PR has eligible reviewers'] = eligible.size > 0 + } + + const result = Object.values(checklist).every((v) => + typeof v === 'boolean' ? v : Object.values(v).some(Boolean), + ) + + log('checklist', JSON.stringify(checklist)) + log('components', JSON.stringify(Array.from(components))) + log('eligible', JSON.stringify(Array.from(eligible))) + log('result', result) + + return { + checklist, + eligible, + components, + result, + } +} + +/** + * Check for merge command in comment + * Format: @projt-launcher-bot merge + */ +function hasMergeCommand(body) { + return (body ?? '') + .replace(/<!--.*?-->/gms, '') + .replace(/(^`{3,})[^`].*?\1/gms, '') + .match(/^@projt-launcher-bot\s+merge\s*$/im) +} + +/** + * Handle merge comment reaction + */ +async function handleMergeComment({ github, body, node_id, reaction }) { + if (!hasMergeCommand(body)) return + + await github.graphql( + `mutation($node_id: ID!, $reaction: ReactionContent!) { + addReaction(input: { + content: $reaction, + subjectId: $node_id + }) + { clientMutationId } + }`, + { node_id, reaction }, + ) +} + +/** + * Handle merge request for a PR + */ +async function handleMerge({ + github, + context, + core, + log, + dry, + pull_request, + events, + maintainers, + getTeamMembers, + getUser, +}) { + const pull_number = pull_request.number + + // Get list of maintainers (project committers) + const committers = new Set( + (await getTeamMembers('projt-maintainers')).map(({ id }) => id), + ) + + // Get changed files + const files = ( + await github.rest.pulls.listFiles({ + ...context.repo, + pull_number, + per_page: 100, + }) + ).data + + // Early exit for large PRs + if (files.length >= 100) { + core.warning('PR touches 100+ files, manual merge required') + return false + } + + // Only look through comments after the latest push + const lastPush = events.findLastIndex( + ({ event, sha, commit_id }) => + ['committed', 'head_ref_force_pushed'].includes(event) && + (sha ?? commit_id) === pull_request.head.sha, + ) + + const comments = events.slice(lastPush + 1).filter( + ({ event, body, user, node_id }) => + ['commented', 'reviewed'].includes(event) && + hasMergeCommand(body) && + user && + (dry || + !events.some( + ({ event, body }) => + ['commented'].includes(event) && + body.match(new RegExp(`^<!-- comment: ${node_id} -->$`, 'm')), + )), + ) + + /** + * Perform the merge + */ + async function merge() { + if (dry) { + core.info(`Would merge #${pull_number}... (dry run)`) + return 'Merge completed (dry run)' + } + + // Use merge queue if available, otherwise regular merge + try { + const resp = await github.graphql( + `mutation($node_id: ID!, $sha: GitObjectID) { + enqueuePullRequest(input: { + expectedHeadOid: $sha, + pullRequestId: $node_id + }) + { + clientMutationId, + mergeQueueEntry { mergeQueue { url } } + } + }`, + { node_id: pull_request.node_id, sha: pull_request.head.sha }, + ) + return [ + `:heavy_check_mark: [Queued](${resp.enqueuePullRequest.mergeQueueEntry.mergeQueue.url}) for merge`, + ] + } catch (e) { + log('Queue merge failed, trying direct merge', e.response?.errors?.[0]?.message) + } + + // Fallback to direct merge + try { + await github.rest.pulls.merge({ + ...context.repo, + pull_number, + merge_method: 'squash', + sha: pull_request.head.sha, + }) + return [':heavy_check_mark: Merged successfully'] + } catch (e) { + return [`:x: Merge failed: ${e.message}`] + } + } + + // Process merge commands + for (const comment of comments) { + const user = await getUser(comment.user.id) + + const { checklist, result } = runChecklist({ + committers, + events, + files, + pull_request, + log, + maintainers: maintainers || [], + user, + userIsMaintainer: committers.has(user.id), + }) + + const response = [] + + if (result) { + response.push(...(await merge())) + } else { + response.push(':x: Cannot merge - checklist not satisfied:') + response.push('') + response.push('```') + response.push(JSON.stringify(checklist, null, 2)) + response.push('```') + } + + if (!dry) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pull_number, + body: [ + `<!-- comment: ${comment.node_id} -->`, + '', + ...response, + ].join('\n'), + }) + + await handleMergeComment({ + github, + body: comment.body, + node_id: comment.node_id, + reaction: result ? 'ROCKET' : 'CONFUSED', + }) + } else { + core.info(`Response: ${response.join('\n')}`) + } + } + + return comments.length > 0 +} + +module.exports = { + runChecklist, + hasMergeCommand, + handleMergeComment, + handleMerge, + getComponentOwners, +} diff --git a/archived/projt-launcher/ci/github-script/package-lock.json b/archived/projt-launcher/ci/github-script/package-lock.json new file mode 100644 index 0000000000..62a633e0d6 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/package-lock.json @@ -0,0 +1,1721 @@ +{ + "name": "github-script", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "version": "1.0.0", + "dependencies": { + "@actions/artifact": "6.1.0", + "@actions/core": "3.0.0", + "@actions/github": "9.0.0", + "bottleneck": "2.19.5", + "commander": "14.0.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@actions/artifact": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-6.1.0.tgz", + "integrity": "sha512-oRn9YhKkboXgIq2TQZ9uj6bhkT5ZUzFtnyTQ0tLGBwImaD0GfWShE5R0tPbN25EJmS3tz5sDd2JnVokAOtNrZQ==", + "license": "MIT", + "dependencies": { + "@actions/core": "^3.0.0", + "@actions/github": "^9.0.0", + "@actions/http-client": "^4.0.0", + "@azure/storage-blob": "^12.30.0", + "@octokit/core": "^7.0.6", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-retry": "^8.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "@protobuf-ts/plugin": "^2.2.3-alpha.1", + "@protobuf-ts/runtime": "^2.9.4", + "archiver": "^7.0.1", + "jwt-decode": "^4.0.0", + "unzip-stream": "^0.3.1" + } + }, + "node_modules/@actions/artifact/node_modules/@actions/http-client": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" + } + }, + "node_modules/@actions/core/node_modules/@actions/http-client": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", + "license": "MIT", + "dependencies": { + "@actions/io": "^3.0.2" + } + }, + "node_modules/@actions/github": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.0.0.tgz", + "integrity": "sha512-yJ0RoswsAaKcvkmpCE4XxBRiy/whH2SdTBHWzs0gi4wkqTDhXMChjSdqBz/F4AeiDlP28rQqL33iHb+kjAMX6w==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^3.0.2", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@actions/http-client": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz", + "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", + "license": "MIT" + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz", + "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.0.7", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.30.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.30.0.tgz", + "integrity": "sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.2.0", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.2.0.tgz", + "integrity": "sha512-YZLxiJ3vBAAnFbG3TFuAMUlxZRexjQX5JDQxOkFGb6e2TpoxH3xyHI6idsMe/QrWtj41U/KoqBxlayzhS+LlwA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.0.tgz", + "integrity": "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.6.0.tgz", + "integrity": "sha512-mfAwI+4GqUtbw/ddfyolEHaAL86ozRIVlOg2A+SVRbjx1CjsMc1YJO+hBSkt/pqfpR+PmWBbZLstHbXP8KGtMQ==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.6.0", + "@typescript/vfs": "^1.5.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", + "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.3.tgz", + "integrity": "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz", + "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.2", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobuf-ts/plugin": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.11.1.tgz", + "integrity": "sha512-HyuprDcw0bEEJqkOWe1rnXUP0gwYLij8YhPuZyZk6cJbIgc/Q0IFgoHQxOXNIXAcXM4Sbehh6kjVnCzasElw1A==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.4.0", + "@bufbuild/protoplugin": "^2.4.0", + "@protobuf-ts/protoc": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "typescript": "^3.9" + }, + "bin": { + "protoc-gen-dump": "bin/protoc-gen-dump", + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/@protobuf-ts/protoc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.11.1.tgz", + "integrity": "sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==", + "license": "Apache-2.0", + "bin": { + "protoc": "protoc.js" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@typescript/vfs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.1.tgz", + "integrity": "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + }, + "node_modules/unzip-stream": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz", + "integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==", + "license": "MIT", + "dependencies": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/archived/projt-launcher/ci/github-script/package.json b/archived/projt-launcher/ci/github-script/package.json new file mode 100644 index 0000000000..e4263e2ddf --- /dev/null +++ b/archived/projt-launcher/ci/github-script/package.json @@ -0,0 +1,19 @@ +{ + "name": "projt-launcher-github-scripts", + "version": "1.0.0", + "description": "GitHub Actions scripts for ProjT Launcher CI", + "private": true, + "scripts": { + "test": "node test/commits.test.js" + }, + "dependencies": { + "@actions/artifact": "6.1.0", + "@actions/core": "3.0.0", + "@actions/github": "9.0.0", + "bottleneck": "2.19.5", + "commander": "14.0.3" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/archived/projt-launcher/ci/github-script/prepare.js b/archived/projt-launcher/ci/github-script/prepare.js new file mode 100644 index 0000000000..2c60314f11 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/prepare.js @@ -0,0 +1,314 @@ +/** + * ProjT Launcher - PR Preparation Script + * Validates PR structure and prepares merge information + */ + +const { classify } = require('../supportedBranches.js') +const { postReview } = require('./reviews.js') + +const SIGNOFF_MARKER = '<!-- bot:missing-signed-off-by -->' + +function stripNoise(body = '') { + return String(body) + .replace(/\r/g, '') + .replace(/<!--.*?-->/gms, '') + .replace(/(^`{3,})[^`].*?\1/gms, '') +} + +function hasSignedOffBy(body = '') { + const cleaned = stripNoise(body) + return /^signed-off-by:\s+.+<[^<>]+>\s*$/im.test(cleaned) +} + +async function dismissSignoffReviews({ github, context, pull_number }) { + const reviews = await github.paginate(github.rest.pulls.listReviews, { + ...context.repo, + pull_number, + }) + + const signoffReviews = reviews.filter( + (r) => + r.user?.login === 'github-actions[bot]' && + r.state === 'CHANGES_REQUESTED' && + typeof r.body === 'string' && + r.body.includes(SIGNOFF_MARKER), + ) + + for (const review of signoffReviews) { + await github.rest.pulls.dismissReview({ + ...context.repo, + pull_number, + review_id: review.id, + message: 'Signed-off-by found, thank you!', + }) + } +} + +/** + * Main PR preparation function + * Validates that the PR targets the correct branch and can be merged + */ +module.exports = async ({ github, context, core, dry }) => { + const payload = context.payload || {} + const pull_number = + payload?.pull_request?.number ?? + (Array.isArray(payload?.merge_group?.pull_requests) && + payload.merge_group.pull_requests[0]?.number) + + if (typeof pull_number !== 'number') { + core.info('No pull request found on this event; skipping prepare step.') + return { ok: true, skipped: true, reason: 'no-pull-request' } + } + + // Wait for GitHub to compute merge status + for (const retryInterval of [5, 10, 20, 40]) { + core.info('Checking whether the pull request can be merged...') + const prInfo = ( + await github.rest.pulls.get({ + ...context.repo, + pull_number, + }) + ).data + + if (prInfo.state !== 'open') { + throw new Error('PR is not open anymore.') + } + + if (prInfo.mergeable == null) { + core.info( + `GitHub is still computing merge status, waiting ${retryInterval} seconds...`, + ) + await new Promise((resolve) => setTimeout(resolve, retryInterval * 1000)) + continue + } + + const { base, head, user } = prInfo + + const authorLogin = user?.login ?? '' + const isBotAuthor = + (user?.type ?? '').toLowerCase() === 'bot' || /\[bot\]$/i.test(authorLogin) + + // Enforce PR template sign-off (Signed-off-by: Name <email>) + if (isBotAuthor) { + core.info(`Skipping Signed-off-by requirement for bot author: ${authorLogin}`) + if (!dry) { + await dismissSignoffReviews({ github, context, pull_number }) + } + } else if (!hasSignedOffBy(prInfo.body)) { + const body = [ + SIGNOFF_MARKER, + '', + '## Missing Signed-off-by', + '', + 'This repository requires a DCO-style sign-off line in the PR description.', + '', + 'Add a line like this to the PR description (under “Signed-off-by”):', + '', + '```', + 'Signed-off-by: Your Name <you@example.com>', + '```', + '', + 'After updating the PR description, this check will re-run automatically.', + ].join('\n') + + await postReview({ github, context, core, dry, body }) + throw new Error('Missing Signed-off-by in PR description') + } else if (!dry) { + await dismissSignoffReviews({ github, context, pull_number }) + } + + // Classify base branch + const baseClassification = classify(base.ref) + core.setOutput('base', baseClassification) + core.info(`Base branch classification: ${JSON.stringify(baseClassification)}`) + + // Classify head branch + const headClassification = + base.repo.full_name === head.repo.full_name + ? classify(head.ref) + : { type: ['wip'] } // PRs from forks are WIP + core.setOutput('head', headClassification) + core.info(`Head branch classification: ${JSON.stringify(headClassification)}`) + + // Validate base branch targeting + if (!baseClassification.type.includes('development') && + !baseClassification.type.includes('release')) { + const body = [ + '## Invalid Target Branch', + '', + `This PR targets \`${base.ref}\`, which is not a valid target branch.`, + '', + '### Valid target branches for ProjT Launcher:', + '', + '| Branch | Purpose |', + '|--------|---------|', + '| `develop` | Main development branch |', + '| `master` / `main` | Stable branch |', + '| `release-X.Y.Z` | Release branches |', + '', + 'Please [change the base branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request) to the appropriate target.', + ].join('\n') + + await postReview({ github, context, core, dry, body }) + throw new Error('PR targets invalid branch.') + } + + // Check for release branch targeting from wrong branch + if (baseClassification.isRelease) { + // For release branches, typically only hotfixes and backports should target them + const isBackport = head.ref.startsWith('backport-') + const isHotfix = head.ref.startsWith('hotfix-') || head.ref.startsWith('hotfix/') + + if (!isBackport && !isHotfix && headClassification.type.includes('wip')) { + const body = [ + '## Release Branch Warning', + '', + `This PR targets the release branch \`${base.ref}\`.`, + '', + 'Release branches should only receive:', + '- **Backports** from the development branch', + '- **Hotfixes** for critical bugs', + '', + 'If this is a regular feature/fix, please target `develop` instead.', + '', + 'If this is intentionally a hotfix, consider naming your branch `hotfix/description`.', + ].join('\n') + + await postReview({ github, context, core, dry, body }) + // This is a warning, not an error + core.warning('PR targets release branch from non-hotfix/backport branch') + } + } + + // Validate feature branches target develop + if (headClassification.isFeature && + !['develop'].includes(base.ref)) { + const body = [ + '## Feature Branch Target', + '', + `Feature branches should typically target \`develop\`, not \`${base.ref}\`.`, + '', + 'Please verify this is the correct target branch.', + ].join('\n') + + core.warning(body) + // Don't block, just warn + } + + // Process merge state + let mergedSha, targetSha + + if (prInfo.mergeable) { + core.info('✓ PR can be merged.') + + mergedSha = prInfo.merge_commit_sha + targetSha = ( + await github.rest.repos.getCommit({ + ...context.repo, + ref: prInfo.merge_commit_sha, + }) + ).data.parents[0].sha + } else { + core.warning('⚠ PR has merge conflicts.') + + mergedSha = head.sha + targetSha = ( + await github.rest.repos.compareCommitsWithBasehead({ + ...context.repo, + basehead: `${base.sha}...${head.sha}`, + }) + ).data.merge_base_commit.sha + } + + // Set outputs for downstream jobs + core.setOutput('mergedSha', mergedSha) + core.setOutput('targetSha', targetSha) + core.setOutput('mergeable', prInfo.mergeable) + core.setOutput('headSha', head.sha) + core.setOutput('baseSha', base.sha) + + // Get changed files for analysis + const files = await github.paginate(github.rest.pulls.listFiles, { + ...context.repo, + pull_number, + per_page: 100, + }) + + // Categorize changes + const categories = { + source: files.filter(f => + f.filename.startsWith('launcher/') + ).length, + ui: files.filter(f => + f.filename.includes('/ui/') + ).length, + build: files.filter(f => + f.filename.includes('CMake') || + f.filename.includes('vcpkg') || + f.filename.endsWith('.cmake') + ).length, + ci: files.filter(f => + f.filename.startsWith('.github/') || + f.filename.startsWith('ci/') + ).length, + docs: files.filter(f => + f.filename.startsWith('docs/') || + f.filename.endsWith('.md') + ).length, + translations: files.filter(f => + f.filename.includes('translations/') + ).length, + } + + core.info(`Changes summary:`) + core.info(` Source files: ${categories.source}`) + core.info(` UI files: ${categories.ui}`) + core.info(` Build files: ${categories.build}`) + core.info(` CI files: ${categories.ci}`) + core.info(` Documentation: ${categories.docs}`) + core.info(` Translations: ${categories.translations}`) + + core.setOutput('categories', JSON.stringify(categories)) + core.setOutput('totalFiles', files.length) + + // Write step summary + if (process.env.GITHUB_STEP_SUMMARY) { + const fs = require('node:fs') + const summary = [ + '## PR Preparation Summary', + '', + `| Property | Value |`, + `|----------|-------|`, + `| PR Number | #${pull_number} |`, + `| Base Branch | \`${base.ref}\` |`, + `| Head Branch | \`${head.ref}\` |`, + `| Mergeable | ${prInfo.mergeable ? '✅ Yes' : '❌ No'} |`, + `| Total Files | ${files.length} |`, + '', + '### Change Categories', + '', + `| Category | Files |`, + `|----------|-------|`, + ...Object.entries(categories).map(([cat, count]) => + `| ${cat.charAt(0).toUpperCase() + cat.slice(1)} | ${count} |` + ), + ].join('\n') + + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary) + } + + return { + mergeable: prInfo.mergeable, + mergedSha, + targetSha, + headSha: head.sha, + baseSha: base.sha, + base: baseClassification, + head: headClassification, + files: files.length, + categories, + } + } + + throw new Error('Timeout waiting for merge status computation') +} 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, +} diff --git a/archived/projt-launcher/ci/github-script/reviews.js b/archived/projt-launcher/ci/github-script/reviews.js new file mode 100644 index 0000000000..749ebc7085 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/reviews.js @@ -0,0 +1,93 @@ +/** + * ProjT Launcher - Review Management + * Handles GitHub PR review operations + */ + +/** + * Dismiss all bot-created reviews on a PR + */ +async function dismissReviews({ github, context, dry }) { + const pull_number = context.payload.pull_request.number + + if (dry) { + return + } + + await Promise.all( + ( + await github.paginate(github.rest.pulls.listReviews, { + ...context.repo, + pull_number, + }) + ) + .filter((review) => review.user?.login === 'github-actions[bot]') + .map(async (review) => { + if (review.state === 'CHANGES_REQUESTED') { + await github.rest.pulls.dismissReview({ + ...context.repo, + pull_number, + review_id: review.id, + message: 'All good now, thank you!', + }) + } + await github.graphql( + `mutation($node_id:ID!) { + minimizeComment(input: { + classifier: RESOLVED, + subjectId: $node_id + }) + { clientMutationId } + }`, + { node_id: review.node_id }, + ) + }), + ) +} + +async function postReview({ github, context, core, dry, body }) { + const pull_number = context.payload.pull_request.number + + const pendingReview = ( + await github.paginate(github.rest.pulls.listReviews, { + ...context.repo, + pull_number, + }) + ).find( + (review) => + review.user?.login === 'github-actions[bot]' && + // If a review is still pending, we can just update this instead + // of posting a new one. + (review.state === 'CHANGES_REQUESTED' || + // No need to post a new review, if an older one with the exact + // same content had already been dismissed. + review.body === body), + ) + + if (dry) { + if (pendingReview) + core.info(`pending review found: ${pendingReview.html_url}`) + else core.info('no pending review found') + core.info(body) + } else { + if (pendingReview) { + await github.rest.pulls.updateReview({ + ...context.repo, + pull_number, + review_id: pendingReview.id, + body, + }) + } else { + await github.rest.pulls.createReview({ + ...context.repo, + pull_number, + event: 'REQUEST_CHANGES', + body, + }) + } + } +} + +module.exports = { + dismissReviews, + postReview, +} diff --git a/archived/projt-launcher/ci/github-script/run b/archived/projt-launcher/ci/github-script/run new file mode 100644 index 0000000000..4f296f5633 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/run @@ -0,0 +1,132 @@ +#!/usr/bin/env -S node --import ./run +/** + * ProjT Launcher - CI Script Runner + * CLI tool for running CI automation scripts locally + */ +import { execSync } from 'node:child_process' +import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { program } from 'commander' +import * as core from '@actions/core' +import { getOctokit } from '@actions/github' + +/** + * Run a CI action locally + */ +async function run(action, owner, repo, pull_number, options = {}) { + // Get GitHub token from gh CLI + const token = execSync('gh auth token', { encoding: 'utf-8' }).trim() + const github = getOctokit(token) + + // Build payload + const payload = !pull_number ? {} : { + pull_request: (await github.rest.pulls.get({ + owner, + repo, + pull_number, + })).data + } + + process.env['INPUT_GITHUB-TOKEN'] = token + + // Set up step summary file + closeSync(openSync('step-summary.md', 'w')) + process.env.GITHUB_STEP_SUMMARY = 'step-summary.md' + + await action({ + github, + context: { + payload, + repo: { + owner, + repo, + }, + }, + core, + dry: true, + ...options, + }) +} + +program + .name('projt-ci') + .description('ProjT Launcher CI automation script runner') + .version('1.0.0') + +program + .command('prepare') + .description('Prepare and validate a pull request') + .argument('<owner>', 'Repository owner (e.g., Project-Tick)') + .argument('<repo>', 'Repository name (e.g., ProjT-Launcher)') + .argument('<pr>', 'Pull Request number') + .option('--no-dry', 'Make actual modifications') + .action(async (owner, repo, pr, options) => { + const prepare = (await import('./prepare.js')).default + await run(prepare, owner, repo, pr, options) + }) + +program + .command('commits') + .description('Validate commit messages and structure') + .argument('<owner>', 'Repository owner') + .argument('<repo>', 'Repository name') + .argument('<pr>', 'Pull Request number') + .option('--no-dry', 'Make actual modifications') + .action(async (owner, repo, pr, options) => { + const commits = (await import('./commits.js')).default + await run(commits, owner, repo, pr, options) + }) + +program + .command('get-teams') + .description('Fetch team information from GitHub organization') + .argument('<owner>', 'Organization/owner name') + .argument('<repo>', 'Repository name') + .argument('[outFile]', 'Output file path (prints to stdout if omitted)') + .action(async (owner, repo, outFile, options) => { + const getTeams = (await import('./get-teams.js')).default + await run(getTeams, owner, repo, undefined, { ...options, outFile }) + }) + +program + .command('reviewers') + .description('Assign reviewers to a pull request') + .argument('<owner>', 'Repository owner') + .argument('<repo>', 'Repository name') + .argument('<pr>', 'Pull Request number') + .option('--no-dry', 'Make actual modifications') + .action(async (owner, repo, pr, options) => { + const { handleReviewers } = await import('./reviewers.js') + const token = execSync('gh auth token', { encoding: 'utf-8' }).trim() + const github = getOctokit(token) + + const pull_request = (await github.rest.pulls.get({ + owner, + repo, + pull_number: pr, + })).data + + const reviews = await github.paginate(github.rest.pulls.listReviews, { + owner, + repo, + pull_number: pr, + }) + + await handleReviewers({ + github, + context: { repo: { owner, repo } }, + core, + log: console.log, + dry: options.dry ?? true, + pull_request, + reviews, + maintainers: [], + owners: [], + getTeamMembers: async () => [], + getUser: async (id) => ({ login: `user-${id}`, id }), + }) + }) + +// Parse CLI arguments +await program.parse() diff --git a/archived/projt-launcher/ci/github-script/shell.nix b/archived/projt-launcher/ci/github-script/shell.nix new file mode 100644 index 0000000000..788fee1815 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/shell.nix @@ -0,0 +1,40 @@ +# ProjT Launcher - GitHub Script Development Shell +# Provides Node.js environment for CI automation scripts +{ + system ? builtins.currentSystem, + pkgs ? import <nixpkgs> { inherit system; }, +}: + +pkgs.mkShell { + name = "projt-launcher-github-script"; + + packages = with pkgs; [ + # Node.js for running scripts + nodejs_20 + + # GitHub CLI for authentication + gh + + # Optional: development tools + nodePackages.npm + ]; + + shellHook = '' + echo "ProjT Launcher GitHub Script Development Environment" + echo "" + echo "Available commands:" + echo " npm install - Install dependencies" + echo " ./run --help - Show available CLI commands" + echo " gh auth login - Authenticate with GitHub" + echo "" + + # Install npm dependencies if package-lock.json exists + if [ -f package-lock.json ] && [ ! -d node_modules ]; then + echo "Installing npm dependencies..." + npm ci + fi + ''; + + # Environment variables + PROJT_CI_ENV = "development"; +} diff --git a/archived/projt-launcher/ci/github-script/test/commits.test.js b/archived/projt-launcher/ci/github-script/test/commits.test.js new file mode 100644 index 0000000000..ed1be49682 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/test/commits.test.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +'use strict' + +process.env.COMMIT_TYPES = 'customtype' + +const assert = require('node:assert/strict') +const commits = require('../commits.js') + +const { validateCommitMessage, normalizeCommitType } = commits + +const validMessages = [ + 'feat(ui): add redesigned settings panel', + 'refactor: drop deprecated launcher flag support', + 'chore(ci): refresh workflows configuration', + '11.feat: support legacy numbered commit type format', + '23.deps(deps): bump dependency pins', + 'release: publish stable build artifacts', + 'customtype: allow env commit type overrides', +] + +for (const message of validMessages) { + const result = validateCommitMessage(message) + assert.equal( + result.valid, + true, + `Expected commit "${message}" to be valid, got: ${result.message}` + ) +} + +const invalidType = validateCommitMessage('unknown(scope): add feature that is real enough') +assert.equal(invalidType.valid, false, 'Expected invalid type to be rejected') +assert.match(invalidType.message, /Unknown commit type/i) + +const shortDescription = validateCommitMessage('feat: short') +assert.equal(shortDescription.valid, false, 'Expected short description to fail validation') +assert.match(shortDescription.message, /too short/i) + +assert.equal(normalizeCommitType('11.feat'), 'feat') +assert.equal(normalizeCommitType('23.deps'), 'deps') +assert.equal(normalizeCommitType('chore'), 'chore') + +console.log('commits.js tests passed') diff --git a/archived/projt-launcher/ci/github-script/withRateLimit.js b/archived/projt-launcher/ci/github-script/withRateLimit.js new file mode 100644 index 0000000000..e7fcfbb513 --- /dev/null +++ b/archived/projt-launcher/ci/github-script/withRateLimit.js @@ -0,0 +1,86 @@ +/** + * ProjT Launcher - Rate Limit Handler + * Manages GitHub API rate limiting for CI scripts + */ + +module.exports = async ({ github, core, maxConcurrent = 1 }, callback) => { + let Bottleneck + try { + Bottleneck = require('bottleneck') + } catch (err) { + core?.warning?.('bottleneck not installed; running without explicit rate limiting') + Bottleneck = class { + constructor() {} + wrap(fn) { + return (...args) => fn(...args) + } + chain() { + return this + } + schedule(fn, ...args) { + return fn(...args) + } + updateSettings() {} + } + } + + const stats = { + issues: 0, + prs: 0, + requests: 0, + artifacts: 0, + } + + // Rate-Limiting and Throttling, see for details: + // https://github.com/octokit/octokit.js/issues/1069#throttling + // https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api + const allLimits = new Bottleneck({ + // Avoid concurrent requests + maxConcurrent, + // Will be updated with first `updateReservoir()` call below. + reservoir: 0, + }) + // Pause between mutative requests + const writeLimits = new Bottleneck({ minTime: 1000 }).chain(allLimits) + github.hook.wrap('request', async (request, options) => { + // Requests to a different host do not count against the rate limit. + if (options.url.startsWith('https://github.com')) return request(options) + // Requests to the /rate_limit endpoint do not count against the rate limit. + if (options.url === '/rate_limit') return request(options) + // Search requests are in a different resource group, which allows 30 requests / minute. + // We do less than a handful each run, so not implementing throttling for now. + if (options.url.startsWith('/search/')) return request(options) + stats.requests++ + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) + return writeLimits.schedule(request.bind(null, options)) + else return allLimits.schedule(request.bind(null, options)) + }) + + async function updateReservoir() { + let response + try { + response = await github.rest.rateLimit.get() + } catch (err) { + core.error(`Failed updating reservoir:\n${err}`) + // Keep retrying on failed rate limit requests instead of exiting the script early. + return + } + // Always keep 1000 spare requests for other jobs to do their regular duty. + // They normally use below 100, so 1000 is *plenty* of room to work with. + const reservoir = Math.max(0, response.data.resources.core.remaining - 1000) + core.info(`Updating reservoir to: ${reservoir}`) + allLimits.updateSettings({ reservoir }) + } + await updateReservoir() + // Update remaining requests every minute to account for other jobs running in parallel. + const reservoirUpdater = setInterval(updateReservoir, 60 * 1000) + + try { + await callback(stats) + } finally { + clearInterval(reservoirUpdater) + core.notice( + `Processed ${stats.prs} PRs, ${stats.issues} Issues, made ${stats.requests + stats.artifacts} API requests and downloaded ${stats.artifacts} artifacts.`, + ) + } +} |
