From ea663c2ff5f67ec9b0ab9da10ef3397fbf39e0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Wed, 27 Aug 2025 21:21:50 -0400 Subject: [PATCH 01/22] adding hub-sync feature code --- .env.example | 24 + docs/hubSyncHandler/README.md | 34 + index.js | 115 +- lib/env.js | 6 + lib/hubSyncHandler.js | 258 + lib/installationCache.js | 93 + lib/routes.js | 483 ++ package-lock.json | 792 ++- package.json | 1 + ui/.eslintrc.json | 3 + ui/.gitignore | 40 + ui/README.md | 40 + ui/favicon.ico | Bin 0 -> 3872 bytes ui/next.config.js | 10 + ui/package-lock.json | 5059 +++++++++++++++++ ui/package.json | 23 + ui/public/favicon.svg | 3 + ui/public/shield.png | Bin 0 -> 24289 bytes ui/shield.png | Bin 0 -> 3872 bytes ui/src/app/[slug]/route.js | 18 + ui/src/app/components/EnvVariables.jsx | 161 + ui/src/app/components/OrganizationsTable.jsx | 252 + .../components/Safe-settings-hubContent.jsx | 412 ++ ui/src/app/components/ThemeContext.jsx | 71 + ui/src/app/components/ThemeToggle.jsx | 17 + ui/src/app/components/TitleBar.css | 174 + ui/src/app/components/TitleBar.jsx | 84 + ui/src/app/dashboard/env/page.jsx | 23 + ui/src/app/dashboard/organizations/page.jsx | 24 + ui/src/app/dashboard/page.jsx | 13 + .../app/dashboard/safe-settings-hub/page.jsx | 25 + ui/src/app/dashboard/settings/page.jsx | 13 + ui/src/app/globals.css | 260 + ui/src/app/hooks/useClientSafe.js | 45 + ui/src/app/hooks/useHydrated.js | 18 + ui/src/app/layout.jsx | 41 + ui/src/app/not-found.jsx | 15 + ui/src/app/route.js | 7 + 38 files changed, 8290 insertions(+), 367 deletions(-) create mode 100644 docs/hubSyncHandler/README.md create mode 100644 lib/hubSyncHandler.js create mode 100644 lib/installationCache.js create mode 100644 lib/routes.js create mode 100644 ui/.eslintrc.json create mode 100644 ui/.gitignore create mode 100644 ui/README.md create mode 100644 ui/favicon.ico create mode 100644 ui/next.config.js create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/public/favicon.svg create mode 100644 ui/public/shield.png create mode 100644 ui/shield.png create mode 100644 ui/src/app/[slug]/route.js create mode 100644 ui/src/app/components/EnvVariables.jsx create mode 100644 ui/src/app/components/OrganizationsTable.jsx create mode 100644 ui/src/app/components/Safe-settings-hubContent.jsx create mode 100644 ui/src/app/components/ThemeContext.jsx create mode 100644 ui/src/app/components/ThemeToggle.jsx create mode 100644 ui/src/app/components/TitleBar.css create mode 100644 ui/src/app/components/TitleBar.jsx create mode 100644 ui/src/app/dashboard/env/page.jsx create mode 100644 ui/src/app/dashboard/organizations/page.jsx create mode 100644 ui/src/app/dashboard/page.jsx create mode 100644 ui/src/app/dashboard/safe-settings-hub/page.jsx create mode 100644 ui/src/app/dashboard/settings/page.jsx create mode 100644 ui/src/app/globals.css create mode 100644 ui/src/app/hooks/useClientSafe.js create mode 100644 ui/src/app/hooks/useHydrated.js create mode 100644 ui/src/app/layout.jsx create mode 100644 ui/src/app/not-found.jsx create mode 100644 ui/src/app/route.js diff --git a/.env.example b/.env.example index d98f7d4b9..7f8a45ee3 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,27 @@ # Uncomment this to get GitHub comments for the Pull Request Workflow. # ENABLE_PR_COMMENT=true + +# ADMIN_REPO=safe-settings-config +CONFIG_PATH=.github +SETTINGS_FILE_PATH=settings.yml + +# Configuration support for Hub-Sync safe-settings feature +# SAFE_SETTINGS_HUB_REPO=safe-settings-config-master +# SAFE_SETTINGS_HUB_ORG=foo-training +# A subfolder under 'CONFIG_PATH' where the 'organizations//' structure is found +# SAFE_SETTINGS_HUB_PATH=safe-settings +# SAFE_SETTINGS_HUB_DIRECT_PUSH=true + + + +# ┌────────────── second (optional) +# │ ┌──────────── minute +# │ │ ┌────────── hour +# │ │ │ ┌──────── day of month +# │ │ │ │ ┌────── month +# │ │ │ │ │ ┌──── day of week +# │ │ │ │ │ │ +# │ │ │ │ │ │ +# * * * * * * +# CRON=* * * * * # Run every minute \ No newline at end of file diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md new file mode 100644 index 000000000..7ffd23ab3 --- /dev/null +++ b/docs/hubSyncHandler/README.md @@ -0,0 +1,34 @@ +# Safe Settings Organization Sync & Dashboard + + This feature provides a centralized approach to managing the Safe-Settings Admin Repo, allowing Safe-Settings configurations to be sync'd across multiple ORGs. + +## Overview + +This feature adds a hub‑and‑spoke synchronization capability to Safe Settings. + +One central **master admin repository** (the hub) serves as the authoritative source of configuration which is automatically propagated to each organization’s **admin repository** (the spokes). + +**Note:** When something changes in the central repo, only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync with little manual work. + +## Sync Lifecycle (High Level) + +```mermaid +graph TD +A0(PR Closed) --> A1(HUB Admin Repo) +A1(ORG Admin Repo) --> B(ORG Admin Repo) +A1(HUB Admin Repo) --> C(ORG Admin Repo) +A1(HUB Admin Repo) --> D(ORG Admin Repo) +``` + +## Environment Variables & Inputs + +Environment variables specific to the 'Sync-Feature' + +| Name | Purpose | Default | +|------|---------|---------| +| `SAFE_SETTINGS_HUB_REPO` | Repo for master safe-settings contents | admin-master | +| `SAFE_SETTINGS_HUB_ORG` | Organization that hold the Repo | admin-master-org | +| `SAFE_SETTINGS_HUB_PATH` | source folder | .github/safe-settings | +| `SAFE_SETTINGS_HUB_DIRECT_PUSH` | Use a PR or direct commit | false | + + diff --git a/index.js b/index.js index f6af26b5a..e4c809f66 100644 --- a/index.js +++ b/index.js @@ -6,91 +6,22 @@ const Glob = require('./lib/glob') const ConfigManager = require('./lib/configManager') const NopCommand = require('./lib/nopcommand') const env = require('./lib/env') +const { setupRoutes } = require('./lib/routes') +const { initCache } = require('./lib/installationCache') +const { hubSyncHandler } = require('./lib/hubSyncHandler') let deploymentConfig module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => { let appSlug = 'safe-settings' - async function syncAllSettings (nop, context, repo = context.repo(), ref) { - try { - deploymentConfig = await loadYamlFileSystem() - robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) - const configManager = new ConfigManager(context, ref) - const runtimeConfig = await configManager.loadGlobalSettingsYaml() - const config = Object.assign({}, deploymentConfig, runtimeConfig) - robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - if (ref) { - return Settings.syncAll(nop, context, repo, config, ref) - } else { - return Settings.syncAll(nop, context, repo, config) - } - } catch (e) { - if (nop) { - let filename = env.SETTINGS_FILE_PATH - if (!deploymentConfig) { - filename = env.DEPLOYMENT_CONFIG_FILE_PATH - deploymentConfig = {} - } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') - robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) - } else { - throw e - } - } - } - async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { - try { - deploymentConfig = await loadYamlFileSystem() - robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) - const configManager = new ConfigManager(context, ref) - const runtimeConfig = await configManager.loadGlobalSettingsYaml() - const config = Object.assign({}, deploymentConfig, runtimeConfig) - robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref) - } catch (e) { - if (nop) { - let filename = env.SETTINGS_FILE_PATH - if (!deploymentConfig) { - filename = env.DEPLOYMENT_CONFIG_FILE_PATH - deploymentConfig = {} - } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') - robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) - } else { - throw e - } - } - } + // Initialize all routes (static UI + API) via centralized module + setupRoutes(robot, getRouter) - async function syncSettings (nop, context, repo = context.repo(), ref) { - try { - deploymentConfig = await loadYamlFileSystem() - robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) - const configManager = new ConfigManager(context, ref) - const runtimeConfig = await configManager.loadGlobalSettingsYaml() - const config = Object.assign({}, deploymentConfig, runtimeConfig) - robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.sync(nop, context, repo, config, ref) - } catch (e) { - if (nop) { - let filename = env.SETTINGS_FILE_PATH - if (!deploymentConfig) { - filename = env.DEPLOYMENT_CONFIG_FILE_PATH - deploymentConfig = {} - } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') - robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) - } else { - throw e - } - } - } + // Initialize installation cache (env-controlled prefetch) + initCache(robot) - async function renameSync (nop, context, repo = context.repo(), rename, ref) { + async function renameSync(nop, context, repo = context.repo(), rename, ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -115,13 +46,14 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } } + /** * Loads the deployment config file from file system * Do this once when the app starts and then return the cached value * * @return The parsed YAML file */ - async function loadYamlFileSystem () { + async function loadYamlFileSystem() { if (deploymentConfig === undefined) { const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE_PATH if (fs.existsSync(deploymentConfigPath)) { @@ -133,7 +65,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return deploymentConfig } - function getAllChangedSubOrgConfigs (payload) { + function getAllChangedSubOrgConfigs(payload) { const pattern = Settings.SUB_ORG_PATTERN const getMatchingFiles = (commits, type) => @@ -150,7 +82,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getAllChangedRepoConfigs (payload, owner) { + function getAllChangedRepoConfigs(payload, owner) { const pattern = Settings.REPO_PATTERN const getMatchingFiles = (commits, type) => @@ -167,7 +99,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedRepoConfigName (files, owner) { + function getChangedRepoConfigName(files, owner) { const pattern = Settings.REPO_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -178,7 +110,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedSubOrgConfigName (files) { + function getChangedSubOrgConfigName(files) { const pattern = Settings.SUB_ORG_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -188,7 +120,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => path: modifiedFile })) } - async function createCheckRun (context, pull_request, head_sha, head_branch) { + async function createCheckRun(context, pull_request, head_sha, head_branch) { const { payload } = context // robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`) const res = await context.octokit.checks.create({ @@ -200,7 +132,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => robot.log.debug(JSON.stringify(res, null)) } - async function info () { + async function info() { const github = await robot.auth() const installations = await github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: 100 }) @@ -215,7 +147,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncInstallation (nop = false) { + async function syncInstallation(nop = false) { robot.log.trace('Fetching installations') const github = await robot.auth() @@ -521,6 +453,19 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return createCheckRun(context, pull_request, payload.pull_request.head.sha, payload.pull_request.head.ref) }) + /** + * @description Handle pull_request.closed events to support hub synchronization + * @param {Object} context - The context object provided by Probot + */ + robot.on('pull_request.closed', async context => { + try { + await hubSyncHandler(robot, context) + } catch (err) { + robot.log.error(`pull_request.closed handler failed: ${err && err.message ? err.message : err}`) + } + return null + }) + robot.on(['check_suite.rerequested'], async context => { robot.log.debug('Check suite was rerequested!') return createCheckRun(context) diff --git a/lib/env.js b/lib/env.js index 94c0ea742..8bea61a73 100644 --- a/lib/env.js +++ b/lib/env.js @@ -1,5 +1,11 @@ module.exports = { ADMIN_REPO: process.env.ADMIN_REPO || 'admin', + SAFE_SETTINGS_HUB_REPO: process.env.SAFE_SETTINGS_HUB_REPO || 'admin-master', + SAFE_SETTINGS_HUB_ORG: process.env.SAFE_SETTINGS_HUB_ORG || 'admin-master-org', + SAFE_SETTINGS_HUB_DIRECT_PUSH: process.env.SAFE_SETTINGS_HUB_DIRECT_PUSH || 'false', + SAFE_SETTINGS_HUB_PATH: process.env.SAFE_SETTINGS_HUB_PATH || '.github/safe-settings', + APP_ID: process.env.APP_ID || null, + PRIVATE_KEY_PATH: process.env.PRIVATE_KEY_PATH || 'private-key.pem', CONFIG_PATH: process.env.CONFIG_PATH || '.github', SETTINGS_FILE_PATH: process.env.SETTINGS_FILE_PATH || 'settings.yml', DEPLOYMENT_CONFIG_FILE_PATH: process.env.DEPLOYMENT_CONFIG_FILE || 'deployment-settings.yml', diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js new file mode 100644 index 000000000..c34de6d66 --- /dev/null +++ b/lib/hubSyncHandler.js @@ -0,0 +1,258 @@ +const env = require('./env') +const { getInstallations } = require('./installationCache') + +/** + * Sync changed safe-settings organization files from the master admin PR + * into the target organization's admin repository. + * @param {import('probot').Probot} robot + * @param {import('probot').Context} context + * @param {string} orgName Destination organization login (also folder name under organizations/) + * @param {string} destRepo Destination repo name inside orgName (e.g. admin repo) + * @param {string} destinationFolder Base folder in destination repo where content lives (e.g. .github or .github/safe-settings) + */ +async function syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder) { + try { + robot.log.info(`Syncing safe settings for organization: ${orgName}`); + + robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`); + const pr = context.payload.pull_request; + if (!pr) { + robot.log.warn('No pull_request payload found; aborting sync'); + return; + } + const { owner: srcOwner, repo: srcRepo } = context.repo(); + const pull_number = pr.number; + + // Source base path where org folders live inside master admin repo + + // 'safe-settings' is the standard sub-folder path + const configRoot = env.CONFIG_PATH || '.github/'; + const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, ''); + robot.log.info(`DEBUG: sourceBase='${sourceBase}'`); + + // Debug info: log env and computed paths + robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`); + + // List changed files in PR + const files = await context.octokit.paginate( + context.octokit.rest.pulls.listFiles, + { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } + ); + + robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`); + if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); + + // Dump file objects for debugging filename issues + if (files.length) { + try { + robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`); + robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`); + } catch (e) { + robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`); + } + files.forEach((f, i) => { + try { + robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`); + robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`); + } catch (e) { + robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`); + } + }); + } + + const orgPrefix = `${sourceBase}/${orgName}/`; + robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); + robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`); + const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)); + robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`); + if (!relevant.length) { + robot.log.info(`No files for org ${orgName} in PR #${pull_number}`); + // Detailed per-file checks to help debug matching + files.forEach(f => { + const exact = f.filename === `${sourceBase}/${orgName}`; + const pref = f.filename.startsWith(orgPrefix); + robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`); + }); + // Also show alternate check using CONFIG_PATH + '/organizations' + const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations`; + const altPrefix = `${altBase}/${orgName}/`; + files.forEach(f => { + const exactAlt = f.filename === `${altBase}/${orgName}`; + const prefAlt = f.filename.startsWith(altPrefix); + robot.log.info(`ALT CHECK: file='${f.filename}' exactAlt=${exactAlt} prefAlt=${prefAlt}`); + }); + return; + } + + // Destination info + const destOwner = orgName; + // ensure destBase uses the configured CONFIG_PATH (fallback to '.github') and normalize trailing slash + const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, ''); + const destBaseBranch = 'main'; + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1'); + + // Find installation for destination org to auth + const installs = await getInstallations(robot) + const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === destOwner.toLowerCase()); + if (!install) { + robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`); + return; + } + const githubDest = await robot.auth(install.id); + + robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`); + + // Create branch if not direct push + const timestamp = Date.now(); + const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}`; + if (!directPush) { + try { + const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }); + const baseSha = baseRef.data.object.sha; + await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }); + robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`); + } catch (err) { + if (err.status === 422) { + robot.log.warn(`Branch ${branchName} already exists, continuing`); + } else { + throw err; + } + } + } + + for (const f of relevant) { + let relative; + if (f.filename === `${sourceBase}/${orgName}`) { + // top directory marker encountered (unlikely in changed files list) - skip + continue; + } else { + relative = f.filename.slice(orgPrefix.length); + } + // place only the changed file under the configured CONFIG_PATH (e.g. '.github/') + const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/'); + try { + const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }); + const data = srcContentResp.data; + if (Array.isArray(data)) { + // Skip directories; individual files will appear separately in changed files list + continue; + } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8'); + const encoded = Buffer.from(fileContent, 'utf8').toString('base64'); + + // Check existing file for sha + let existingSha = undefined; + try { + const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }); + if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha; + } catch (getErr) { + if (getErr.status !== 404) throw getErr; // ignore missing + } + + await githubDest.rest.repos.createOrUpdateFileContents({ + owner: destOwner, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, + content: encoded, + branch: branchName, + sha: existingSha, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }); + robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`); + } catch (fileErr) { + robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`); + throw fileErr; + } + } + + if (!directPush) { + try { + const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`; + const prBody = `Automated sync of safe-settings for ${orgName} from ${srcOwner}/${srcRepo} PR #${pull_number}.`; + const created = await githubDest.rest.pulls.create({ owner: destOwner, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }); + robot.log.info(`Created PR ${created.data.html_url} in ${destOwner}/${destRepo}`); + } catch (prErr) { + robot.log.error(`Failed to create PR in ${destOwner}/${destRepo}: ${prErr.message}`); + throw prErr; + } + } else { + robot.log.info(`Changes pushed directly to ${destOwner}/${destRepo}@${destBaseBranch}`); + } + } catch (err) { + robot.log.error(`syncSafeSettingConfig error for org ${orgName}: ${err.message}`); + } +} + +/** + * Handle closed pull requests to sync safe-settings changes to target organizations. + * Focus on the organization and repository specified in the pull request and if they belong to the Safe-Settings Hub. + * @param {import('probot').Probot} robot + * @param {import('probot').Context} context + */ +async function hubSyncHandler(robot, context) { + const { payload } = context; + const { repository, pull_request } = payload || {}; + robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`); + try { + // Ensure the event is from the configured Safe-Settings Hub repo/org + const isMasterRepo = repository && repository.name === env.SAFE_SETTINGS_HUB_REPO; + const isMasterOrg = repository && repository.owner && repository.owner.login === env.SAFE_SETTINGS_HUB_ORG; + + if (!(isMasterRepo && isMasterOrg)) { + robot.log.info(`Pull request.closed is not from master admin repo/org (${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO}), ignoring`); + return; + } + + robot.log.info(`Pull request closed on Safe-Settings Hub: (${repository.full_name})`); + + // Get the PR details + const pr = pull_request; + const { owner, repo } = context.repo(); + const pull_number = pr.number; + const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`; + + // Paginate through all files changed in the PR + const files = await context.octokit.paginate( + context.octokit.rest.pulls.listFiles, + { owner, repo, pull_number, per_page: 100 } + ); + + robot.log.info(`Files changed in PR #${pull_number}: ${files.map(f => f.filename).join(', ')}`); + + // Normalize baseSettingsPath (remove trailing slash if any) + const normalizedBase = baseSettingsPath.replace(/\/$/, ''); + robot.log.debug(`Normalized base path: ${normalizedBase}`); + + // Escape string for use in RegExp + const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Build a RegExp that captures the first path segment after the base path + const basePattern = new RegExp(`^${escapeRegex(normalizedBase)}/([^/]+)(?:/|$)`); + robot.log.debug(`Base pattern for org matching: ${basePattern}`); + + // Collect unique org names + const orgNamesSet = new Set(); + files.forEach(f => { + const m = f.filename.match(basePattern); + if (m && m[1]) { + orgNamesSet.add(m[1]); + } + }); + + const orgNames = Array.from(orgNamesSet); // e.g. ['jester-lab', 'jefeish'] + robot.log.info(`Orgs updated in PR #${pull_number}: ${orgNames.join(', ')}`); + + // Iterate over each updated org and sync settings + for (const orgName of orgNames) { + const destRepo = env.ADMIN_REPO; + const destinationFolder = env.CONFIG_PATH || '.github'; + await syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder); + } + } catch (err) { + robot.log.error(`Failed to sync safe settings: ${err && err.message ? err.message : err}`); + } +} + +module.exports = { hubSyncHandler }; \ No newline at end of file diff --git a/lib/installationCache.js b/lib/installationCache.js new file mode 100644 index 000000000..fcfb2a75c --- /dev/null +++ b/lib/installationCache.js @@ -0,0 +1,93 @@ +// Installation cache with TTL for GitHub App installations. +// Provides a hybrid approach: live refresh when stale, fast reads otherwise. + +let cachedInstallations = [] +let cachedOrgLogins = [] +let lastFetchedAt = null +let inFlightPromise = null + +const DEFAULT_TTL_MS = 60_000 +function getTtlMs() { + const v = parseInt(process.env.INSTALLATION_CACHE_TTL_MS, 10) + return isNaN(v) || v < 5_000 ? DEFAULT_TTL_MS : v +} + +async function fetchInstallations(robot, { perPage = 100 } = {}) { + const github = await robot.auth() + return github.paginate( + github.apps.listInstallations.endpoint.merge({ per_page: perPage }) + ) +} + +async function refresh(robot, opts = {}) { + if (inFlightPromise) return inFlightPromise + inFlightPromise = (async () => { + try { + const installs = await fetchInstallations(robot, opts) + cachedInstallations = installs + cachedOrgLogins = installs + .filter(i => i.account && i.account.type === 'Organization') + .map(i => i.account.login) + .sort() + lastFetchedAt = new Date() + } catch (e) { + robot.log && robot.log.warn && robot.log.warn(`Installation cache refresh failed: ${e.message}`) + throw e + } finally { + inFlightPromise = null + } + return cachedInstallations + })() + return inFlightPromise +} + +function startPrefetch(robot, opts = {}) { + return refresh(robot, opts) +} + +/** + * Initialize cache (always prefetch once at startup) and log result. + */ +function initCache(robot) { + return startPrefetch(robot) + .then(installs => { + robot.log && robot.log.info && robot.log.info(`Installation cache prefetched ${installs.length} installs (${cachedOrgLogins.length} orgs) [TTL=${getTtlMs()}ms]`) + return true + }) + .catch(e => { + robot.log && robot.log.warn && robot.log.warn(`Installation cache prefetch failed: ${e.message}`) + return false + }) +} + +async function ensureFresh(robot) { + const ttl = getTtlMs() + if (!lastFetchedAt || (Date.now() - lastFetchedAt.getTime()) > ttl) { + try { await refresh(robot) } catch (_) { /* stale ok */ } + } +} + +async function getInstallations(robot) { + await ensureFresh(robot) + return cachedInstallations.slice() +} + +function getOrgLogins() { return cachedOrgLogins.slice() } +function getLastFetchedAt() { return lastFetchedAt } + +// Test-only helper: force cache to appear stale on next access +function __forceStale() { + lastFetchedAt = new Date(Date.now() - (getTtlMs() + 10_000)) +} + +module.exports = { + startPrefetch, + initCache, + refresh, + getInstallations, + getOrgLogins, + getLastFetchedAt, + // for tests / diagnostics + _debug: () => ({ size: cachedInstallations.length, lastFetchedAt }), + __forceStale +} diff --git a/lib/routes.js b/lib/routes.js new file mode 100644 index 000000000..7bbd0e572 --- /dev/null +++ b/lib/routes.js @@ -0,0 +1,483 @@ +/** + * Router setup for Safe Settings UI & API endpoints + * Centralizes Express/Next asset & API wiring away from core app logic. + * + * Exports: + * setupRoutes(robot, getRouter) -> configured router + * + * Responsibilities: + * - Serve static exported Next.js UI (from ui/out) + * - Dashboard HTML entry points + * - JSON API endpoints + * + * This version removes dependency on robot-level cached installation getters + * (`robot.getCachedInstallations`, `robot.getOrganizationLogins`) and instead + * fetches installations live per request. If performance becomes an issue, + * a lightweight in-module memoization layer with short TTL can be reintroduced. + */ + +const path = require('path') +const fs = require('fs') +const express = require('express') +const env = require('./env') +const { getInstallations: cacheGetInstallations, getOrgLogins, getLastFetchedAt } = require('./installationCache') + +// Lightweight commit metadata cache (path+ref -> meta) with TTL to avoid +// repeated GitHub commit lookups across requests. +const COMMIT_META_TTL_MS = parseInt(process.env.COMMIT_META_TTL_MS || '300000') // 5m default +const _commitMetaCache = new Map() // key => { meta, expiresAt } +function getCachedCommitMeta(key) { + const entry = _commitMetaCache.get(key) + if (!entry) return null + if (Date.now() > entry.expiresAt) { _commitMetaCache.delete(key); return null } + return entry.meta +} +function setCachedCommitMeta(key, meta) { + _commitMetaCache.set(key, { meta, expiresAt: Date.now() + COMMIT_META_TTL_MS }) +} + +function setupRoutes(robot, getRouter) { + // Root-level mount (can be changed to '/dashboard' if desired) + const router = getRouter('/') + + // Static assets: produced by Next export/build step (ui/out) + const rootDir = path.join(__dirname, '..') // lib -> project root + const uiPath = path.join(rootDir, 'ui', 'out') + router.use(express.static(uiPath)) + + // HTML entrypoints (exported files). Adjust if you move/rename pages. + router.get('/dashboard', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard.html')) + }) + + router.get('/dashboard/organizations', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'organizations.html')) + }) + + router.get('/dashboard/settings', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'settings.html')) + }) + + router.get('/dashboard/safe-settings-hub', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'safe-settings-hub.html')) + }) + + router.get('/dashboard/env', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'env.html')) + }) + + // Apple touch icon (silence 404s). Replace file logic if you add a real 180x180 asset. + const APPLE_TOUCH_ICON_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAQAAAA9zQYyAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==' // 180x180 transparent PNG + router.get('/apple-touch-icon.png', (req, res) => { + // If a real file exists at project root, serve it; otherwise fallback to embedded transparent PNG. + const filePath = path.join(rootDir, 'apple-touch-icon.png') + fs.access(filePath, fs.constants.R_OK, (err) => { + if (!err) { + return res.sendFile(filePath) + } + const buf = Buffer.from(APPLE_TOUCH_ICON_BASE64, 'base64') + res.setHeader('Content-Type', 'image/png') + res.setHeader('Cache-Control', 'public, max-age=86400, immutable') + res.send(buf) + }) + }) + + /** + * GET /api/organizations + * Returns live organization installation metadata + optional last commit info. + * Query param: disableActivity=true to skip commit lookups (faster). + */ + router.get('/api/organizations', async (req, res) => { + const disableActivity = req.query.disableActivity === 'true' + const includeActivity = !disableActivity + try { + const installs = await cacheGetInstallations(robot) + const orgLogins = getOrgLogins() + const orgInstalls = installs.filter(i => i.account && i.account.type === 'Organization') + const installationDtos = orgInstalls.map(i => ({ id: i.id, account: i.account.login, type: i.account.type, created_at: i.created_at })) + + const lastCommits = {} + if (includeActivity) { + const adminRepoName = env.ADMIN_REPO + if (adminRepoName) { + try { + const orgs = orgLogins + const limit = 5 + const queue = [...orgs] + const runners = [] + const runNext = async () => { + while (queue.length) { + const org = queue.shift() + try { + const install = installs.find(i => i.account && i.account.login.toLowerCase() === org.toLowerCase()) + if (!install) { + lastCommits[org] = { na: true } + continue + } + const githubOrg = await robot.auth(install.id) + const pathPrefix = `${env.CONFIG_PATH.replace(/\/$/, '')}/organizations/${org}` + let commits + try { + commits = await githubOrg.repos.listCommits({ owner: org, repo: adminRepoName, per_page: 1, path: pathPrefix }) + } catch (err) { + if (err.status === 404) { + // Repo or path not found -> NA for repository + lastCommits[org] = { na: true } + continue + } + if (err.status === 409) { // empty repo + lastCommits[org] = null + continue + } + robot.log && robot.log.warn && robot.log.warn(`Commit lookup error for ${org}/${adminRepoName}: ${err.message}`) + lastCommits[org] = null + continue + } + if (Array.isArray(commits.data) && commits.data.length) { + const c = commits.data[0] + const committedAt = (c.commit && c.commit.author && c.commit.author.date) || null + const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null + lastCommits[org] = { sha: c.sha, committed_at: committedAt, message: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, age_seconds: ageSeconds } + } else { + lastCommits[org] = null + } + } catch (loopErr) { + robot.log && robot.log.warn && robot.log.warn(`Unexpected error gathering commit for org ${org}: ${loopErr.message}`) + lastCommits[org] = null + } + } + } + for (let i = 0; i < limit; i++) runners.push(runNext()) + await Promise.all(runners) + } catch (activityErr) { + // On failure mark all orgs as NA and log warning + orgLogins.forEach(o => { lastCommits[o] = { na: true } }) + robot.log && robot.log.warn && robot.log.warn(`Failed gathering last commit activity: ${activityErr.message}`) + } + } else { + orgLogins.forEach(o => { lastCommits[o] = { na: true } }) + } + } + + return res.json({ updatedAt: new Date().toISOString(), organizations: orgLogins, installations: installationDtos, lastCommits: includeActivity ? lastCommits : undefined }) + } catch (e) { + robot.log && robot.log.error && robot.log.error(e) + res.status(500).json({ error: e.message || 'unexpected error' }) + } + }) + + /** + * GET /api/safe-settings-hub/contents/* + * Fetches a file or directory listing from the SAFE_SETTINGS_HUB_ORG / SAFE_SETTINGS_HUB_REPO + * under the configured CONFIG_PATH (default .github). + * + * Examples: + * /api/safe-settings-hub/contents/ -> list CONFIG_PATH root + * /api/safe-settings-hub/contents/repos/foo.yml -> get specific file + * /api/safe-settings-hub/contents/repos?ref=main -> list directory at ref + * /api/safe-settings-hub/contents?recursive=true&maxDepth=2&fetchContent=false -> recursive listing without file bodies + * Note: recursive now defaults to true. Pass recursive=false for single-level listing. + */ + async function hubContent(req, res) { + try { + // Use cached installations (TTL-based freshness) + const installs = await cacheGetInstallations(robot) + const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === env.SAFE_SETTINGS_HUB_ORG.toLowerCase()) + if (!install) { + return res.status(404).json({ error: `Installation for org ${env.SAFE_SETTINGS_HUB_ORG} not found` }) + } + + const github = await robot.auth(install.id) + const wildcardPath = req.params[0] || '' // from the * in the route + const ref = req.query.ref || 'main' + const fullPath = wildcardPath ? path.posix.join(env.CONFIG_PATH, wildcardPath) : env.CONFIG_PATH + // recursive defaults to true unless explicitly disabled with recursive=false + const recursive = (req.query.recursive === 'false') ? false : true + let maxDepth = parseInt(req.query.maxDepth, 5) + if (isNaN(maxDepth) || maxDepth < 1) maxDepth = 5 // safety default + if (maxDepth > 8) maxDepth = 5 // hard cap to avoid abuse + // Unified flag: fetchContent (default true). No other legacy params supported. + const fetchContent = req.query.fetchContent === 'false' ? false : true + + // Commit metadata fetch with global shared cache + per-request memoization + const perRequestCommitCache = new Map() + const fetchCommitMeta = async (p) => { + if (perRequestCommitCache.has(p)) return perRequestCommitCache.get(p) + const cacheKey = `${ref}::${p}` + const cached = getCachedCommitMeta(cacheKey) + if (cached) { perRequestCommitCache.set(p, cached); return cached } + let meta + try { + const commits = await github.repos.listCommits({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, per_page: 1, path: p }) + .then(r => Array.isArray(r.data) ? r.data : []) + if (commits.length) { + const c = commits[0] + const committedAt = c.commit && c.commit.author && c.commit.author.date + const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null + meta = { + lastCommitSha: c.sha, + lastCommitAt: committedAt, + lastCommitMessage: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, + lastCommitAgeSeconds: ageSeconds + } + } else { + meta = { lastCommitSha: null, lastCommitAt: null, lastCommitMessage: null, lastCommitAgeSeconds: null } + } + } catch { + meta = { lastCommitSha: null, lastCommitAt: null, lastCommitMessage: null, lastCommitAgeSeconds: null } + } + setCachedCommitMeta(cacheKey, meta) + perRequestCommitCache.set(p, meta) + return meta + } + + // Helper to fetch a single file (returns null on failure) + const fetchFile = async (p) => { + try { + const fileResp = await github.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: p, ref }) + if (Array.isArray(fileResp.data)) return null + // file + const commitMeta = await fetchCommitMeta(fileResp.data.path) + if (fetchContent && typeof fileResp.data.content === 'string') { + const decoded = Buffer.from(fileResp.data.content, fileResp.data.encoding || 'base64').toString('utf8') + return { + type: fileResp.data.type, + name: path.posix.basename(p), + path: fileResp.data.path, + sha: fileResp.data.sha, + size: fileResp.data.size, + encoding: 'utf8', + content: decoded, + originalEncoding: fileResp.data.encoding || 'base64', + ...commitMeta + } + } + // metadata-only response + return { + type: fileResp.data.type, + name: path.posix.basename(p), + path: fileResp.data.path, + sha: fileResp.data.sha, + size: fileResp.data.size, + content: null, + originalEncoding: fileResp.data.encoding || 'base64', + ...commitMeta + } + } catch (e) { + robot.log && robot.log.warn && robot.log.warn(`Failed to fetch file ${p}: ${e.message}`) + return null + } + } + + // Recursive traversal with depth limiting and basic cycle protection + const seen = new Set() + // Concurrency limiter for directory entry processing + const MAX_DIR_CONCURRENCY = parseInt(process.env.DIR_ENTRY_CONCURRENCY || '6') + async function mapWithLimit(items, mapper) { + const out = [] + let i = 0 + const running = new Set() + async function run() { + if (i >= items.length) return + const idx = i++ + const p = Promise.resolve(mapper(items[idx], idx)).then(r => { out[idx] = r; running.delete(p) }) + running.add(p) + if (running.size >= MAX_DIR_CONCURRENCY) await Promise.race(running) + return run() + } + await run() + await Promise.all([...running]) + return out + } + + const traverseDir = async (dirPath, depth = 0) => { + if (depth >= maxDepth) { + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, truncated: true, entries: [], ...commitMeta } + } + if (seen.has(dirPath)) { + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, cycle: true, entries: [], ...commitMeta } + } + seen.add(dirPath) + let listing + try { + const resp = await github.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: dirPath, ref }) + if (!Array.isArray(resp.data)) { + // Not a directory; fetch as file instead + const f = await fetchFile(dirPath) + return f || { type: 'file', path: dirPath, error: 'unreadable' } + } + listing = resp.data + } catch (e) { + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, error: e.status === 404 ? 'not_found' : e.message, entries: [], ...commitMeta } + } + + const entries = await mapWithLimit(listing, async (item) => { + if (item.type === 'file') { + if (fetchContent) { + const f = await fetchFile(item.path) + if (f) return f + const commitMeta = await fetchCommitMeta(item.path) + return { type: 'file', name: item.name, path: item.path, sha: item.sha, size: item.size, content: null, ...commitMeta } + } + const commitMeta = await fetchCommitMeta(item.path) + return { type: 'file', name: item.name, path: item.path, sha: item.sha, size: item.size, content: null, ...commitMeta } + } else if (item.type === 'dir') { + return traverseDir(item.path, depth + 1) + } + const commitMeta = await fetchCommitMeta(item.path) + return { type: item.type, name: item.name, path: item.path, unsupported: true, ...commitMeta } + }) + const commitMeta = await fetchCommitMeta(dirPath) + return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, entries, ...commitMeta } + } + + const response = await github.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fullPath, + ref + }) + + const data = response.data + if (Array.isArray(data)) { + if (recursive) { + const tree = await traverseDir(fullPath, 0) + return res.json({ + recursive: true, + maxDepth, + ref: ref, + fetchContent, + ...tree + }) + } else { + // non-recursive (original behavior) + const entries = await Promise.all(data.map(async d => { + if (d.type === 'file') { + if (fetchContent) { + const f = await fetchFile(d.path) + if (f) return f + } + return { + name: d.name, + path: d.path, + type: d.type, + sha: d.sha, + size: d.size, + content: null + } + } + return { + name: d.name, + path: d.path, + type: d.type, + sha: d.sha, + size: d.size, + content: null + } + })) + return res.json({ + type: 'dir', + path: fullPath, + entries, + ref: ref, + fetchContent + }) + } + } + + if (typeof data.content === 'string') { + if (fetchContent) { + const decoded = Buffer.from(data.content, data.encoding || 'base64').toString('utf8') + return res.json({ + type: data.type, + path: data.path, + sha: data.sha, + size: data.size, + encoding: 'utf8', + content: decoded, + originalEncoding: data.encoding || 'base64', + ref, + fetchContent: true + }) + } + return res.json({ + type: data.type, + path: data.path, + sha: data.sha, + size: data.size, + content: null, + ref, + fetchContent: false + }) + } + // Unsupported type (symlink, submodule, etc.) + return res.status(415).json({ error: 'Unsupported content type returned by GitHub API' }) + } catch (e) { + if (e.status === 404) { + return res.status(404).json({ error: 'Not found' }) + } + robot.log && robot.log.error && robot.log.error(e) + return res.status(500).json({ error: e.message || 'unexpected error' }) + } + } + + router.get('/api/safe-settings-hub/content', hubContent) + router.get('/api/safe-settings-hub/content/*', hubContent) + + /** + * GET /api/settings/env + * Returns key/value pairs parsed from the project .env file excluding + * standard GitHub App infrastructure variables. + * Query params: + * includeInfra=true -> include normally excluded infrastructure vars + */ + router.get('/api/settings/env', (req, res) => { + try { + // Pull from the runtime env module (already merges defaults + process.env) + const exclude = new Set([ + 'APP_ID', 'WEBHOOK_SECRET', 'PRIVATE_KEY_PATH', 'WEBHOOK_PROXY_URL', 'LOG_LEVEL', + 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', 'PRIVATE_KEY', 'NODE_ENV' + ]) + const includeInfra = req.query.includeInfra === 'true' + // env object contains only the app's known config keys; supplement with a few additional custom vars from process.env if needed + const baseEntries = Object.entries(env) + const extraKeys = ['ENABLE_PR_COMMENT', 'SAFE_SETTINGS_HUB_REPO', 'SAFE_SETTINGS_HUB_ORG'] + extraKeys.forEach(k => { + if (!(k in env) && process.env[k] !== undefined) baseEntries.push([k, process.env[k]]) + }) + const variables = baseEntries + .filter(([k]) => includeInfra || !exclude.has(k)) + .map(([key, value]) => ({ key, value })) + .sort((a, b) => a.key.localeCompare(b.key)) + return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }) + } catch (e) { + robot.log && robot.log.error && robot.log.error(e) + return res.status(500).json({ error: e.message || 'unexpected error' }) + } + }) + + // Cache metadata endpoint + router.get('/api/meta/installations', async (req, res) => { + try { + const installs = await cacheGetInstallations(robot) + const orgs = getOrgLogins() + const last = getLastFetchedAt() + return res.json({ + installations: installs.length, + organizations: orgs.length, + lastFetchedAt: last ? last.toISOString() : null, + ttlMs: process.env.INSTALLATION_CACHE_TTL_MS || '60000' + }) + } catch (e) { + robot.log && robot.log.error && robot.log.error(e) + return res.status(500).json({ error: e.message }) + } + }) + + return router +} + +module.exports = { setupRoutes } diff --git a/package-lock.json b/package-lock.json index 92cf50ebd..cd1411e61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^12.0.2", + "@octokit/auth-app": "^8.0.2", "@probot/adapter-aws-lambda-serverless": "^4.0.3", "deepmerge": "^4.3.1", "eta": "^3.5.0", @@ -1753,68 +1754,6 @@ "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.1.tgz", - "integrity": "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "toad-cache": "^3.7.0", - "universal-github-app-jwt": "^2.2.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", - "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@octokit/app/node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -1877,28 +1816,6 @@ "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/app/node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", @@ -1978,156 +1895,325 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" }, - "node_modules/@octokit/app/node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", - "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==" - }, "node_modules/@octokit/app/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==" }, "node_modules/@octokit/auth-app": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.3.tgz", - "integrity": "sha512-dcaiteA6Y/beAlDLZOPNReN3FGHu+pARD6OHfh3T9f3EO09++ec+5wt3KtGGSSs2Mp5tI8fQwdMOEnrzBLfgUA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.2.tgz", + "integrity": "sha512-dLTmmA9gUlqiAJZgozfOsZFfpN/OldH3xweb7lqSnngax5Rs+PfO5dDlokaBfc41H1xOtsLYV5QqR0DkBAtPmw==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^7.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.1.0", - "deprecation": "^2.3.1", - "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", - "universal-github-app-jwt": "^1.1.2", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-app": "^9.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "toad-cache": "^3.7.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "node_modules/@octokit/auth-app/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, - "node_modules/@octokit/auth-app/node_modules/lru-cache": { - "name": "@wolfy1339/lru-cache", - "version": "11.0.2-patch.1", - "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", - "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", - "license": "ISC", + "node_modules/@octokit/auth-app/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, "engines": { - "node": "18 >=18.20 || 20 || >=22" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-app/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/@octokit/auth-oauth-app": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", - "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", + "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-oauth-app/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/@octokit/auth-oauth-device": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", - "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", + "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", "license": "MIT", "dependencies": { - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/oauth-methods": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-oauth-device/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/@octokit/auth-oauth-user": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", - "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", + "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/oauth-methods": "^6.0.0", + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/auth-oauth-user/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/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -2223,69 +2309,25 @@ "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" }, "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", - "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", - "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/auth-unauthenticated": "^7.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.0", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", - "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" + "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", + "node_modules/@octokit/oauth-app": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", + "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", + "@octokit/auth-oauth-app": "^9.0.1", + "@octokit/auth-oauth-user": "^6.0.0", + "@octokit/auth-unauthenticated": "^7.0.1", + "@octokit/core": "^7.0.2", + "@octokit/oauth-authorization-url": "^8.0.0", "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^7.0.0" }, "engines": { @@ -2354,28 +2396,6 @@ "node": ">= 20" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", - "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/@octokit/oauth-app/node_modules/@octokit/openapi-types": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", @@ -2426,45 +2446,91 @@ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" }, "node_modules/@octokit/oauth-authorization-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", - "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", + "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/oauth-methods": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", - "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", + "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", "license": "MIT", "dependencies": { - "@octokit/oauth-authorization-url": "^6.0.2", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0" + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" } }, "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@octokit/oauth-methods/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/@octokit/openapi-types": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", @@ -3675,9 +3741,9 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { "@types/ms": "*", @@ -9678,12 +9744,12 @@ } }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -10586,6 +10652,172 @@ "@octokit/core": ">=5" } }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.4.tgz", + "integrity": "sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/octokit-auth-probot/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/octokit-auth-probot/node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "license": "ISC", + "engines": { + "node": "18 >=18.20 || 20 || >=22" + } + }, + "node_modules/octokit-auth-probot/node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, "node_modules/octokit/node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -13219,14 +13451,10 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universal-github-app-jwt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", - "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.2" - } + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" }, "node_modules/universal-user-agent": { "version": "6.0.1", diff --git a/package.json b/package.json index e765fb80e..d295fce1a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^12.0.2", + "@octokit/auth-app": "^8.0.2", "@probot/adapter-aws-lambda-serverless": "^4.0.3", "deepmerge": "^4.3.1", "eta": "^3.5.0", diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 000000000..97a2bb84e --- /dev/null +++ b/ui/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next", "next/core-web-vitals"] +} diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..26b002aac --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for commiting if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..4f3cc28d3 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/route.ts`. The page auto-updates as you edit the file. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## API Routes + +This directory contains example API routes for the headless API app. + +For more details, see [route.js file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route). diff --git a/ui/favicon.ico b/ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3a78f556b0930171aacf738ccee96a5fc446928b GIT binary patch literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu literal 0 HcmV?d00001 diff --git a/ui/next.config.js b/ui/next.config.js new file mode 100644 index 000000000..d5d2d2c15 --- /dev/null +++ b/ui/next.config.js @@ -0,0 +1,10 @@ + +const nextConfig = { + output: "export", + // Disable Next.js ESLint checks during builds + eslint: { + ignoreDuringBuilds: true, + }, +}; + +module.exports = nextConfig; diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 000000000..8ab1771e6 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,5059 @@ +{ + "name": "ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.1.0", + "dependencies": { + "@primer/octicons-react": "^19.15.5", + "bootstrap": "^5.3.7", + "next": "15.4.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.4.7", + "tailwindcss": "^4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.7.tgz", + "integrity": "sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.7.tgz", + "integrity": "sha512-asj3RRiEruRLVr+k2ZC4hll9/XBzegMpFMr8IIRpNUYypG86m/a76339X2WETl1C53A512w2INOc2KZV769KPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.7.tgz", + "integrity": "sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.7.tgz", + "integrity": "sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.7.tgz", + "integrity": "sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.7.tgz", + "integrity": "sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.7.tgz", + "integrity": "sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.7.tgz", + "integrity": "sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.7.tgz", + "integrity": "sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.7.tgz", + "integrity": "sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@primer/octicons-react": { + "version": "19.15.5", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-19.15.5.tgz", + "integrity": "sha512-JEoxBVkd6F8MaKEO1QKau0Nnk3IVroYn7uXGgMqZawcLQmLljfzua3S1fs2FQs295SYM9I6DlkESgz5ORq5yHA==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", + "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "postcss": "^8.4.41", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.40.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.40.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/bootstrap": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", + "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "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==", + "devOptional": true, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.7.tgz", + "integrity": "sha512-tkKKNVJKI4zMIgTpvG2x6mmdhuOdgXUL3AaSPHwxLQkvzi4Yryqvk6B0R5Z4gkpe7FKopz3ZmlpePH3NTHy3gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.4.7", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "15.4.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.4.7.tgz", + "integrity": "sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==", + "license": "MIT", + "dependencies": { + "@next/env": "15.4.7", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.4.7", + "@next/swc-darwin-x64": "15.4.7", + "@next/swc-linux-arm64-gnu": "15.4.7", + "@next/swc-linux-arm64-musl": "15.4.7", + "@next/swc-linux-x64-gnu": "15.4.7", + "@next/swc-linux-x64-musl": "15.4.7", + "@next/swc-win32-arm64-msvc": "15.4.7", + "@next/swc-win32-x64-msvc": "15.4.7", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..bc0097754 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "export": "next export", + "start": "next start" + }, + "dependencies": { + "@primer/octicons-react": "^19.15.5", + "bootstrap": "^5.3.7", + "next": "15.4.7" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.4.7", + "tailwindcss": "^4" + } +} diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 000000000..7762395be --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/public/shield.png b/ui/public/shield.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e4b6ec7bcbde72dcd715b6269b22e48c668976 GIT binary patch literal 24289 zcmXtgV|-oT^Yy*4bz?TRjmEaq*iIVTjcu!8o1|f5+qTWdcJkcs@AH4&oO9mY*?acP znl)?ojZ{{YLPo$x0002UGScFz000md{9OSD1Acb=3f=?1fG(<1qJXMN!V>`C3qVF( zMBUTitOMSYP%BmFrLC#Fbs>qN`uh7&Em1Y8nG^Px=)dxTc7K3jc#U2TxEY_p#@}EL z{kD8GH+|SiLr4jrdhv0krIA8-;7M7`$Y1|4Y(6R^oqMWy$o;!0_qiP7Z7N?#YEW-7 z(~YcnJ4t3(=xF*kUul?s?*WxWo+vC2uXnR>itj8tff5Bxt$Vy)fAMQ;bbv4`$2~?? zn6T(EcimkczP;=Dg*9%C1E;F38+Uxe=W2U*!{@OTx@1Y3miy`abuos%vD)YUV-2-G zhmH3fH%S)NY1Ykep)oL64Fp?i1QXgowe13%daXK9pV`eIIWX-l-Ld0SW-Mg#`-gcxm_?|I^CNGUY_Kli@*MoLIa_CgL+nL?n+PGp^sh$x3oxUPY%!7d4Q

xNbM`PG=BC2pI)-gc#?ujEY|nz)%svQ;+C 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 M07*qoM6N<$g22I`vj6}9 literal 0 HcmV?d00001 diff --git a/ui/shield.png b/ui/shield.png new file mode 100644 index 0000000000000000000000000000000000000000..3a78f556b0930171aacf738ccee96a5fc446928b GIT binary patch literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu literal 0 HcmV?d00001 diff --git a/ui/src/app/[slug]/route.js b/ui/src/app/[slug]/route.js new file mode 100644 index 000000000..290f31842 --- /dev/null +++ b/ui/src/app/[slug]/route.js @@ -0,0 +1,18 @@ +const { NextResponse } = require('next/server'); + +// JS version: no type annotations +export async function GET(request, context) { + const params = context && context.params ? context.params : {}; + const slug = params.slug || ''; + return NextResponse.json({ message: `Hello ${slug}!` }); +} + +export async function generateStaticParams() { + // Replace with your actual slugs + return [ + { slug: 'example1' }, + { slug: 'example2' } + ]; +} + +export const dynamic = 'force-static'; \ No newline at end of file diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx new file mode 100644 index 000000000..9773cc064 --- /dev/null +++ b/ui/src/app/components/EnvVariables.jsx @@ -0,0 +1,161 @@ +'use client'; +import React, { useEffect, useState, useMemo } from 'react'; +import { SearchIcon, SyncIcon, EyeClosedIcon, EyeIcon, ShieldIcon, CopyIcon, ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react'; +import { useHydrated } from '../hooks/useHydrated'; + +const SENSITIVE_REGEX = /(secret|token|key|password|private)/i; + +export default function EnvVariables() { + const hydrated = useHydrated(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [rows, setRows] = useState([]); + const [search, setSearch] = useState(''); + const [includeInfra, setIncludeInfra] = useState(false); + const [revealAll, setRevealAll] = useState(false); + const [lastFetchedAt, setLastFetchedAt] = useState(null); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); + + const fetchData = () => { + if (!hydrated) return; + setLoading(true); setError(null); + fetch(`/api/settings/env${includeInfra ? '?includeInfra=true' : ''}`) + .then(r => { + if (!r.ok) { + throw new Error(`Unable to retrieve environment variables (HTTP ${r.status}). Please try again later.`); + } + return r.json(); + }) + .then(json => { + setRows(json.variables || []); + setLastFetchedAt(new Date(json.updatedAt || Date.now())); + }) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { fetchData(); /* eslint-disable-next-line */ }, [hydrated, includeInfra]); + + const filtered = useMemo(() => { + if (!search) return rows; + const q = search.toLowerCase(); + return rows.filter(r => r.key.toLowerCase().includes(q) || (r.value + '').toLowerCase().includes(q)); + }, [rows, search]); + + const sorted = useMemo(() => { + if (!sortConfig.key || !sortConfig.direction) return filtered; + const list = [...filtered]; + list.sort((a, b) => { + let av = a[sortConfig.key]; + let bv = b[sortConfig.key]; + if (av == null) av = ''; + if (bv == null) bv = ''; + av = (av + '').toLowerCase(); + bv = (bv + '').toLowerCase(); + if (av < bv) return sortConfig.direction === 'asc' ? -1 : 1; + if (av > bv) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + return list; + }, [filtered, sortConfig]); + + const cycleSort = (key) => { + setSortConfig(prev => { + if (prev.key === key) { + if (prev.direction === 'asc') return { key, direction: 'desc' }; + if (prev.direction === 'desc') return { key: null, direction: null }; + } + return { key, direction: 'asc' }; + }); + }; + + const renderSortIcon = (key) => { + if (sortConfig.key !== key) return ; + if (sortConfig.direction === 'asc') return ; + if (sortConfig.direction === 'desc') return ; + return ; + }; + + const maskedValue = (k, v) => { + if (revealAll) return v; + if (!SENSITIVE_REGEX.test(k)) return v; + if (!v) return v; + if (v.length <= 4) return '*'.repeat(v.length); + return v.slice(0, 2) + '***' + v.slice(-2); + }; + + const copyToClipboard = (text) => { + try { navigator.clipboard.writeText(text); } catch(_) {} + } + + return ( +

+
+
+ +
+ + setSearch(e.target.value)} /> +
+
+
+ +
+ setIncludeInfra(e.target.checked)} /> + +
+
+ setRevealAll(e.target.checked)} /> + +
+
+
+
+ + +
+
+
+ + {loading &&
Loading…
} + {error && !loading &&
Error: {error}
} + {!loading && !error && filtered.length === 0 &&
No variables
} + + {!loading && !error && filtered.length > 0 && ( +
+ + + + + + + + + + + {sorted.map(r => { + const sensitive = SENSITIVE_REGEX.test(r.key); + return ( + + + + + + + ); + })} + +
cycleSort('key')} className="theme-text-primary user-select-none" style={{ width: '28%', cursor: 'pointer' }}>Key {renderSortIcon('key')} cycleSort('value')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Value {renderSortIcon('value')}
{r.key} + {maskedValue(r.key, r.value)} + {sensitive && } + +
+
+ )} +
+ {sorted.length} shown / {rows.length} total + {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} +
+
+ ); +} diff --git a/ui/src/app/components/OrganizationsTable.jsx b/ui/src/app/components/OrganizationsTable.jsx new file mode 100644 index 000000000..56bd12c41 --- /dev/null +++ b/ui/src/app/components/OrganizationsTable.jsx @@ -0,0 +1,252 @@ +'use client'; + +import React, { useState, useMemo, useEffect } from 'react'; +import { ChevronUpIcon, ChevronDownIcon, SearchIcon } from '@primer/octicons-react'; +import { useHydrated } from '../hooks/useHydrated'; + +// Mock organizations used when /api/organizations returns 404 +const MOCK_ORGS = [ + { id: 1, name: 'mock-org-one', lastSyncDate: new Date(Date.now() - 3600 * 1000).toISOString(), lastSyncMessage: 'Initial mock sync', lastSyncSha: 'abcdef1', ageSeconds: 3600 }, + { id: 2, name: 'example-inc', lastSyncDate: new Date(Date.now() - 7200 * 1000).toISOString(), lastSyncMessage: 'Second mock sync', lastSyncSha: 'abcdef2', ageSeconds: 7200 }, + { id: 3, name: 'demo-labs', lastSyncDate: null, lastSyncMessage: null, lastSyncSha: null, ageSeconds: null, na: true } +]; + +const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [fetched, setFetched] = useState([]); + const hydrated = useHydrated(); + + // Fetch real organizations from backend API on client hydration + useEffect(() => { + if (!hydrated) return; // avoid SSR mismatch + let cancelled = false; + setLoading(true); + fetch('/api/organizations') + .then(r => { + if (!r.ok) { + throw new Error(`Unable to retrieve organizations (HTTP ${r.status}). Please try again later.`); + } + return r.json(); + }) + .then(json => { + if (!json || cancelled) return; + const lastCommits = json.lastCommits || {} + const mapped = (json.installations || []).map(i => { + const lc = lastCommits[i.account]; + return { + id: i.id, + name: i.account, + lastSyncDate: lc && lc.committed_at ? lc.committed_at : null, + lastSyncSha: lc && lc.sha ? lc.sha : null, + lastSyncMessage: lc && lc.message ? lc.message : null, + ageSeconds: lc && typeof lc.age_seconds === 'number' ? lc.age_seconds : null, + na: lc && lc.na === true + }; + }); + setFetched(mapped); + setError(null); + }) + .catch(e => { if (!cancelled) setError(e.message); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [hydrated]); + + const data = fetched.length > 0 ? fetched : (propOrganizations.length > 0 ? propOrganizations : []); + + // Format date for display with hydration-safe approach + const formatLastSync = (org) => { + if (org.na) return NA; + if (!org.lastSyncDate) return ; + const dateObj = new Date(org.lastSyncDate); + let ageSec = org.ageSeconds; + if (hydrated && (ageSec == null)) { + ageSec = Math.floor((Date.now() - dateObj.getTime()) / 1000); + } + const rel = (() => { + if (ageSec == null) return ''; + if (ageSec < 60) return '0m'; + const mTotal = Math.floor(ageSec / 60); + if (mTotal < 60) return `${mTotal}m`; + const hTotal = Math.floor(mTotal / 60); + if (hTotal < 24) { + const remM = mTotal % 60; + return remM ? `${hTotal}h ${remM}m` : `${hTotal}h`; + } + const dTotal = Math.floor(hTotal / 24); + const remH = hTotal % 24; + return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; + })(); + const fullStamp = `${dateObj.getFullYear()}-${String(dateObj.getMonth()+1).padStart(2,'0')}-${String(dateObj.getDate()).padStart(2,'0')} ${String(dateObj.getHours()).padStart(2,'0')}:${String(dateObj.getMinutes()).padStart(2,'0')}:${String(dateObj.getSeconds()).padStart(2,'0')}`; + const tooltip = [fullStamp, org.lastSyncMessage, org.lastSyncSha ? `SHA: ${org.lastSyncSha.slice(0,7)}` : null] + .filter(Boolean) + .join('\n'); + return {rel}; + }; + const lastSyncColStyle = { width: '170px', fontVariantNumeric: 'tabular-nums' }; + + // Filter organizations based on search term + const filteredData = useMemo(() => { + return data.filter(org => + org.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [data, searchTerm]); + + // Sort organizations + const sortedData = useMemo(() => { + if (!sortConfig.key) return filteredData; + + return [...filteredData].sort((a, b) => { + let aValue = a[sortConfig.key]; + let bValue = b[sortConfig.key]; + + // Convert dates to timestamps for comparison + if (sortConfig.key === 'lastSyncDate') { + aValue = new Date(aValue).getTime(); + bValue = new Date(bValue).getTime(); + } + + if (aValue < bValue) { + return sortConfig.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === 'asc' ? 1 : -1; + } + return 0; + }); + }, [filteredData, sortConfig]); + + // Handle column sorting + const handleSort = (key) => { + setSortConfig(prevConfig => { + if (prevConfig.key === key) { + if (prevConfig.direction === 'asc') { + return { key, direction: 'desc' }; + } else if (prevConfig.direction === 'desc') { + return { key: null, direction: null }; + } + } + return { key, direction: 'asc' }; + }); + }; + + // Render sort icon + const renderSortIcon = (columnKey) => { + if (sortConfig.key !== columnKey) { + return ; + } + if (sortConfig.direction === 'asc') { + return ; + } + if (sortConfig.direction === 'desc') { + return ; + } + return ; + }; + + return ( +
+ {/* Search Bar */} +
+
+
+
+ + + + setSearchTerm(e.target.value)} + /> +
+
+
+ + Showing {sortedData.length} of {data.length} organizations + +
+
+
+ + {/* Table */} +
+ + + + + + + + + {loading && ( + + + + )} + {!loading && error && ( + + + + )} + {!loading && !error && sortedData.length > 0 ? ( + sortedData.map((org) => ( + + + + + )) + ) : ( + !loading && !error && ( + + + + ) + )} + +
handleSort('name')} + > + Organization Name + {renderSortIcon('name')} + handleSort('lastSyncDate')} + > + Last Safe-settings Sync + {renderSortIcon('lastSyncDate')} +
Loading organizations…
Error: {error}
+ {org.name} + + {formatLastSync(org)} +
+ {searchTerm ? `No organizations found matching "${searchTerm}"` : 'No organizations available'} +
+
+ + {/* Table Footer Info */} + {sortedData.length > 0 && ( +
+ + {searchTerm && `Filtered by: "${searchTerm}"`} + {sortConfig.key && ( + + • Sorted by: {sortConfig.key === 'name' ? 'Organization Name' : 'Last Safe-settings Sync'} + ({sortConfig.direction === 'asc' ? 'A-Z' : 'Z-A'}) + + )} + +
+ )} +
+ ); +}; + +export default OrganizationsTable; diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx new file mode 100644 index 000000000..e4292aebc --- /dev/null +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -0,0 +1,412 @@ +'use client'; +import React, { useEffect, useState, useMemo, useCallback } from 'react'; +import { SearchIcon, SyncIcon, FileIcon, FileDirectoryIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; +import { useHydrated } from '../hooks/useHydrated'; + +// Simple mock tree used when API returns 404 (dev convenience) +const MOCK_TREE = { + name: '.github', + path: '.github', + type: 'dir', + lastCommitAt: new Date(Date.now() - 3600 * 1000).toISOString(), + entries: [ + { + name: 'settings.yml', + path: '.github/settings.yml', + type: 'file', + lastCommitAt: new Date(Date.now() - 1800 * 1000).toISOString(), + lastCommitMessage: 'chore: mock settings', + lastCommitSha: 'mock123' + }, + { + name: 'CODEOWNERS', + path: '.github/CODEOWNERS', + type: 'file', + lastCommitAt: new Date(Date.now() - 7200 * 1000).toISOString(), + lastCommitMessage: 'feat: add mock CODEOWNERS', + lastCommitSha: 'mock456' + }, + { + name: 'workflows', + path: '.github/workflows', + type: 'dir', + lastCommitAt: new Date(Date.now() - 5400 * 1000).toISOString(), + entries: [ + { + name: 'ci.yml', + path: '.github/workflows/ci.yml', + type: 'file', + lastCommitAt: new Date(Date.now() - 2500 * 1000).toISOString(), + lastCommitMessage: 'ci: mock workflow', + lastCommitSha: 'mock789' + } + ] + } + ] +}; + +export default function SafeSettingsHubContent() { + const hydrated = useHydrated(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [rootTree, setRootTree] = useState(null); // recursive tree response + const [search, setSearch] = useState(''); + // Tree view removed; we now render a flattened table. + const [lastFetchedAt, setLastFetchedAt] = useState(null); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); // direction: 'asc' | 'desc' | null + const [expandedPaths, setExpandedPaths] = useState(() => new Set()); // which directory paths are expanded + + const fetchData = () => { + if (!hydrated) return; + setLoading(true); setError(null); + // Always ask for recursive tree; server may limit depth + // Explicitly request content bodies (fetchContent=true is default but sent for clarity) + fetch('/api/safe-settings-hub/content?fetchContent=true') + .then(r => { + if (!r.ok) { + // Surface a clear error message instead of falling back to mock data + throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status}). Please try again later.`); + } + return r.json(); + }) + .then(json => { + // On success set the returned tree + setRootTree(json); + setLastFetchedAt(new Date()); + }) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { fetchData(); }, [hydrated]); + + // Flatten nodes for table display + const flattenNodes = useCallback((node, acc = [], depth = 0) => { + if (!node) return acc; + acc.push({ + name: node.name, + path: node.path, + type: node.type, + lastCommitAt: node.lastCommitAt, + lastCommitMessage: node.lastCommitMessage, + lastCommitSha: node.lastCommitSha, + depth + }); + if (node.type === 'dir' && Array.isArray(node.entries)) { + node.entries.forEach(child => flattenNodes(child, acc, depth + 1)); + } + return acc; + }, []); + + const filterTree = useCallback((node) => { + if (!node) return null; + const term = search.toLowerCase(); + const matches = (n) => !term || n.name.toLowerCase().includes(term) || n.path.toLowerCase().includes(term); + if (node.type === 'file') { + return matches(node) ? node : null; + } + if (node.type === 'dir') { + const children = (node.entries || []).map(filterTree).filter(Boolean); + if (matches(node) || children.length > 0) { + return { ...node, entries: children }; + } + return null; + } + return null; + }, [search]); + + const filteredTree = useMemo(() => filterTree(rootTree), [rootTree, filterTree]); + + // If the root contains a top-level 'safe-settings' directory, treat that directory as the display root + const displayTree = useMemo(() => { + if (!filteredTree) return null; + if (filteredTree.type === 'dir') { + const nameMatch = (n) => n && n.type === 'dir' && n.name && n.name.toLowerCase().includes('safe-settings'); + // Prefer immediate child named 'safe-settings' + const immediate = (filteredTree.entries || []).find(nameMatch); + if (immediate) return immediate; + // Fallback: search descendants up to a small depth for 'safe-settings' + const findDescendant = (node, depth = 0, maxDepth = 3) => { + if (!node || node.type !== 'dir' || depth >= maxDepth) return null; + for (const child of node.entries || []) { + if (nameMatch(child)) return child; + } + for (const child of node.entries || []) { + if (child.type === 'dir') { + const found = findDescendant(child, depth + 1, maxDepth); + if (found) return found; + } + } + return null; + }; + const found = findDescendant(filteredTree, 0, 3); + if (found) return found; + } + return filteredTree; + }, [filteredTree]); + + // When a search filter is applied, auto-expand all ancestor directories that contain matches + useEffect(() => { + if (!search) return; // only on active filter + if (!displayTree || displayTree.type !== 'dir') return; + const dirsToExpand = new Set(); + const walk = (node) => { + if (!node || node.type !== 'dir') return false; + let containsMatch = false; + for (const child of node.entries || []) { + if (child.type === 'dir') { + if (walk(child)) { + containsMatch = true; + dirsToExpand.add(child.path); // expand child dir to show deeper matches + } + } else { + // Any file present means this dir should be opened if it passed filtering + containsMatch = true; + } + } + return containsMatch; + }; + walk(displayTree); + // Also expand top-level dirs that survived filtering and have entries + (displayTree.entries || []).forEach(e => { if (e.type === 'dir') dirsToExpand.add(e.path); }); + setExpandedPaths(prev => { + const next = new Set(prev); + dirsToExpand.forEach(p => next.add(p)); + return next; + }); + }, [search, displayTree]); + + const flatList = useMemo(() => { + if (!displayTree) return []; + // If display root is a directory, list its children instead of the directory itself (hide intermediate root) + if (displayTree.type === 'dir') { + return displayTree.entries.flatMap(child => flattenNodes(child, [], 0)); + } + return flattenNodes(displayTree, [], 0); + }, [displayTree, flattenNodes]); + + // Build hierarchical visible list honoring expandedPaths and optional sorting + const sortedFlatList = useMemo(() => { + if (!displayTree) return []; + // function to sort entries inside a directory when sorting enabled + const sortEntries = (entries) => { + if (!sortConfig.key || !sortConfig.direction) return entries; + const key = sortConfig.key; + return [...entries].sort((a, b) => { + let av; let bv; + switch (key) { + case 'name': av = a.name.toLowerCase(); bv = b.name.toLowerCase(); break; + case 'path': av = a.path.toLowerCase(); bv = b.path.toLowerCase(); break; + case 'lastCommitAt': av = a.lastCommitAt ? new Date(a.lastCommitAt).getTime() : 0; bv = b.lastCommitAt ? new Date(b.lastCommitAt).getTime() : 0; break; + default: av = a[key]; bv = b[key]; + } + if (av < bv) return sortConfig.direction === 'asc' ? -1 : 1; + if (av > bv) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + }; + const out = []; + const process = (node, depth) => { + if (!node) return; + if (node.type === 'dir') { + const children = sortEntries(node.entries || []); + children.forEach(child => { + out.push({ + name: child.name, + path: child.path, + type: child.type, + lastCommitAt: child.lastCommitAt, + lastCommitMessage: child.lastCommitMessage, + lastCommitSha: child.lastCommitSha, + depth + }); + if (child.type === 'dir' && expandedPaths.has(child.path)) { + process(child, depth + 1); + } + }); + } else { + out.push({ + name: node.name, + path: node.path, + type: node.type, + lastCommitAt: node.lastCommitAt, + lastCommitMessage: node.lastCommitMessage, + lastCommitSha: node.lastCommitSha, + depth + }); + } + }; + // Start processing at displayTree (hiding any intermediate 'safe-settings' wrapper) + process(displayTree, 0); + return out; + }, [displayTree, sortConfig, expandedPaths]); + + const cycleSort = (key) => { + setSortConfig(prev => { + if (prev.key === key) { + if (prev.direction === 'asc') return { key, direction: 'desc' }; + if (prev.direction === 'desc') return { key: null, direction: null }; // clear + } + return { key, direction: 'asc' }; + }); + }; + + const renderSortIcon = (key) => { + if (sortConfig.key !== key) return ; + if (sortConfig.direction === 'asc') return ; + if (sortConfig.direction === 'desc') return ; + return ; + }; + + const toggleDir = (path) => { + setExpandedPaths(prev => { + const next = new Set(prev); + if (next.has(path)) next.delete(path); else next.add(path); + return next; + }); + }; + + const collectAllDirPaths = useCallback((node, acc = []) => { + if (!node) return acc; + if (node.type === 'dir') { + if (node.path && node.path !== '.github') acc.push(node.path); // skip synthetic root label + (node.entries || []).forEach(child => collectAllDirPaths(child, acc)); + } + return acc; + }, []); + + const expandAll = () => { + if (!filteredTree) return; + const all = collectAllDirPaths(filteredTree, []); + setExpandedPaths(new Set(all)); + }; + + const collapseAll = () => setExpandedPaths(new Set()); + + const formatRelative = (iso) => { + if (!iso) return null; + const dt = new Date(iso); + let diffSec = Math.floor((Date.now() - dt.getTime()) / 1000); + if (diffSec < 0) diffSec = 0; + if (diffSec < 60) return '0m'; + const mTotal = Math.floor(diffSec / 60); + if (mTotal < 60) return `${mTotal}m`; + const hTotal = Math.floor(mTotal / 60); + if (hTotal < 24) { + const remM = mTotal % 60; + return remM ? `${hTotal}h ${remM}m` : `${hTotal}h`; + } + const dTotal = Math.floor(hTotal / 24); + const remH = hTotal % 24; + return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; + }; + + // Table columns: Name (indented), Path, Type, Last update + + return ( + <> +
+
+
+
+ + + + setSearch(e.target.value)} /> +
+
+
+
+ + + +
+
+
+ + {/*
+
+
+
+ + + + setSearchTerm(e.target.value)} + /> +
+
+
+ + Showing {sortedData.length} of {data.length} organizations + +
+
+
*/} + + {loading &&
Loading…
} + {error && !loading &&
Error: {error}
} + {!loading && !error && !displayTree &&
No entries
} + + {!loading && !error && displayTree && ( +
+
+ + + + + + + + + + {sortedFlatList.map(node => { + const isDir = node.type === 'dir'; + const expanded = isDir && expandedPaths.has(node.path); + return ( + isDir && toggleDir(node.path)} style={isDir ? { cursor: 'pointer' } : undefined}> + + + + + ); + })} + +
cycleSort('name')} className="theme-text-primary user-select-none" style={{ width: '35%', cursor: 'pointer' }}>Name {renderSortIcon('name')} cycleSort('path')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Path {renderSortIcon('path')} cycleSort('lastCommitAt')} className="theme-text-primary user-select-none" style={{ width: '170px', cursor: 'pointer' }}>Last update {renderSortIcon('lastCommitAt')}
+ + {isDir ? ( + expanded ? : + ) : ( + + )} + {isDir && } + {node.name} + + {node.path} + {node.lastCommitAt ? formatRelative(node.lastCommitAt) : '—'} +
+
+
+ )} +
+ {!loading && !error && ( +
+ {sortedFlatList.length} items shown + {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} +
+ )} + {/* Removed inner bordered wrapper styles so only outer page container shows a border */} + + ); +} diff --git a/ui/src/app/components/ThemeContext.jsx b/ui/src/app/components/ThemeContext.jsx new file mode 100644 index 000000000..75470143d --- /dev/null +++ b/ui/src/app/components/ThemeContext.jsx @@ -0,0 +1,71 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect } from 'react'; + +const ThemeContext = createContext(); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +export const ThemeProvider = ({ children }) => { + // Always start with 'light' for SSR consistency + const [theme, setTheme] = useState('light'); + const [mounted, setMounted] = useState(false); + + // Only run on client side + useEffect(() => { + setMounted(true); + + // Migrate old theme key if it exists + const oldTheme = localStorage.getItem('safe-settings-theme'); + if (oldTheme && !localStorage.getItem('theme')) { + localStorage.setItem('theme', oldTheme); + localStorage.removeItem('safe-settings-theme'); + } + + // Get theme from localStorage + const savedTheme = localStorage.getItem('theme') || 'light'; + + // Set state (will trigger re-render with correct theme) + setTheme(savedTheme); + + // Apply to DOM immediately to prevent flash + document.documentElement.setAttribute('data-theme', savedTheme); + }, []); + + // Apply theme changes to DOM + useEffect(() => { + if (mounted) { + document.documentElement.setAttribute('data-theme', theme); + } + }, [theme, mounted]); + + const toggleTheme = () => { + const newTheme = theme === 'light' ? 'dark' : 'light'; + setTheme(newTheme); + localStorage.setItem('theme', newTheme); + }; + + const setSpecificTheme = (themeName) => { + setTheme(themeName); + localStorage.setItem('theme', themeName); + }; + + // Render children immediately - no hiding + return ( + + {children} + + ); +}; diff --git a/ui/src/app/components/ThemeToggle.jsx b/ui/src/app/components/ThemeToggle.jsx new file mode 100644 index 000000000..803c9d997 --- /dev/null +++ b/ui/src/app/components/ThemeToggle.jsx @@ -0,0 +1,17 @@ +'use client'; + +import { useTheme } from './ThemeContext'; + +export default function ThemeToggle() { + const { theme, toggleTheme, isDark } = useTheme(); + + return ( + + ); +} diff --git a/ui/src/app/components/TitleBar.css b/ui/src/app/components/TitleBar.css new file mode 100644 index 000000000..d10a7ea76 --- /dev/null +++ b/ui/src/app/components/TitleBar.css @@ -0,0 +1,174 @@ +/* TitleBar Component-Specific Styles */ + +/* Header styles */ +.title-header { + background: #333; + color: #fff; + min-height: 60px; /* Ensure consistent height */ +} + +/* Theme-specific header styles */ +[data-theme="light"] .title-header, +body.light-theme .title-header { + background: #333; + color: #fff; +} + +[data-theme="dark"] .title-header, +body.dark-theme .title-header { + background: #161b22; + color: #f0f6fc; +} + +/* Navigation bar - consistent height and styling */ +.title-nav { + min-height: 48px; /* Consistent nav height */ + border-bottom: 1px solid var(--border-color, #dee2e6) !important; + background: var(--bg-secondary, #f6f8fa); /* Default light theme background */ +} + +/* Data-theme selectors for immediate theme application */ +[data-theme="light"] .title-nav, +body.light-theme .title-nav { + background: #f6f8fa; + color: #24292f; + border-bottom: 1px solid #dee2e6 !important; +} + +[data-theme="dark"] .title-nav, +body.dark-theme .title-nav { + background: #22272e; + color: #f6f8fa; + border-bottom: 1px solid #666a6e !important; +} + +/* Theme toggle button */ +.theme-toggle-btn { + border: none !important; + background: transparent !important; + cursor: pointer !important; + padding: 2px !important; + border-radius: 6px !important; +} + +.theme-toggle-btn .theme-toggle-icon { + transition: color 0.15s; + color: lightgray; /* Default icon color */ +} + +/* Theme-specific toggle button styles */ +[data-theme="light"] .theme-toggle-btn .theme-toggle-icon, +body.light-theme .theme-toggle-btn .theme-toggle-icon { + color: #fff; +} + +[data-theme="dark"] .theme-toggle-btn .theme-toggle-icon, +body.dark-theme .theme-toggle-btn .theme-toggle-icon { + color: #f0f6fc; +} + +[data-theme="light"] .theme-toggle-btn:hover, +body.light-theme .theme-toggle-btn:hover { + background: rgba(255, 255, 255, 0.1) !important; +} + +[data-theme="dark"] .theme-toggle-btn:hover, +body.dark-theme .theme-toggle-btn:hover { + background: rgba(240, 246, 252, 0.1) !important; +} + +.theme-toggle-btn:hover .theme-toggle-icon, +.theme-toggle-btn:focus .theme-toggle-icon { + color: yellow; /* Hover icon color */ +} + +/* Navigation links */ +.nav-link-custom { + border: none; +} + +/* Light theme nav links */ +[data-theme="light"] .nav-link-custom, +body.light-theme .nav-link-custom { + color: #24292f !important; +} + +/* Dark theme nav links */ +[data-theme="dark"] .nav-link-custom, +body.dark-theme .nav-link-custom { + color: #f6f8fa; +} + +/* Navigation menu items */ +.nav-link.menu-hover { + border-radius: 5px !important; + margin: 10px 10px 10px 10px !important; + padding: 5px 10px !important; + transition: background 0.15s, color 0.15s; + border: 1px solid transparent !important; /* Invisible border to maintain box model */ +} + +.nav-link.menu-hover:hover { + background: var(--bg-accent) !important; + border-radius: 5px !important; + border: 1px solid transparent !important; /* Keep same border width */ +} + +/* Theme-specific hover colors */ +[data-theme="light"] .nav-link.menu-hover:hover, +body.light-theme .nav-link.menu-hover:hover { + background-color: #eaecef !important; +} + +[data-theme="dark"] .nav-link.menu-hover:hover, +body.dark-theme .nav-link.menu-hover:hover { + background-color: #30363d !important; +} + +/* Override Bootstrap's default nav-tabs border-radius */ +.nav-tabs .nav-link { + border-radius: 5px !important; + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs .nav-link:hover { + border-radius: 5px !important; + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs .nav-link:focus { + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs .nav-link:active { + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.nav-tabs { + border-top: none; + border-bottom: none; +} + +.menu-hover.active { + background: transparent; + border: none !important; +} + +/* Active menu indicator */ +.menu-active-indicator { + position: absolute; + left: 0; + right: 0; + bottom: -10px; + height: 2px; + background: rgb(253, 140, 115); /* Orange-red underline color */ + border-radius: 1px; +} diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx new file mode 100644 index 000000000..be57b934e --- /dev/null +++ b/ui/src/app/components/TitleBar.jsx @@ -0,0 +1,84 @@ +"use client"; +import { usePathname } from "next/navigation"; +import React from "react"; +import { GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; +import { useTheme } from './ThemeContext'; +import './TitleBar.css'; + +export default function TitleBar() { + const pathname = usePathname(); + const { isDark, toggleTheme } = useTheme(); + + // Always render the TitleBar structure to prevent layout shift + return ( + <> +
+
+ + + + + Safe-Settings Dashboard + + +
+
+ + + ); +} diff --git a/ui/src/app/dashboard/env/page.jsx b/ui/src/app/dashboard/env/page.jsx new file mode 100644 index 000000000..6022b0b96 --- /dev/null +++ b/ui/src/app/dashboard/env/page.jsx @@ -0,0 +1,23 @@ +import TitleBar from "../../components/TitleBar"; +import EnvVariables from "../../components/EnvVariables"; + +export default function EnvVarsPage() { + return ( +
+ +
+
+

App Environment Settings

+

+ These are the current settings used by the app. Some values are hidden or + masked for security. +

+
+
+
+ +
+
+
+ ); +} diff --git a/ui/src/app/dashboard/organizations/page.jsx b/ui/src/app/dashboard/organizations/page.jsx new file mode 100644 index 000000000..e5712bf0e --- /dev/null +++ b/ui/src/app/dashboard/organizations/page.jsx @@ -0,0 +1,24 @@ +import TitleBar from "../../components/TitleBar"; +import OrganizationsTable from "../../components/OrganizationsTable"; + +export default function OrganizationsPage() { + return ( +
+ +
+
+

+ Organizations +

+

+ List all the installations of the App and the last time Safe-settings configurations were synced. +

+
+ +
+ +
+
+
+ ); +} diff --git a/ui/src/app/dashboard/page.jsx b/ui/src/app/dashboard/page.jsx new file mode 100644 index 000000000..92cd1ac12 --- /dev/null +++ b/ui/src/app/dashboard/page.jsx @@ -0,0 +1,13 @@ +import TitleBar from "../components/TitleBar"; + +export default function DashboardPage() { + return ( +
+ +
+

Welcome to the Safe-Settings Hub Dashboard

+

Select a menu item above to get started.

+
+
+ ); +} diff --git a/ui/src/app/dashboard/safe-settings-hub/page.jsx b/ui/src/app/dashboard/safe-settings-hub/page.jsx new file mode 100644 index 000000000..a8bbe5810 --- /dev/null +++ b/ui/src/app/dashboard/safe-settings-hub/page.jsx @@ -0,0 +1,25 @@ +import TitleBar from "../../components/TitleBar"; +import MasterAdminContents from "../../components/Safe-settings-hubContent"; + +export default function SafeSettingsHubConfigPage() { + return ( +
+ +
+
+

+ Safe-Settings Hub Content +

+

+ Listing files maintained by the Safe-Settings Global configuration (all ORG's). + Files are retrieved from `/api/safe-settings-hub/content`. +

+
+
+
+ +
+
+
+ ); +} diff --git a/ui/src/app/dashboard/settings/page.jsx b/ui/src/app/dashboard/settings/page.jsx new file mode 100644 index 000000000..e63d76620 --- /dev/null +++ b/ui/src/app/dashboard/settings/page.jsx @@ -0,0 +1,13 @@ +import TitleBar from "../../components/TitleBar"; + +export default function SettingsPage() { + return ( +
+ +
+

Settings

+

Settings options will go here.

+
+
+ ); +} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css new file mode 100644 index 000000000..d4fb0de01 --- /dev/null +++ b/ui/src/app/globals.css @@ -0,0 +1,260 @@ +/* Global Theme Variables */ +/* Default theme variables (light theme as default) */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-accent: #eaecef; + --text-primary: #24292f; + --text-secondary: #6c757d; + --border-color: #dee2e6; +} + +/* Theme variables based on data-theme attribute */ +[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-accent: #eaecef; + --text-primary: #24292f; + --text-secondary: #6c757d; + --border-color: #dee2e6; +} + +[data-theme="dark"] { + --bg-primary: rgb(13,17,22); + --bg-secondary: #444444; + --bg-accent: #30363d; + --text-primary: #f0f6fc; + --text-secondary: #dddddd; + --border-color: #4d4d4d; +} + +/* Legacy support for body classes */ +body.light-theme { + --bg-primary: #ffffff; + --bg-secondary: #f8f9fa; + --bg-accent: #eaecef; + --text-primary: #24292f; + --text-secondary: #6c757d; + --border-color: #dee2e6; +} + +body.dark-theme { + --bg-primary: #161b22; + --bg-secondary: #444444; + --bg-accent: #30363d; + --text-primary: #f0f6fc; + --text-secondary: #e3e3e3; +} + +/* Global Theme Styles */ +/* Default body styling (light theme as default) */ +body { + background: var(--bg-primary, #fff) !important; + color: var(--text-primary, #24292f) !important; +} + +/* Theme-specific body styles using data-theme */ +[data-theme="light"] body, +body.light-theme { + background: #fff; + color: #24292f; +} + +[data-theme="dark"] body, +body.dark-theme { + background: rgb(45, 46, 47); + color: #f6f8fa; +} + +/* Global Main Element Theme */ +[data-theme="light"] main, +body.light-theme main { + background: #fff; + color: #24292f; +} + +[data-theme="dark"] main, +body.dark-theme main { + /* background: #161b22; */ + color: #f6f8fa; +} + +[data-theme="light"] .nav-link, +body.light-theme .nav-link { + color: #24292f; +} + +[data-theme="dark"] .nav-link, +body.dark-theme .nav-link { + color: #f6f8fa !important; +} + +/* title bar nav tabs */ +[data-theme="dark"] .nav-tabs, +body.dark-theme .nav-tabs { + background: #22272e; + border: none !important; +} + +[data-theme="light"] .nav-tabs, +body.light-theme .nav-tabs { + border: none !important; +} + +/* Apply theme variables to main element */ +main { + color: var(--text-primary) !important; + padding: 1rem; + border-radius: 12px; + margin-top: 1rem; +} + +/* Theme Utility Classes */ +.theme-bg-primary { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.theme-bg-secondary { + background-color: var(--bg-secondary); + color: var(--text-primary) !important; +} + +.theme-bg-accent { + background-color: var(--bg-accent); + color: var(--text-primary) !important; +} + +.theme-text-primary { + color: var(--text-primary) !important; +} + +.theme-text-secondary { + color: var(--text-secondary) !important; +} + +.theme-border { + border-color: var(--border-color) !important; /* override bootstrap .border */ +} + +.border.theme-border, .theme-border.border { + border-color: var(--border-color) !important; +} +/* Global Font Utility Classes */ +.dark-font { + color: #f6f8fa; +} + +.light-font { + color: #24292f; +} + +/* Organizations Table Styles */ +.ui-table .table { + background-color: var(--bg-primary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .table thead th { + background-color: var(--bg-secondary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; + font-weight: 600; +} + +.ui-table .table tbody td { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .table tbody tr:hover { + background-color: var(--bg-accent) !important; +} + +.ui-table .sortable-header:hover { + background-color: var(--bg-accent) !important; +} + +.ui-table .input-group-text { + background-color: var(--bg-secondary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .form-control { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.ui-table .form-control:focus { + background-color: var(--bg-primary); + color: var(--text-primary) !important; + border-color: var(--border-color) !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.ui-table .form-control::placeholder { + color: var(--text-secondary) !important; +} + +.list-group-item { + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} + +.list-group-item:hover { + background-color: var(--bg-accent) !important; +} + +.text-muted { + color: var(--text-secondary) !important; +} + +span.text-muted { + color: var(--text-secondary) !important; +} + +code { + color: var(--text-primary) !important; +} + +element { + color: var(--text-primary) !important; + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; +} + +.input-group-text { + background-color: var(--bg-secondary) !important; + border: 1px solid var(--border-color) !important; + color: var(--text-primary) !important; +} + +.table { + border-radius: 12px !important; + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border-color) !important; +} + +/* Env vars table dark mode override */ +[data-theme="dark"] .env-vars table, body.dark-theme .env-vars table { + background-color: var(--bg-primary) !important; +} +[data-theme="dark"] .env-vars thead th, body.dark-theme .env-vars thead th { + background-color: var(--bg-secondary) !important; +} + +th{ + font-weight: 600; + background-color: var(--bg-secondary) !important; +} + +tr td{ + background-color: var(--bg-primary) !important; + color: var(--text-primary) !important; + border-color: var(--border-color) !important; +} diff --git a/ui/src/app/hooks/useClientSafe.js b/ui/src/app/hooks/useClientSafe.js new file mode 100644 index 000000000..fabb0a510 --- /dev/null +++ b/ui/src/app/hooks/useClientSafe.js @@ -0,0 +1,45 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +/** + * Custom hook to handle client-side mounting + * Helps prevent hydration mismatches by ensuring client-specific code + * only runs after the component has mounted on the client + */ +export const useIsClient = () => { + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + return isClient; +}; + +/** + * Custom hook for client-safe date formatting + * Returns a consistent format between server and client to prevent hydration issues + */ +export const useClientSafeDate = () => { + const isClient = useIsClient(); + + const formatDate = (dateString) => { + if (!isClient) { + // Server-side: return a simple format that matches potential client output + return new Date(dateString).toISOString().split('T')[0]; + } + + // Client-side: full formatting + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + return { formatDate, isClient }; +}; diff --git a/ui/src/app/hooks/useHydrated.js b/ui/src/app/hooks/useHydrated.js new file mode 100644 index 000000000..7ab514a7f --- /dev/null +++ b/ui/src/app/hooks/useHydrated.js @@ -0,0 +1,18 @@ +'use client'; + +import { useState, useEffect } from 'react'; + +/** + * Hook that ensures consistent rendering between server and client + * Prevents hydration mismatches by showing a simple version first, + * then upgrading to the full version after hydration + */ +export const useHydrated = () => { + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return hydrated; +}; diff --git a/ui/src/app/layout.jsx b/ui/src/app/layout.jsx new file mode 100644 index 000000000..1104d2572 --- /dev/null +++ b/ui/src/app/layout.jsx @@ -0,0 +1,41 @@ +import './globals.css'; +import { ThemeProvider } from './components/ThemeContext'; + +// (Optional) Next.js App Router metadata API – safe to add +export const metadata = { + title: 'Safe Settings', + description: 'Safe Settings dashboard', + icons: { + icon: [ + { url: '/favicon.svg', type: 'image/svg+xml' }, + { url: '/favicon.ico', sizes: 'any' } + ], + apple: '/apple-touch-icon.png', + shortcut: '/favicon.ico' + } +}; + +export default function RootLayout({ children }) { + return ( + + + {/* Existing Bootstrap CSS */} + + {/* Favicon / icons */} + + + {/* Optional apple-touch-icon (provide file or remove link) */} + {/* */} + + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/not-found.jsx b/ui/src/app/not-found.jsx new file mode 100644 index 000000000..da79f865d --- /dev/null +++ b/ui/src/app/not-found.jsx @@ -0,0 +1,15 @@ +"use client"; +import TitleBar from "./components/TitleBar"; + +export default function NotFound() { + return ( +
+ +
+

404

+

Sorry, the page you are looking for does not exist.

+ Go to Dashboard +
+
+ ); +} diff --git a/ui/src/app/route.js b/ui/src/app/route.js new file mode 100644 index 000000000..af7296c8e --- /dev/null +++ b/ui/src/app/route.js @@ -0,0 +1,7 @@ +const { NextResponse } = require('next/server'); + +export async function GET() { + return NextResponse.json({ message: 'Hello world!' }); +} + +export const dynamic = 'force-static'; From 6457790edf6cf8e8e3b35ec2e15d041e9178afa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Sun, 31 Aug 2025 21:30:18 -0400 Subject: [PATCH 02/22] adjusting ui --- docs/hubSyncHandler/README.md | 22 + lib/utils.js | 0 ui/favico.ico | Bin 0 -> 3961 bytes ui/favico.png | Bin 0 -> 3961 bytes ui/favicon.ico | Bin 3872 -> 0 bytes ui/shield.png | Bin 3872 -> 3961 bytes ui/src/app/components/EnvVariables.jsx | 3 +- .../components/Safe-settings-hubContent.jsx | 518 +++++++----------- ui/src/app/components/TitleBar.jsx | 18 +- ui/src/app/globals.css | 33 +- 10 files changed, 248 insertions(+), 346 deletions(-) create mode 100644 lib/utils.js create mode 100644 ui/favico.ico create mode 100644 ui/favico.png delete mode 100644 ui/favicon.ico diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index 7ffd23ab3..4f4d06c80 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -32,3 +32,25 @@ Environment variables specific to the 'Sync-Feature' | `SAFE_SETTINGS_HUB_DIRECT_PUSH` | Use a PR or direct commit | false | +--- +--- + +## Hub Sync Scenarios + +1. Sync the `Hub Admin Repo` changes to a `Safe-Settings Admin Repo` in **the same ORG** as the Hub Admin Repo. + +2. Sync the `Hub Admin changes` to a `Safe-Settings Admin Repo` in **a different ORG**. + +3. _`'Global'`_ `Hub Admin Repo` updates. +Changes will `applied to all Organization` + + +```mermaid +flowchart LR +PR --> Hub +Hub --> ORG-A +Hub -..- ORG-B +Hub -..- ORG-C + + +``` \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/favico.ico b/ui/favico.ico new file mode 100644 index 0000000000000000000000000000000000000000..bf67019abb1784abbeba87886cf3e5769b53534c GIT binary patch literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ literal 0 HcmV?d00001 diff --git a/ui/favico.png b/ui/favico.png new file mode 100644 index 0000000000000000000000000000000000000000..bf67019abb1784abbeba87886cf3e5769b53534c GIT binary patch literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ literal 0 HcmV?d00001 diff --git a/ui/favicon.ico b/ui/favicon.ico deleted file mode 100644 index 3a78f556b0930171aacf738ccee96a5fc446928b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu diff --git a/ui/shield.png b/ui/shield.png index 3a78f556b0930171aacf738ccee96a5fc446928b..bf67019abb1784abbeba87886cf3e5769b53534c 100644 GIT binary patch literal 3961 zcmV-<4~FoGP)Px^Gf6~2RCr$Poq13cS02Z|Gh8AfhsdoQ9)KsJqNqV52926{#3XJmlNdF|%DuHY zH!&MsO+41xltvRb8l#)jBT-ir74O?1f<_P#L@v4HzG3UvI)oXzdwTkuKX_FstDNcA zuRrrX@BQBIy*4^nX&Jd}XQ19zKeDzUYg~HK-6ML~ow>XDy-aIsuFe`7WPqhd^KhLG z;X1u;fL^Ebq_*A8$wfha9@ESgo$d~xOVPF9lFn9t-Ws5)4v-HP+mG26Alv6!Lsb-@ zrLe>R)EcU60Of!s7eFcJ+K3yL;?~o@92RY03{NTczH(;>;Nc{fW*tKst4+HfRnu)Tsu3!$=((Ywl*j( zE=EyN5o#M6QC?k(qKYb%Ro9`VrA6YS%X7Kr0B!U-gnGH6pN~6wd31$Gmre{kds`dW z+UU{L*oX$^zb4eyH=?4p4!I@eNXah1x$D{Z{bmjto0{dJA)*$|01~K}5P!sm`J;Om zN7&oyVP~U14R}#!0P9)){)0dG~T4C-|**{kmpsijHcP9r-9T0#i z0|VgR*+I4kDk!VOw?{5wbHYUw+^s}IQ<4z$iF%8+pWr~lLiUTAq2l;iw^ud7`5$MHes&uJuY{G%0G<+R@4yRJHP}itj zZ&VZ@O#%}_{4jf1Z}_`*0ZkZcm8Rxqq~{l7&7PC^`SfKFR3(a{0A~#e#tTuQ=xlGR zMBB~QNr6sfO&yl*NWi+d(`LUf<_n4dBu!_A2jk@j`iW_iBG-KQ{Q+z^a29oqjk4A+ zJ3tD4AMW21^T&k2(N3G5$402CwjLk-7>}*T5>Zp%AR9zk0n!*ZD%cwfALtJsm(J}P zS>MzQ8U>Y{Na`By^)LsPA1|}86d(L|KlUZ2$`*Xf3NWynGu|9O2m^iHp)(1Hn_F72 z=I~_{R@S12TNgwIdSZZ&J386fNbU{QbqzRk{T2>hzK-j8#qf3Oihn&n6?&6Ni`sA| z?H1nHv>VrQ3MJo{Dv%8zsry1y2xdj}f(^@RBD=H_um5}!rPXzyfSSPd^>k&f8Q|+K z`e6vjKX)V`@n$XvBn8mjyE-B6!xz!V$D>`nG&`@2JB_8=6J$|YHh>{su2?f`G-r=G zmY#>t4kV(w-gpovr5_m-$c&pe3d@mNP>Rgr3Z|!)*VZE|I|rr3g>Z9ngr9p?g!*|R zBG?xLdwS!$V~N;w_yVYBwJpw$4p=vDI;IV0pTw%_>M?ogCI-6HA`3to(>{G95+eec zGm)%iSJF*vIB^xV4VJ}YVm8)0JJ?~#>{z@sA(FE_@k!V5mo?u@jt^x4I6S};%chOs z4V2Xe_>bAKn9l={GB}T~+J-~P>5@93WPp@CA$T+?@Rg(2bMblnrFIINYkg5}HnQ^W znEP5gh1b3`4bvn0^IjK!`8or!xmkQ}Q8K_jo-X)&Mig8f?RguUTULcdKOD#1ntDMn z=$UAI%*mxQ()j>QOfAClejd(FIJEdB1bOmLPDK^fnDO}*BxT+^gDs{=2KdzQKA1D2 zFOvY}DwMz5ot!CX3}g_sw6x|0jmAL3>~1C-O=IAvvt#k%_+hZob0;q-pSXPI5v<;Q zTnszr8c6^t3Hsrrp%~N4hv^_(eFID(fUdP@4IBW0=1uzL@(c zB767Xyd`Z5?74UibJzXCBxl7GNdPGzet%*FLcLu%?_JZ-i0{s(;g`e=G;pNe4Z5=Z z2;8%eS0#!FvPbzC!3oEC3s`=9wemP5W`QJ!nB{UFnR#*1d^Ct zgvEO=FhOzK!ltVzfQE}$w07&_i0@u~64CvFIN{Jl^8U6s{BSZ!eSnV*3C6t9Va)a; zTcNT3XnHO_*?Xb&V?JFa0lYV)x6^vdt50Iu@UV7kbHlwTGqd=ogZOg)8TA1^9oYvj zMul?jq){(B@W*Xt2`5BX=>YH7&_?So=g+|G(StdId|DgRx_ISp$J7TnXJiPT9~r`V zV*+?6H3v)fT@hjsz^*dBpkvQj1R9$j8Tf&bM3e3vI30 zNra~&N@s0*Z8jzk4P}CIuA&*hr-%1p_B{ADi+*P&k@Kji6_*8;0+2E|TV8t-V|gZ# zMpF2f!&oIUn3fdA%m@#{ywUyO$deRJFD%3I{fW3yR4%mB8GTO*KuUSh7&s`voAch3 z+FJbML2QUWE2@2x0LFy)VbRzDaN{{)P+V1uwMSEskR~qoCI#Tk5d*ONnMv?=b>@U~ zr>qk1ZHvRtr<2tO81C)`E|GO+n{Dri+=X%P#sODB80Kl4Gn96ORZ zq{LNd@A&m^cH{UTH!W@2y}(Jzi#A@)j`;iJVd(GU#(Q&G11~?2h}43+7B^BXz=&W! zte-yvp?=(}5Ngc1^lZ#szXMqX?N>@d8Y>E*AsDvR>+r$DLou!oB@?*kISOa~cjXq= z9Zf-v3K?l92YW`{XU9hHhA-5vUFWV~?z-(DQ!GiD8Q@w3kS!RVJ~#*q#`H70yGEIh zji;_*M`8wQEjVUjX%D1S)`Evd;NvGIn2kMY6N>hVziK;y2uzt-43KN>O5@+^$3`&+ zBh3}&7f-s0-N{+Vxm(4VooQQ-P=K@#+{?=yFN`0Cc`=dgwlvw=M&}8qF58Ujc}49% z7LYO%GzOYmyOO%^O&o%ULj7ACK?(9xH}X+YSC0U9XY}%LVGdAIAysHBJaHormvf4d zURch1ddeI?(sf{fH%5m9V9L-?3<>h(O`^6X_2a3_Sg?KvPms@_GUEkk$=bEArwi83 zet_BUHIyx9Q}Jt3CTOM}?B5OJ`ud`4o4g#&$OYv!$Sa*K8|TrzeK4lC5A0b&3QDwA)HPu1xitKCIrH9te!rLkq>yCZ zgh>4L(P+4K?gTrYtpq-pl)id-!%oYVm@HZw0W_y8UxWNzo$&7`#ux?Fv`nJ$Zo~1b zppADrGsBg=zLHmhrE$L_udLeWHf##hX3^IRXY!`&`Pxds^TZ|J;6}cfEVQxK3_v}- zvCgs|AyfzY9&N7A8qy2Tjp%DMBNtZIVC~T>sH|&XPHW8_6$(FBC!+&K)Z>0j&cqi9 z$uQkuWCKX!E}eaOZd`=Xupy+qH1mG8>j>8EKLf)-Bq7(e`_6r>b)#4Ax?pFc8!D<2 zlZ(8&cE+Ny{Sg)9-I`#aq-y-NY!`0CNv=u<7w@AktdUc_$fl-$x71P2s zi7c8Fg^wPOfup_fagTC;bTaGRZE?6&Xn9c2#)yf2qgVf$sMjG}DnuF!X;t#Z_(2Hv z=-OIcNmU(IA4)>v?INbf^z(6J(o2znUWmJTiet0WpF!R=tq0njh?THyomYRAc0i&UWj>FMsRVF95 z=sGTdbH)v2mTMFTnk(fsb$D~rZqXSR_WJd!E4D6v+h_qRm&%44KG@e6&qsx#hpUs( zTA9L@lB!x%);BU?4IM)NzJcjcB~g@pj5ABbjJ+Vj|PHpLug!Jm`5m0K4Np$m)v z%BeC{PMPY69^IMO9O%?4*U?^z87XJDHzi9pfb?3!>C`N&*?SU4uV%=0q?Z~<>XvO< ze7GGz`U|;LCW!Qo$Rh&-Fm+%6>lUco0BNgo^O1|p;mh>AVo*FPU%98Qp+43el-w%Q z&1j+s^LE24(f!f8yNl8D$O$m{Rz8zqzHs9kG!3xt}@2WXdm`S9nz8-W$W8RP)n7y1IVwg#Q?QZSqp$d)D;C#3zfA1C`4UR0JTtAeSpH$6%9~rmDL1jiMo;i zs;RP?04-5h5PiNvmda`X6r-*z0M$@g4S-_Ql?9+0Dk~3AoVv0BR9a;v z0ZLIdFdGNtKlYC{1120V<`k5&-2;R}p|psH_A)In-4Ipb{#}4^SR; z6$2=@%5nmfQ(Z*?%BixP0OeFyQGjx)EFVC5)m0pzTq?@}PziNa0Vs#casX69T~z?e zp|W&<%BZUvK&dKA0jQ+9ssfavvJ`+ys;eqMDJqKxsI0oG0~D*W7=UW1s|i3cDvJTA zhPs*n6r-}`0M$`fGk}(=YzaU$)zuWBB`RA2P)&6;1!#%N!U3wQuI2!Rsw@Pc7V26B zKp`p%0jPz#Rsm3m%H{!TqpsBeG^a8jKrPj^Du8?{^8wUSU8@4fr!qG{ZPm3pfLtoG z0kj5ntr8%c%4`6wL0zi^$fk08fYza|)dDnCxh+6zQrD^hwpF<;Kxk> zrLNTjG^m`Ln;V{=U(oumWEdK5Eq|)_e+w1y#H8|_f`UVZ#RaYZN`{DP6uspCU+W!7 T%E8+J00000NkvXXu0mjfmfmB+ literal 3872 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 TT(ZZ`R$f3)+eoWH!zt!}y#zCu diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx index 9773cc064..fd7eedd15 100644 --- a/ui/src/app/components/EnvVariables.jsx +++ b/ui/src/app/components/EnvVariables.jsx @@ -153,8 +153,7 @@ export default function EnvVariables() { )}
- {sorted.length} shown / {rows.length} total - {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} + {sorted.length} shown / {rows.length} total
); diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx index e4292aebc..3a40154c2 100644 --- a/ui/src/app/components/Safe-settings-hubContent.jsx +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -1,115 +1,57 @@ 'use client'; import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { SearchIcon, SyncIcon, FileIcon, FileDirectoryIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; +import { SearchIcon, FileIcon, FileDirectoryIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; import { useHydrated } from '../hooks/useHydrated'; -// Simple mock tree used when API returns 404 (dev convenience) +// Match the left index width and reuse for the search input +const LEFT_COL_WIDTH = 320; + const MOCK_TREE = { name: '.github', path: '.github', type: 'dir', - lastCommitAt: new Date(Date.now() - 3600 * 1000).toISOString(), + lastCommitAt: new Date().toISOString(), entries: [ - { - name: 'settings.yml', - path: '.github/settings.yml', - type: 'file', - lastCommitAt: new Date(Date.now() - 1800 * 1000).toISOString(), - lastCommitMessage: 'chore: mock settings', - lastCommitSha: 'mock123' - }, - { - name: 'CODEOWNERS', - path: '.github/CODEOWNERS', - type: 'file', - lastCommitAt: new Date(Date.now() - 7200 * 1000).toISOString(), - lastCommitMessage: 'feat: add mock CODEOWNERS', - lastCommitSha: 'mock456' - }, - { - name: 'workflows', - path: '.github/workflows', - type: 'dir', - lastCommitAt: new Date(Date.now() - 5400 * 1000).toISOString(), - entries: [ - { - name: 'ci.yml', - path: '.github/workflows/ci.yml', - type: 'file', - lastCommitAt: new Date(Date.now() - 2500 * 1000).toISOString(), - lastCommitMessage: 'ci: mock workflow', - lastCommitSha: 'mock789' - } - ] - } + { name: 'CODEOWNERS', path: '.github/CODEOWNERS', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'add CODEOWNERS' }, + { name: 'workflows', path: '.github/workflows', type: 'dir', lastCommitAt: new Date().toISOString(), entries: [ + { name: 'ci.yml', path: '.github/workflows/ci.yml', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'ci: add' } + ] } ] }; -export default function SafeSettingsHubContent() { +export default function SafeSettingsHubContent3b() { const hydrated = useHydrated(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [rootTree, setRootTree] = useState(null); // recursive tree response + const [rootTree, setRootTree] = useState(null); const [search, setSearch] = useState(''); - // Tree view removed; we now render a flattened table. + const [expandedPaths, setExpandedPaths] = useState(() => new Set()); + const [selectedPath, setSelectedPath] = useState(null); const [lastFetchedAt, setLastFetchedAt] = useState(null); - const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); // direction: 'asc' | 'desc' | null - const [expandedPaths, setExpandedPaths] = useState(() => new Set()); // which directory paths are expanded const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - // Always ask for recursive tree; server may limit depth - // Explicitly request content bodies (fetchContent=true is default but sent for clarity) - fetch('/api/safe-settings-hub/content?fetchContent=true') + fetch('/api/safe-settings-hub/content?fetchContent=true') .then(r => { - if (!r.ok) { - // Surface a clear error message instead of falling back to mock data - throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status}). Please try again later.`); - } + if (!r.ok) throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status})`); return r.json(); }) - .then(json => { - // On success set the returned tree - setRootTree(json); - setLastFetchedAt(new Date()); - }) - .catch(e => setError(e.message)) + .then(json => { setRootTree(json); setLastFetchedAt(new Date()); }) + .catch(() => setRootTree(MOCK_TREE)) .finally(() => setLoading(false)); }; useEffect(() => { fetchData(); }, [hydrated]); - // Flatten nodes for table display - const flattenNodes = useCallback((node, acc = [], depth = 0) => { - if (!node) return acc; - acc.push({ - name: node.name, - path: node.path, - type: node.type, - lastCommitAt: node.lastCommitAt, - lastCommitMessage: node.lastCommitMessage, - lastCommitSha: node.lastCommitSha, - depth - }); - if (node.type === 'dir' && Array.isArray(node.entries)) { - node.entries.forEach(child => flattenNodes(child, acc, depth + 1)); - } - return acc; - }, []); - const filterTree = useCallback((node) => { if (!node) return null; const term = search.toLowerCase(); - const matches = (n) => !term || n.name.toLowerCase().includes(term) || n.path.toLowerCase().includes(term); - if (node.type === 'file') { - return matches(node) ? node : null; - } + const matches = (n) => !term || (n.name && n.name.toLowerCase().includes(term)) || (n.path && n.path.toLowerCase().includes(term)); + if (node.type === 'file') return matches(node) ? node : null; if (node.type === 'dir') { const children = (node.entries || []).map(filterTree).filter(Boolean); - if (matches(node) || children.length > 0) { - return { ...node, entries: children }; - } + if (matches(node) || children.length) return { ...node, entries: children }; return null; } return null; @@ -117,25 +59,18 @@ export default function SafeSettingsHubContent() { const filteredTree = useMemo(() => filterTree(rootTree), [rootTree, filterTree]); - // If the root contains a top-level 'safe-settings' directory, treat that directory as the display root const displayTree = useMemo(() => { if (!filteredTree) return null; if (filteredTree.type === 'dir') { const nameMatch = (n) => n && n.type === 'dir' && n.name && n.name.toLowerCase().includes('safe-settings'); - // Prefer immediate child named 'safe-settings' const immediate = (filteredTree.entries || []).find(nameMatch); if (immediate) return immediate; - // Fallback: search descendants up to a small depth for 'safe-settings' const findDescendant = (node, depth = 0, maxDepth = 3) => { if (!node || node.type !== 'dir' || depth >= maxDepth) return null; - for (const child of node.entries || []) { - if (nameMatch(child)) return child; - } - for (const child of node.entries || []) { - if (child.type === 'dir') { - const found = findDescendant(child, depth + 1, maxDepth); - if (found) return found; - } + for (const child of node.entries || []) if (nameMatch(child)) return child; + for (const child of node.entries || []) if (child.type === 'dir') { + const found = findDescendant(child, depth + 1, maxDepth); + if (found) return found; } return null; }; @@ -145,268 +80,205 @@ export default function SafeSettingsHubContent() { return filteredTree; }, [filteredTree]); - // When a search filter is applied, auto-expand all ancestor directories that contain matches - useEffect(() => { - if (!search) return; // only on active filter - if (!displayTree || displayTree.type !== 'dir') return; - const dirsToExpand = new Set(); - const walk = (node) => { - if (!node || node.type !== 'dir') return false; - let containsMatch = false; + useEffect(() => { if (!displayTree) return; setSelectedPath(prev => prev || displayTree.path); }, [displayTree]); + + const findNodeByPath = useCallback((node, path) => { + if (!node) return null; + if (node.path === path) return node; + if (node.type === 'dir') { for (const child of node.entries || []) { - if (child.type === 'dir') { - if (walk(child)) { - containsMatch = true; - dirsToExpand.add(child.path); // expand child dir to show deeper matches - } - } else { - // Any file present means this dir should be opened if it passed filtering - containsMatch = true; - } + const found = findNodeByPath(child, path); + if (found) return found; } - return containsMatch; - }; - walk(displayTree); - // Also expand top-level dirs that survived filtering and have entries - (displayTree.entries || []).forEach(e => { if (e.type === 'dir') dirsToExpand.add(e.path); }); - setExpandedPaths(prev => { - const next = new Set(prev); - dirsToExpand.forEach(p => next.add(p)); - return next; - }); - }, [search, displayTree]); - - const flatList = useMemo(() => { - if (!displayTree) return []; - // If display root is a directory, list its children instead of the directory itself (hide intermediate root) - if (displayTree.type === 'dir') { - return displayTree.entries.flatMap(child => flattenNodes(child, [], 0)); } - return flattenNodes(displayTree, [], 0); - }, [displayTree, flattenNodes]); + return null; + }, []); - // Build hierarchical visible list honoring expandedPaths and optional sorting - const sortedFlatList = useMemo(() => { - if (!displayTree) return []; - // function to sort entries inside a directory when sorting enabled - const sortEntries = (entries) => { - if (!sortConfig.key || !sortConfig.direction) return entries; - const key = sortConfig.key; - return [...entries].sort((a, b) => { - let av; let bv; - switch (key) { - case 'name': av = a.name.toLowerCase(); bv = b.name.toLowerCase(); break; - case 'path': av = a.path.toLowerCase(); bv = b.path.toLowerCase(); break; - case 'lastCommitAt': av = a.lastCommitAt ? new Date(a.lastCommitAt).getTime() : 0; bv = b.lastCommitAt ? new Date(b.lastCommitAt).getTime() : 0; break; - default: av = a[key]; bv = b[key]; - } - if (av < bv) return sortConfig.direction === 'asc' ? -1 : 1; - if (av > bv) return sortConfig.direction === 'asc' ? 1 : -1; - return 0; - }); - }; - const out = []; - const process = (node, depth) => { - if (!node) return; - if (node.type === 'dir') { - const children = sortEntries(node.entries || []); - children.forEach(child => { - out.push({ - name: child.name, - path: child.path, - type: child.type, - lastCommitAt: child.lastCommitAt, - lastCommitMessage: child.lastCommitMessage, - lastCommitSha: child.lastCommitSha, - depth - }); - if (child.type === 'dir' && expandedPaths.has(child.path)) { - process(child, depth + 1); - } - }); - } else { - out.push({ - name: node.name, - path: node.path, - type: node.type, - lastCommitAt: node.lastCommitAt, - lastCommitMessage: node.lastCommitMessage, - lastCommitSha: node.lastCommitSha, - depth - }); - } - }; - // Start processing at displayTree (hiding any intermediate 'safe-settings' wrapper) - process(displayTree, 0); - return out; - }, [displayTree, sortConfig, expandedPaths]); + const selectedNode = useMemo(() => { + if (!displayTree || !selectedPath) return null; + return findNodeByPath(displayTree, selectedPath); + }, [displayTree, selectedPath, findNodeByPath]); - const cycleSort = (key) => { - setSortConfig(prev => { - if (prev.key === key) { - if (prev.direction === 'asc') return { key, direction: 'desc' }; - if (prev.direction === 'desc') return { key: null, direction: null }; // clear - } - return { key, direction: 'asc' }; - }); - }; + const toggleDir = (path) => { setExpandedPaths(prev => { const next = new Set(prev); if (next.has(path)) next.delete(path); else next.add(path); return next; }); }; - const renderSortIcon = (key) => { - if (sortConfig.key !== key) return ; - if (sortConfig.direction === 'asc') return ; - if (sortConfig.direction === 'desc') return ; - return ; + const formatTimeAgo = (iso) => { + if (!iso) return '—'; + const dt = new Date(iso); + if (Number.isNaN(dt.getTime())) return iso; + const diffSec = Math.floor((Date.now() - dt.getTime()) / 1000); + if (diffSec < 60) return 'just now'; + const diffMin = Math.floor(diffSec / 60); + if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`; + const diffH = Math.floor(diffMin / 60); + if (diffH < 24) return `${diffH} hour${diffH === 1 ? '' : 's'} ago`; + const diffD = Math.floor(diffH / 24); + if (diffD < 30) return `${diffD} day${diffD === 1 ? '' : 's'} ago`; + const diffM = Math.floor(diffD / 30); + if (diffM < 12) return diffM === 1 ? '1 month ago' : `${diffM} months ago`; + const diffY = Math.floor(diffD / 365); + if (diffY === 1) return 'last year'; + return `${diffY} years ago`; }; - const toggleDir = (path) => { - setExpandedPaths(prev => { - const next = new Set(prev); - if (next.has(path)) next.delete(path); else next.add(path); - return next; - }); - }; + const repoCount = useMemo(() => { + if (!rootTree) return '—'; + const rp = rootTree.reposProcessed || rootTree.repos || null; + if (!rp) return '—'; + if (Array.isArray(rp)) return rp.length; + if (typeof rp === 'object') return Object.keys(rp).length; + return '—'; + }, [rootTree]); - const collectAllDirPaths = useCallback((node, acc = []) => { - if (!node) return acc; - if (node.type === 'dir') { - if (node.path && node.path !== '.github') acc.push(node.path); // skip synthetic root label - (node.entries || []).forEach(child => collectAllDirPaths(child, acc)); + const renderTree = (node, depth = 0) => { + if (!node) return null; + if (node.type === 'file') { + const selected = selectedPath === node.path; + return ( +
setSelectedPath(node.path)}> + + {node.name} +
+ ); } - return acc; - }, []); - - const expandAll = () => { - if (!filteredTree) return; - const all = collectAllDirPaths(filteredTree, []); - setExpandedPaths(new Set(all)); + const expanded = expandedPaths.has(node.path); + const selected = selectedPath === node.path; + return ( +
+
+
{ toggleDir(node.path); setSelectedPath(node.path); }} className="d-inline-flex align-items-center"> + {expanded ? : } + + {node.name} +
+
+ {expanded && (node.entries || []).map(child => renderTree(child, depth + 1))} +
+ ); }; - const collapseAll = () => setExpandedPaths(new Set()); + const childrenForSelected = useMemo(() => { if (!selectedNode) return []; if (selectedNode.type === 'dir') return selectedNode.entries || []; return []; }, [selectedNode]); - const formatRelative = (iso) => { - if (!iso) return null; - const dt = new Date(iso); - let diffSec = Math.floor((Date.now() - dt.getTime()) / 1000); - if (diffSec < 0) diffSec = 0; - if (diffSec < 60) return '0m'; - const mTotal = Math.floor(diffSec / 60); - if (mTotal < 60) return `${mTotal}m`; - const hTotal = Math.floor(mTotal / 60); - if (hTotal < 24) { - const remM = mTotal % 60; - return remM ? `${hTotal}h ${remM}m` : `${hTotal}h`; - } - const dTotal = Math.floor(hTotal / 24); - const remH = hTotal % 24; - return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; - }; + const fileContent = useMemo(() => { if (!selectedNode || selectedNode.type !== 'file') return null; return selectedNode.content || selectedNode.body || selectedNode.text || selectedNode.preview || null; }, [selectedNode]); - // Table columns: Name (indented), Path, Type, Last update + const fileLines = useMemo(() => fileContent ? fileContent.split('\n') : [], [fileContent]); + const lineCount = fileLines.length; + const locCount = fileLines.filter(l => l.trim()).length; + const byteCount = useMemo(() => { + if (!fileContent) return 0; + try { return new TextEncoder().encode(fileContent).length; } catch (e) { return fileContent.length; } + }, [fileContent]); return ( - <>
-
-
-
- - - - setSearch(e.target.value)} /> -
-
-
-
- - - -
-
-
- - {/*
-
-
-
- - - - setSearchTerm(e.target.value)} - /> +
+
+
+
+ + setSearch(e.target.value)} />
+ {selectedNode &&
{selectedNode.path}
}
-
- - Showing {sortedData.length} of {data.length} organizations - +
+
+
+ {/* edit button intentionally removed */}
-
*/} - +
+ {loading &&
Loading…
} - {error && !loading &&
Error: {error}
} - {!loading && !error && !displayTree &&
No entries
} + {error &&
{error}
} + {!loading && !displayTree &&
No entries
} + + {!loading && displayTree && ( +
+
+ {/* left tree */} + {displayTree.type === 'dir' && displayTree.name && displayTree.name.toLowerCase().includes('safe-settings') + ? (displayTree.entries || []).map(child => renderTree(child, 0)) + : renderTree(displayTree) + } +
- {!loading && !error && displayTree && ( -
-
- - - - - - - - - - {sortedFlatList.map(node => { - const isDir = node.type === 'dir'; - const expanded = isDir && expandedPaths.has(node.path); - return ( - isDir && toggleDir(node.path)} style={isDir ? { cursor: 'pointer' } : undefined}> - - - - - ); - })} - -
cycleSort('name')} className="theme-text-primary user-select-none" style={{ width: '35%', cursor: 'pointer' }}>Name {renderSortIcon('name')} cycleSort('path')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Path {renderSortIcon('path')} cycleSort('lastCommitAt')} className="theme-text-primary user-select-none" style={{ width: '170px', cursor: 'pointer' }}>Last update {renderSortIcon('lastCommitAt')}
- - {isDir ? ( - expanded ? : +
+ {/* right content (dir/file view) */} + {selectedNode && selectedNode.type === 'dir' && ( +
+ {/* path rendered next to the filter at the top; removed empty toolbar to avoid extra top gap */} +
+ + + + + + + + + + {(childrenForSelected.length === 0) && ( + + )} + {childrenForSelected.map(child => ( + setSelectedPath(child.path)}> + + + + + ))} + +
NameCommit-MessageLast commit date
No entries
+ + {child.type === 'dir' ? : } + {child.name} + + {child.lastCommitMessage || '—'}{child.lastCommitAt ? formatTimeAgo(child.lastCommitAt) : '—'}
+
+
+ )} + + {selectedNode && selectedNode.type === 'file' && ( +
+ {/* path rendered next to the filter at the top; removed empty toolbar to avoid extra top gap */} +
+ {/* file header with border and rounded top, followed by a bordered code area with rounded bottom */} +
+
+
+
+ + +
+
+ +
+
+
+ {fileLines.map((_, i) =>
{i + 1}
)} +
+
+ {fileLines.length === 0 ? ( +
No content available
) : ( - + fileLines.map((ln, i) =>
{ln || ' '}
) )} - {isDir && } - {node.name} - -
{node.path} - {node.lastCommitAt ? formatRelative(node.lastCommitAt) : '—'} -
+
+
+
+
+
+
+ )} + + {!selectedNode && ( +
Select a folder or file from the left to view contents.
+ )} +
)} + + {/* footer (items shown) removed */} - {!loading && !error && ( -
- {sortedFlatList.length} items shown - {lastFetchedAt && Fetched {lastFetchedAt.toLocaleTimeString()}} -
- )} - {/* Removed inner bordered wrapper styles so only outer page container shows a border */} - ); } diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index be57b934e..818f4a356 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -1,7 +1,7 @@ "use client"; import { usePathname } from "next/navigation"; import React from "react"; -import { GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; +import { GlobeIcon, GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; import { useTheme } from './ThemeContext'; import './TitleBar.css'; @@ -37,13 +37,13 @@ export default function TitleBar() {
  • - + - Organizations - {pathname === "/dashboard/organizations" && ( + Safe-Settings Hub + {pathname === "/dashboard/safe-settings-hub" && ( )} @@ -51,13 +51,13 @@ export default function TitleBar() {
  • - + - Safe-Settings Hub - {pathname === "/dashboard/safe-settings-hub" && ( + Organizations + {pathname === "/dashboard/organizations" && ( )} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index d4fb0de01..46564ec39 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -20,7 +20,7 @@ } [data-theme="dark"] { - --bg-primary: rgb(13,17,22); + --bg-primary: rgb(13, 17, 22); --bg-secondary: #444444; --bg-accent: #30363d; --text-primary: #f0f6fc; @@ -86,7 +86,8 @@ body.light-theme .nav-link { [data-theme="dark"] .nav-link, body.dark-theme .nav-link { - color: #f6f8fa !important; + /* color: #f6f8fa !important; */ + color: #6c757d !important; } /* title bar nav tabs */ @@ -104,9 +105,9 @@ body.light-theme .nav-tabs { /* Apply theme variables to main element */ main { color: var(--text-primary) !important; - padding: 1rem; + /* padding: 1rem; */ border-radius: 12px; - margin-top: 1rem; + /* margin-top: 1rem; */ } /* Theme Utility Classes */ @@ -135,12 +136,15 @@ main { } .theme-border { - border-color: var(--border-color) !important; /* override bootstrap .border */ + border-color: var(--border-color) !important; + /* override bootstrap .border */ } -.border.theme-border, .theme-border.border { +.border.theme-border, +.theme-border.border { border-color: var(--border-color) !important; } + /* Global Font Utility Classes */ .dark-font { color: #f6f8fa; @@ -209,7 +213,7 @@ main { background-color: var(--bg-accent) !important; } -.text-muted { +.text-muted { color: var(--text-secondary) !important; } @@ -225,6 +229,8 @@ element { color: var(--text-primary) !important; background-color: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; + margin-left: 10px !important; + gap: 1.5rem !important; } .input-group-text { @@ -241,20 +247,23 @@ element { } /* Env vars table dark mode override */ -[data-theme="dark"] .env-vars table, body.dark-theme .env-vars table { +[data-theme="dark"] .env-vars table, +body.dark-theme .env-vars table { background-color: var(--bg-primary) !important; } -[data-theme="dark"] .env-vars thead th, body.dark-theme .env-vars thead th { + +[data-theme="dark"] .env-vars thead th, +body.dark-theme .env-vars thead th { background-color: var(--bg-secondary) !important; } -th{ +th { font-weight: 600; background-color: var(--bg-secondary) !important; } -tr td{ +tr td { background-color: var(--bg-primary) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; -} +} \ No newline at end of file From 498bbbd13d93c0dc576225c890ac81308a8c33c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Tue, 9 Sep 2025 23:04:26 -0400 Subject: [PATCH 03/22] hub improvements --- docs/hubSyncHandler/README.md | 41 +- index.js | 98 +- lib/hubSyncHandler.js | 684 +++++++++-- lib/installationCache.js | 78 +- lib/mergeDeep.js | 4 +- lib/plugins/archive.js | 95 +- lib/plugins/branches.js | 8 +- lib/plugins/environments.js | 2 +- lib/plugins/overrides.js | 16 +- lib/plugins/rulesets.js | 10 +- lib/routes.js | 340 ++++-- lib/settings.js | 18 +- package-lock.json | 1054 ++++++++++++++++- package.json | 4 +- test/unit/index.test.js | 10 +- test/unit/lib/hubSyncHandler.test.js | 106 ++ test/unit/lib/plugins/archive.test.js | 84 +- test/unit/lib/plugins/environments.test.js | 58 +- test/unit/lib/plugins/rulesets.test.js | 30 +- test/unit/lib/routes.test.js | 140 +++ test/unit/lib/settings.test.js | 129 +- ui/README.md | 29 + ui/favico.ico | Bin 3961 -> 0 bytes ui/favico.png | Bin 3961 -> 0 bytes ui/favicon.svg | 3 + ui/next.config.js | 9 + ui/package-lock.json | 100 +- ui/package.json | 8 +- ui/public/favicon.svg | 2 +- ui/public/shield.png | Bin 24289 -> 0 bytes ui/shield.png | Bin 3961 -> 4883 bytes ui/shield.svg | 3 + ui/src/app/components/EnvVariables.jsx | 20 +- ui/src/app/components/HubOrgGraph.jsx | 140 +++ ui/src/app/components/OrganizationsTable.jsx | 508 ++++++-- .../components/Safe-settings-hubContent.jsx | 28 +- ui/src/app/components/TitleBar.css | 6 +- ui/src/app/components/TitleBar.jsx | 88 +- ui/src/app/dashboard/help/page.jsx | 35 + ui/src/app/dashboard/organizations/page.jsx | 2 +- ui/src/app/dashboard/page.jsx | 12 + .../app/dashboard/safe-settings-hub/page.jsx | 2 +- ui/src/app/dashboard/settings/page.jsx | 13 - ui/src/app/globals.css | 24 +- ui/src/app/route.js | 7 - 45 files changed, 3334 insertions(+), 714 deletions(-) create mode 100644 test/unit/lib/hubSyncHandler.test.js create mode 100644 test/unit/lib/routes.test.js delete mode 100644 ui/favico.ico delete mode 100644 ui/favico.png create mode 100644 ui/favicon.svg delete mode 100644 ui/public/shield.png create mode 100644 ui/shield.svg create mode 100644 ui/src/app/components/HubOrgGraph.jsx create mode 100644 ui/src/app/dashboard/help/page.jsx delete mode 100644 ui/src/app/dashboard/settings/page.jsx delete mode 100644 ui/src/app/route.js diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index 4f4d06c80..df08f0f6f 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -44,13 +44,40 @@ Environment variables specific to the 'Sync-Feature' 3. _`'Global'`_ `Hub Admin Repo` updates. Changes will `applied to all Organization` +--- -```mermaid -flowchart LR -PR --> Hub -Hub --> ORG-A -Hub -..- ORG-B -Hub -..- ORG-C +## Safe-Settings Hub API endpoints + +### API Endpoints + +The following table summarizes the Safe Settings API endpoints: + +| Endpoint | Method | Purpose | Example Usage | +|------------------------------------------|--------|------------------------------------------------------|---------------| +| `/api/safe-settings/installation` | GET | Organization installation, repo, and sync status | Fetch org status | +| `/api/safe-settings/hub/content` | GET | List hub repo files/directories | List hub files | +| `/api/safe-settings/hub/content/*` | GET | Fetch specific file or directory from hub repo | Get file content | +| `/api/safe-settings/hub/import` | POST | Import settings from orgs into the hub | Import org settings | +| `/api/safe-settings/env` | GET | App environment/config variables | Get env vars | + +**Examples:** +- Fetch org installation status: + ```http + GET /api/safe-settings/installation + ``` +- Import settings from orgs: + ```http + POST /api/safe-settings/hub/import + Body: { "orgs": ["org1", "org2"] } + ``` +- List hub repo files: + ```http + GET /api/safe-settings/hub/content?ref=main&recursive=true + ``` +- Get environment variables: + ```http + GET /api/safe-settings/env + ``` +--- -``` \ No newline at end of file diff --git a/index.js b/index.js index e4c809f66..265965183 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,86 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => // Initialize installation cache (env-controlled prefetch) initCache(robot) - async function renameSync(nop, context, repo = context.repo(), rename, ref) { + async function syncAllSettings (nop, context, repo = context.repo(), ref) { + try { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context, ref) + const runtimeConfig = await configManager.loadGlobalSettingsYaml() + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + if (ref) { + return Settings.syncAll(nop, context, repo, config, ref) + } else { + return Settings.syncAll(nop, context, repo, config) + } + } catch (e) { + if (nop) { + let filename = env.SETTINGS_FILE_PATH + if (!deploymentConfig) { + filename = env.DEPLOYMENT_CONFIG_FILE_PATH + deploymentConfig = {} + } + const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + } else { + throw e + } + } + } + + async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { + try { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context, ref) + const runtimeConfig = await configManager.loadGlobalSettingsYaml() + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref) + } catch (e) { + if (nop) { + let filename = env.SETTINGS_FILE_PATH + if (!deploymentConfig) { + filename = env.DEPLOYMENT_CONFIG_FILE_PATH + deploymentConfig = {} + } + const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + } else { + throw e + } + } + } + + async function syncSettings (nop, context, repo = context.repo(), ref) { + try { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context, ref) + const runtimeConfig = await configManager.loadGlobalSettingsYaml() + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + return Settings.sync(nop, context, repo, config, ref) + } catch (e) { + if (nop) { + let filename = env.SETTINGS_FILE_PATH + if (!deploymentConfig) { + filename = env.DEPLOYMENT_CONFIG_FILE_PATH + deploymentConfig = {} + } + const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) + Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + } else { + throw e + } + } + } + + async function renameSync (nop, context, repo = context.repo(), rename, ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -46,14 +125,13 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } } - /** * Loads the deployment config file from file system * Do this once when the app starts and then return the cached value * * @return The parsed YAML file */ - async function loadYamlFileSystem() { + async function loadYamlFileSystem () { if (deploymentConfig === undefined) { const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE_PATH if (fs.existsSync(deploymentConfigPath)) { @@ -65,7 +143,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return deploymentConfig } - function getAllChangedSubOrgConfigs(payload) { + function getAllChangedSubOrgConfigs (payload) { const pattern = Settings.SUB_ORG_PATTERN const getMatchingFiles = (commits, type) => @@ -82,7 +160,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getAllChangedRepoConfigs(payload, owner) { + function getAllChangedRepoConfigs (payload, owner) { const pattern = Settings.REPO_PATTERN const getMatchingFiles = (commits, type) => @@ -99,7 +177,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedRepoConfigName(files, owner) { + function getChangedRepoConfigName (files, owner) { const pattern = Settings.REPO_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -110,7 +188,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => })) } - function getChangedSubOrgConfigName(files) { + function getChangedSubOrgConfigName (files) { const pattern = Settings.SUB_ORG_PATTERN const modifiedFiles = files.filter((s) => pattern.test(s)) @@ -120,7 +198,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => path: modifiedFile })) } - async function createCheckRun(context, pull_request, head_sha, head_branch) { + async function createCheckRun (context, pull_request, head_sha, head_branch) { const { payload } = context // robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`) const res = await context.octokit.checks.create({ @@ -132,7 +210,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => robot.log.debug(JSON.stringify(res, null)) } - async function info() { + async function info () { const github = await robot.auth() const installations = await github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: 100 }) @@ -147,7 +225,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncInstallation(nop = false) { + async function syncInstallation (nop = false) { robot.log.trace('Fetching installations') const github = await robot.auth() diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js index c34de6d66..8ef694afe 100644 --- a/lib/hubSyncHandler.js +++ b/lib/hubSyncHandler.js @@ -1,6 +1,21 @@ const env = require('./env') const { getInstallations } = require('./installationCache') +/** + * Get authenticated octokit client for an org installation + * @param {import('probot').Probot} robot + * @param {string} orgName + * @returns {Promise} Authenticated client or null + */ +async function getOrgInstallation (robot, orgName) { + const installs = await getInstallations(robot) + const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === orgName.toLowerCase()) + if (!install) { + return null + } + return await robot.auth(install.id) +} + /** * Sync changed safe-settings organization files from the master admin PR * into the target organization's admin repository. @@ -10,178 +25,176 @@ const { getInstallations } = require('./installationCache') * @param {string} destRepo Destination repo name inside orgName (e.g. admin repo) * @param {string} destinationFolder Base folder in destination repo where content lives (e.g. .github or .github/safe-settings) */ -async function syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder) { +async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationFolder) { try { - robot.log.info(`Syncing safe settings for organization: ${orgName}`); + robot.log.info(`Syncing safe settings for organization: ${orgName}`) - robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`); - const pr = context.payload.pull_request; + robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`) + const pr = context.payload.pull_request if (!pr) { - robot.log.warn('No pull_request payload found; aborting sync'); - return; + robot.log.warn('No pull_request payload found; aborting sync') + return } - const { owner: srcOwner, repo: srcRepo } = context.repo(); - const pull_number = pr.number; + const { owner: srcOwner, repo: srcRepo } = context.repo() + const pull_number = pr.number // Source base path where org folders live inside master admin repo // 'safe-settings' is the standard sub-folder path - const configRoot = env.CONFIG_PATH || '.github/'; - const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, ''); - robot.log.info(`DEBUG: sourceBase='${sourceBase}'`); + const configRoot = env.CONFIG_PATH || '.github/' + const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, '') + robot.log.info(`DEBUG: sourceBase='${sourceBase}'`) // Debug info: log env and computed paths - robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`); + robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`) // List changed files in PR const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } - ); + ) - robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`); - if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); + robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`) + if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) // Dump file objects for debugging filename issues if (files.length) { try { - robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`); - robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`); + robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`) + robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`) } catch (e) { - robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`); + robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`) } files.forEach((f, i) => { try { - robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`); - robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`); + robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`) + robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`) } catch (e) { - robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`); + robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`) } - }); + }) } - const orgPrefix = `${sourceBase}/${orgName}/`; - robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`); - robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`); - const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)); - robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`); + const orgPrefix = `${sourceBase}/${orgName}/` + robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) + robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`) + const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)) + robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`) if (!relevant.length) { - robot.log.info(`No files for org ${orgName} in PR #${pull_number}`); + robot.log.info(`No files for org ${orgName} in PR #${pull_number}`) // Detailed per-file checks to help debug matching files.forEach(f => { - const exact = f.filename === `${sourceBase}/${orgName}`; - const pref = f.filename.startsWith(orgPrefix); - robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`); - }); + const exact = f.filename === `${sourceBase}/${orgName}` + const pref = f.filename.startsWith(orgPrefix) + robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`) + }) // Also show alternate check using CONFIG_PATH + '/organizations' - const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations`; - const altPrefix = `${altBase}/${orgName}/`; + const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations` + const altPrefix = `${altBase}/${orgName}/` files.forEach(f => { - const exactAlt = f.filename === `${altBase}/${orgName}`; - const prefAlt = f.filename.startsWith(altPrefix); - robot.log.info(`ALT CHECK: file='${f.filename}' exactAlt=${exactAlt} prefAlt=${prefAlt}`); - }); - return; + const exactAlt = f.filename === `${altBase}/${orgName}` + const prefAlt = f.filename.startsWith(altPrefix) + robot.log.info(`ALT CHECK: file='${f.filename}' exactAlt=${exactAlt} prefAlt=${prefAlt}`) + }) + return } // Destination info - const destOwner = orgName; + const destOwner = orgName // ensure destBase uses the configured CONFIG_PATH (fallback to '.github') and normalize trailing slash - const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, ''); - const destBaseBranch = 'main'; - const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1'); + const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, '') + const destBaseBranch = 'main' + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') - // Find installation for destination org to auth - const installs = await getInstallations(robot) - const install = installs.find(i => i.account && i.account.type === 'Organization' && i.account.login.toLowerCase() === destOwner.toLowerCase()); - if (!install) { - robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`); - return; + // Find installation for destination org to auth (reusable helper) + const githubDest = await getOrgInstallation(robot, destOwner) + if (!githubDest) { + robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`) + return } - const githubDest = await robot.auth(install.id); - robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`); + robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`) // Create branch if not direct push - const timestamp = Date.now(); - const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}`; + const timestamp = Date.now() + const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}` if (!directPush) { try { - const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }); - const baseSha = baseRef.data.object.sha; - await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }); - robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`); + const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }) + const baseSha = baseRef.data.object.sha + await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) + robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`) } catch (err) { if (err.status === 422) { - robot.log.warn(`Branch ${branchName} already exists, continuing`); + robot.log.warn(`Branch ${branchName} already exists, continuing`) } else { - throw err; + throw err } } } for (const f of relevant) { - let relative; + let relative if (f.filename === `${sourceBase}/${orgName}`) { // top directory marker encountered (unlikely in changed files list) - skip - continue; + continue } else { - relative = f.filename.slice(orgPrefix.length); + relative = f.filename.slice(orgPrefix.length) } // place only the changed file under the configured CONFIG_PATH (e.g. '.github/') - const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/'); + const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/') try { - const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }); - const data = srcContentResp.data; + const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }) + const data = srcContentResp.data if (Array.isArray(data)) { // Skip directories; individual files will appear separately in changed files list - continue; + continue } - const fileContent = Buffer.from(data.content, data.encoding).toString('utf8'); - const encoded = Buffer.from(fileContent, 'utf8').toString('base64'); + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') + const encoded = Buffer.from(fileContent, 'utf8').toString('base64') // Check existing file for sha - let existingSha = undefined; + let existingSha try { - const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }); - if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha; + const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }) + if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha } catch (getErr) { - if (getErr.status !== 404) throw getErr; // ignore missing + if (getErr.status !== 404) throw getErr // ignore missing } await githubDest.rest.repos.createOrUpdateFileContents({ owner: destOwner, repo: destRepo, - path: destPath, + path: destPath, message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, content: encoded, branch: branchName, sha: existingSha, committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } - }); - robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`); + }) + robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`) } catch (fileErr) { - robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`); - throw fileErr; + robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`) + throw fileErr } } if (!directPush) { try { - const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`; - const prBody = `Automated sync of safe-settings for ${orgName} from ${srcOwner}/${srcRepo} PR #${pull_number}.`; - const created = await githubDest.rest.pulls.create({ owner: destOwner, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }); - robot.log.info(`Created PR ${created.data.html_url} in ${destOwner}/${destRepo}`); + const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` + const prBody = `Automated sync of safe-settings for ${orgName} from ${srcOwner}/${srcRepo} PR #${pull_number}.` + const created = await githubDest.rest.pulls.create({ owner: destOwner, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }) + robot.log.info(`Created PR ${created.data.html_url} in ${destOwner}/${destRepo}`) } catch (prErr) { - robot.log.error(`Failed to create PR in ${destOwner}/${destRepo}: ${prErr.message}`); - throw prErr; + robot.log.error(`Failed to create PR in ${destOwner}/${destRepo}: ${prErr.message}`) + throw prErr } } else { - robot.log.info(`Changes pushed directly to ${destOwner}/${destRepo}@${destBaseBranch}`); + robot.log.info(`Changes pushed directly to ${destOwner}/${destRepo}@${destBaseBranch}`) } } catch (err) { - robot.log.error(`syncSafeSettingConfig error for org ${orgName}: ${err.message}`); + robot.log.error(`syncSafeSettingConfig error for org ${orgName}: ${err.message}`) } } @@ -191,68 +204,493 @@ async function syncSafeSettingConfig(robot, context, orgName, destRepo, destinat * @param {import('probot').Probot} robot * @param {import('probot').Context} context */ -async function hubSyncHandler(robot, context) { - const { payload } = context; - const { repository, pull_request } = payload || {}; - robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`); +async function hubSyncHandler (robot, context) { + const { payload } = context + const { repository, pull_request } = payload || {} + robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`) try { // Ensure the event is from the configured Safe-Settings Hub repo/org - const isMasterRepo = repository && repository.name === env.SAFE_SETTINGS_HUB_REPO; - const isMasterOrg = repository && repository.owner && repository.owner.login === env.SAFE_SETTINGS_HUB_ORG; + const isMasterRepo = repository && repository.name === env.SAFE_SETTINGS_HUB_REPO + const isMasterOrg = repository && repository.owner && repository.owner.login === env.SAFE_SETTINGS_HUB_ORG if (!(isMasterRepo && isMasterOrg)) { - robot.log.info(`Pull request.closed is not from master admin repo/org (${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO}), ignoring`); - return; + robot.log.info(`Pull request.closed is not from master admin repo/org (${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO}), ignoring`) + return } - robot.log.info(`Pull request closed on Safe-Settings Hub: (${repository.full_name})`); + robot.log.info(`Pull request closed on Safe-Settings Hub: (${repository.full_name})`) // Get the PR details - const pr = pull_request; - const { owner, repo } = context.repo(); - const pull_number = pr.number; - const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`; + const pr = pull_request + const { owner, repo } = context.repo() + const pull_number = pr.number // Paginate through all files changed in the PR const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 } - ); + ) - robot.log.info(`Files changed in PR #${pull_number}: ${files.map(f => f.filename).join(', ')}`); + robot.log.info(`Files changed in PR #${pull_number}: ${files.map(f => f.filename).join(', ')}`) - // Normalize baseSettingsPath (remove trailing slash if any) - const normalizedBase = baseSettingsPath.replace(/\/$/, ''); - robot.log.debug(`Normalized base path: ${normalizedBase}`); + // Routing logic: check for 'globals' or 'organizations' folder changes + const globalsChanged = files.some(f => /\/globals\//.test(f.filename)) + const orgsChanged = files.some(f => /\/organizations\//.test(f.filename)) - // Escape string for use in RegExp - const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (globalsChanged) { + robot.log.info('Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).') + await module.exports.syncHubGlobalsUpdate(robot, context, files) + } + + if (orgsChanged) { + robot.log.info('Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...).') + // Only sync updates in organization subfolders, not files directly in organizations folder + const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations` + const normalizedBase = baseSettingsPath.replace(/\/$/, '') + const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Only match files in org subfolders: .../organizations//... + const orgSubfolderPattern = new RegExp(`^${escapeRegex(normalizedBase)}/([^/]+)/.+`) + const orgNamesSet = new Set() + files.forEach(f => { + const m = f.filename.match(orgSubfolderPattern) + if (m && m[1]) { + orgNamesSet.add(m[1]) + } + }) + const orgNames = Array.from(orgNamesSet) + robot.log.info(`Orgs updated in PR #${pull_number}: ${orgNames.join(', ')}`) + for (const orgName of orgNames) { + const destRepo = env.ADMIN_REPO + const destinationFolder = env.CONFIG_PATH || '.github' + await module.exports.syncHubOrgUpdate(robot, context, orgName, destRepo, destinationFolder) + } + } + } catch (err) { + robot.log.error(`Failed to sync safe settings: ${err && err.message ? err.message : err}`) + } +} - // Build a RegExp that captures the first path segment after the base path - const basePattern = new RegExp(`^${escapeRegex(normalizedBase)}/([^/]+)(?:/|$)`); - robot.log.debug(`Base pattern for org matching: ${basePattern}`); +/** + * Handle updates in the globals folder and sync to destinations defined in manifest.yml rules + * @param {import('probot').Probot} robot + * @param {import('probot').Context} context + * @param {Array} files - Array of changed file objects from PR + */ +async function syncHubGlobalsUpdate (robot, context, files) { + robot.log.info('syncHubGlobalsUpdate: Processing globals folder changes.') + // Step 1: Load manifest.yml rules from the hub repo + const yaml = require('js-yaml') + const util = require('util') + const manifestPath = `${env.CONFIG_PATH}/${env.SAFE_SETTINGS_HUB_PATH}/globals/manifest.yml` + let manifest + try { + // Get manifest.yml from the hub repo (default branch: main) + const resp = await context.octokit.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: manifestPath, + ref: 'main' + }) - // Collect unique org names - const orgNamesSet = new Set(); - files.forEach(f => { - const m = f.filename.match(basePattern); - if (m && m[1]) { - orgNamesSet.add(m[1]); + const manifestContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') + manifest = yaml.load(manifestContent) + robot.log.info('Loaded manifest.yml rules from hub repo:' + JSON.stringify(manifest, null, 2)) + } catch (err) { + robot.log.error('Failed to load manifest.yml from hub repo:' + err.message) + return + } + // Step 2: Determine which update to sync where + // Find changed files in the globals folder + const changedGlobals = files.filter(f => /\/globals\//.test(f.filename)) + if (!changedGlobals.length) { + robot.log.info('No changed files in globals folder.') + return + } + + // For each changed file, match against manifest rules + for (const fileObj of changedGlobals) { + const fileName = fileObj.filename.split('/').pop() + // Prevent manifest.yml from being synced to organizations + if (fileName === 'manifest.yml') { + robot.log.info(`Skipping sync for manifest.yml (should only exist in hub)`) + continue + } + robot.log.info(`Evaluating globals file: ${fileObj.filename}`) + for (const rule of manifest.rules || []) { + // Check if file matches rule.files (glob match, simple * and exact) + const matchesFile = (rule.files || []).some(pattern => { + if (pattern === fileName) return true + if (pattern.startsWith('*') && fileName.endsWith(pattern.slice(1))) return true + if (pattern.endsWith('*') && fileName.startsWith(pattern.slice(0, -1))) return true + if (pattern.includes('*')) { + // Simple contains match for * + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') + return regex.test(fileName) + } + return false + }) + if (!matchesFile) continue + + // Determine target orgs + const targets = rule.targets || [] + robot.log.info(`Rule '${rule.name}' matches file '${fileName}'. Targets: ${targets.join(', ')}`) + // Step 3: handle mergeStrategy and actual sync + const mergeStrategy = rule.mergeStrategy || 'merge' + for (const orgPattern of targets) { + // For demo, treat '*' as all orgs, otherwise match orgs by pattern + let orgsToSync = [] + if (orgPattern === '*') { + // Get all org installations + const installs = await getInstallations(robot) + orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization').map(i => i.account.login) + } else if (orgPattern.endsWith('*')) { + // Prefix match + const prefix = orgPattern.slice(0, -1) + const installs = await getInstallations(robot) + orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization' && i.account.login.startsWith(prefix)).map(i => i.account.login) + } else { + orgsToSync = [orgPattern] + } + for (const orgName of orgsToSync) { + robot.log.info(`Preparing to sync file '${fileName}' to org '${orgName}' with mergeStrategy='${mergeStrategy}'`) + // Check if org has a safe-settings config repo (repo exists) + const destRepo = env.ADMIN_REPO + // use the octokit client authenticated for the hub installation + const githubDest = await getOrgInstallation(robot, orgName) + if (!githubDest) { + robot.log.info(`Skipping org ${orgName}: no installation found.`) + continue + } + let repoExists = false + try { + await githubDest.repos.get({ owner: orgName, repo: destRepo }) + repoExists = true + } catch (err) { + if (err.status === 404) { + robot.log.info(`Skipping org ${orgName}: config repo '${destRepo}' does not exist.`) + continue + } else { + throw err + } + } + if (!repoExists) continue + // Check if file exists in org's repo + const destPath = `${env.CONFIG_PATH}/${fileName}` + let exists = false + let existingContent = null + try { + robot.log.info(`Checking existence of ${destPath} in ${orgName}/${destRepo}`) + const resp = await githubDest.repos.getContent({ + owner: orgName, + repo: destRepo, + path: destPath, + ref: 'main' + }) + if (!Array.isArray(resp.data)) { + robot.log.info(`Found ${destPath} in ${orgName}/${destRepo}`) + exists = true + existingContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') + } + } catch (err) { + if (err.status === 404) { + robot.log.info(`File ${destPath} not found in ${orgName}/${destRepo} (this is fine for both merge strategies)`) + exists = false + existingContent = null + } else { + robot.log.info(`Error checking ${destPath} in ${orgName}/${destRepo}: ${err.message}`) + throw err + } + } + // Merge strategy logic + if (mergeStrategy === 'merge' && exists) { + robot.log.info(`Skipping sync of ${fileName} to ${orgName} (already exists & mergeStrategy=${mergeStrategy})`) + continue + } + // For overwrite or merge with no existing file, sync + robot.log.info(`Syncing ${fileName} to ${orgName} (mergeStrategy=${mergeStrategy})`) + // Actual sync logic: create or update file in org repo + try { + // Get source file content from hub repo (use PR head SHA if available, else main) + let srcContentResp + const pr = context.payload && context.payload.pull_request + const srcRef = pr && pr.head && pr.head.sha ? pr.head.sha : 'main' + srcContentResp = await context.octokit.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fileObj.filename, + ref: srcRef + }) + const data = srcContentResp.data + if (Array.isArray(data)) { + robot.log.info(`Skipping directory ${fileObj.filename}`) + continue + } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') + const encoded = Buffer.from(fileContent, 'utf8').toString('base64') + + // Prepare commit message and branch + const destBaseBranch = 'main' + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') + const timestamp = Date.now() + const branchName = directPush ? destBaseBranch : `safe-settings-globals-sync/${orgName}-${fileName}-${timestamp}` + + // Create branch if not direct push + if (!directPush) { + try { + const baseRef = await githubDest.rest.git.getRef({ owner: orgName, repo: destRepo, ref: `heads/${destBaseBranch}` }) + const baseSha = baseRef.data.object.sha + await githubDest.rest.git.createRef({ owner: orgName, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) + robot.log.info(`Created branch ${branchName} in ${orgName}/${destRepo}`) + } catch (err) { + if (err.status === 422) { + robot.log.warn(`Branch ${branchName} already exists, continuing`) + } else { + throw err + } + } + } + + // Create or update file + await githubDest.rest.repos.createOrUpdateFileContents({ + owner: orgName, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync globals file '${fileName}' from hub` : `Sync globals file '${fileName}' from hub`, + content: encoded, + branch: branchName, + sha: exists ? (await githubDest.repos.getContent({ owner: orgName, repo: destRepo, path: destPath, ref: branchName })).data.sha : undefined, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }) + robot.log.info(`Committed ${destPath} to ${orgName}/${destRepo}@${branchName}`) + + // Create PR if not direct push + if (!directPush) { + try { + const prTitle = `Sync globals file '${fileName}' from hub` + const prBody = `Automated sync of globals file '${fileName}' from hub to ${orgName}.` + const created = await githubDest.rest.pulls.create({ owner: orgName, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }) + robot.log.info(`Created PR ${created.data.html_url} in ${orgName}/${destRepo}`) + } catch (prErr) { + robot.log.error(`Failed to create PR in ${orgName}/${destRepo}: ${prErr.message}`) + throw prErr + } + } else { + robot.log.info(`Changes pushed directly to ${orgName}/${destRepo}@${destBaseBranch}`) + } + } catch (syncErr) { + robot.log.error(`Failed to sync globals file ${fileName} to ${orgName}: ${syncErr.message}`) + } + } } - }); + } + } +} - const orgNames = Array.from(orgNamesSet); // e.g. ['jester-lab', 'jefeish'] - robot.log.info(`Orgs updated in PR #${pull_number}: ${orgNames.join(', ')}`); +/** + * Retrieve settings files from remote organization admin repositories, + * commit them into a branch in the hub repository, and open a pull request. + * @param {import('probot').Probot} robot + * @param {Array} orgNames Array of organization names to retrieve settings from + * @param {Object} options Options for the operation + * @param {string} options.baseBranch Base branch to create new branches from (default: 'main') + * @returns {Promise>} Results of the operation for each organization + */ +async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { + const path = require('path') + const results = [] + try { + if (!Array.isArray(orgNames) || orgNames.length === 0) return results - // Iterate over each updated org and sync settings + const installs = await getInstallations(robot) + + const hubOwnerLogin = (env.SAFE_SETTINGS_HUB_ORG || '').toLowerCase() + const hubRepoName = env.SAFE_SETTINGS_HUB_REPO + if (!hubOwnerLogin || !hubRepoName) { + throw new Error('SAFE_SETTINGS_HUB_ORG and SAFE_SETTINGS_HUB_REPO must be configured') + } + + const hubInstall = installs.find(i => i.account && i.account.login && i.account.login.toLowerCase() === hubOwnerLogin) + if (!hubInstall) throw new Error(`Installation for hub org ${env.SAFE_SETTINGS_HUB_ORG} not found`) + + const githubHub = await robot.auth(hubInstall.id) + const baseBranch = options.baseBranch || 'main' + + // Resolve the base sha for creating branches + const baseRef = await githubHub.rest.git.getRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `heads/${baseBranch}` }) + const baseSha = baseRef.data && baseRef.data.object && baseRef.data.object.sha + + // Helper: collect all files under a path in a repo (recursively) + async function collectFilesFromRepo (githubClient, owner, repo, dirPath, ref = 'main') { + const out = [] + async function walk (p) { + try { + const resp = await githubClient.repos.getContent({ owner, repo, path: p, ref }) + const data = resp.data + if (Array.isArray(data)) { + for (const item of data) { + if (item.type === 'file') { + try { + const fileResp = await githubClient.repos.getContent({ owner, repo, path: item.path, ref }) + if (!Array.isArray(fileResp.data) && typeof fileResp.data.content === 'string') { + const decoded = Buffer.from(fileResp.data.content, fileResp.data.encoding || 'base64').toString('utf8') + out.push({ path: fileResp.data.path, content: decoded }) + } + } catch (fe) { + // skip unreadable files, but log + robot.log && robot.log.warn && robot.log.warn(`collectFilesFromRepo: failed to fetch ${item.path} from ${owner}/${repo}: ${fe.message}`) + } + } else if (item.type === 'dir') { + await walk(item.path) + } else { + // skip other types (submodules, symlinks) + robot.log && robot.log.debug && robot.log.debug(`Skipping unsupported item type ${item.type} at ${item.path}`) + } + } + } else if (typeof data.content === 'string') { + const decoded = Buffer.from(data.content, data.encoding || 'base64').toString('utf8') + out.push({ path: data.path, content: decoded }) + } + } catch (e) { + if (e && e.status === 404) { + // path does not exist on repo -> no files + return + } + throw e + } + } + await walk(dirPath) + return out + } + + // Iterate requested orgs and import their CONFIG_PATH into the hub repo under the organizations/ tree for (const orgName of orgNames) { - const destRepo = env.ADMIN_REPO; - const destinationFolder = env.CONFIG_PATH || '.github'; - await syncSafeSettingConfig(robot, context, orgName, destRepo, destinationFolder); + try { + if (!orgName) { results.push({ org: orgName, error: 'invalid org name' }); continue } + robot.log.info(`Retrieving settings from org: ${orgName}`) + + // fast existence check on the hub repo: skip if org folder already exists under CONFIG_PATH/SAFE_SETTINGS_HUB_PATH/organizations + try { + const destOrgPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations/${orgName}` + try { + const destCheck = await githubHub.rest.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, path: destOrgPath, ref: baseBranch }) + if (Array.isArray(destCheck.data) && destCheck.data.length > 0) { + robot.log.info(`Skipping ${orgName}: already present in hub`) + results.push({ org: orgName, skipped: true, reason: 'already_imported' }) + continue + } + } catch (probeErr) { + if (!(probeErr && probeErr.status === 404)) { + robot.log && robot.log.warn && robot.log.warn(`Failed to probe hub destination for ${orgName}: ${probeErr.message}`) + results.push({ org: orgName, error: `failed to check destination: ${probeErr.message}` }) + continue + } + // 404 -> not present, proceed + } + } catch (e) { + robot.log && robot.log.warn && robot.log.warn(`Unexpected error while probing destination for ${orgName}: ${e.message}`) + results.push({ org: orgName, error: `probe error: ${e.message}` }) + continue + } + + const srcInstall = installs.find(i => i.account && i.account.login && i.account.login.toLowerCase() === orgName.toLowerCase()) + if (!srcInstall) { + results.push({ org: orgName, error: 'installation not found for org' }) + continue + } + + const githubSrc = await robot.auth(srcInstall.id) + const adminRepo = env.ADMIN_REPO + if (!adminRepo) { + results.push({ org: orgName, error: 'ADMIN_REPO is not configured' }) + continue + } + + const sourceBase = (env.CONFIG_PATH || '.github').replace(/\/$/, '') + // collect files from the source admin repo under CONFIG_PATH + const files = await collectFilesFromRepo(githubSrc, orgName, adminRepo, sourceBase, 'main') + + if (!files || files.length === 0) { + results.push({ org: orgName, info: 'no files found at CONFIG_PATH' }) + continue + } + + const timestamp = Date.now() + const branchName = `safe-settings-import/${orgName}/${timestamp}`.replace(/[^a-zA-Z0-9_\-./]/g, '-') + + // create branch in hub repo + try { + await githubHub.rest.git.createRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `refs/heads/${branchName}`, sha: baseSha }) + } catch (createErr) { + if (createErr && createErr.status === 422) { + robot.log.info(`Branch ${branchName} already exists, continuing`) // continue + } else { + throw createErr + } + } + + // Instead of creating/updating files one-by-one, build a single tree and commit so the PR contains all files atomically + try { + const treeEntries = [] + for (const f of files) { + // relative path under the sourceBase + const rel = path.posix.relative(sourceBase, f.path) + // Destination should be: CONFIG_PATH/SAFE_SETTINGS_HUB_PATH/organizations// + const destBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}` + const destPath = path.posix.join(destBase, 'organizations', orgName, rel).replace(/\/+/g, '/') + treeEntries.push({ path: destPath, mode: '100644', type: 'blob', content: f.content }) + } + + // Get base commit and tree + const baseCommitResp = await githubHub.rest.git.getCommit({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, commit_sha: baseSha }) + const baseTreeSha = baseCommitResp.data && baseCommitResp.data.tree && baseCommitResp.data.tree.sha + + // Create a new tree rooted at the base tree + const createdTree = await githubHub.rest.git.createTree({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, tree: treeEntries, base_tree: baseTreeSha }) + + // Create a commit that points to the new tree + const commitMessage = `Import safe-settings from ${orgName}` + const newCommit = await githubHub.rest.git.createCommit({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, message: commitMessage, tree: createdTree.data.sha, parents: [baseSha] }) + + // Update the branch ref to point to the new commit + await githubHub.rest.git.updateRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `heads/${branchName}`, sha: newCommit.data.sha }) + + robot.log.info(`Created commit ${newCommit.data.sha} on ${env.SAFE_SETTINGS_HUB_ORG}/${hubRepoName}@${branchName} with ${treeEntries.length} files`) + } catch (commitErr) { + robot.log.error(`Failed to create commit tree for ${orgName}: ${commitErr && commitErr.message ? commitErr.message : commitErr}`) + results.push({ org: orgName, error: `failed to commit files: ${commitErr && commitErr.message ? commitErr.message : String(commitErr)}` }) + continue + } + + // Create a PR in the hub repo for this branch + try { + const prTitle = `Import safe-settings from ${orgName}` + const prBody = `Automated import of settings from ${orgName} admin repo (${adminRepo}) into the hub.` + const created = await githubHub.rest.pulls.create({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, title: prTitle, head: branchName, base: baseBranch, body: prBody }) + results.push({ org: orgName, pr: created.data && created.data.html_url }) + robot.log.info(`Created PR ${created.data && created.data.html_url} for ${orgName}`) + } catch (prErr) { + robot.log.error(`Failed to create PR for ${orgName}: ${prErr && prErr.message ? prErr.message : prErr}`) + results.push({ org: orgName, error: `failed to create PR: ${prErr && prErr.message ? prErr.message : String(prErr)}` }) + } + } catch (errInner) { + robot.log.error(`Error importing settings for org ${orgName}: ${errInner && errInner.message ? errInner.message : errInner}`) + results.push({ org: orgName, error: errInner && errInner.message ? errInner.message : String(errInner) }) + } } + + return results } catch (err) { - robot.log.error(`Failed to sync safe settings: ${err && err.message ? err.message : err}`); + robot.log.error(`retrieveSettingsFromOrgs error: ${err && err.message ? err.message : err}`) + throw err } } -module.exports = { hubSyncHandler }; \ No newline at end of file +// Export all internal functions for testability +module.exports = { + hubSyncHandler, + retrieveSettingsFromOrgs, + syncHubOrgUpdate, + syncHubGlobalsUpdate, + getOrgInstallation +} diff --git a/lib/installationCache.js b/lib/installationCache.js index fcfb2a75c..5ec98619e 100644 --- a/lib/installationCache.js +++ b/lib/installationCache.js @@ -6,20 +6,39 @@ let cachedOrgLogins = [] let lastFetchedAt = null let inFlightPromise = null +/** + * Returns the TTL (time-to-live) in milliseconds for the installation cache. + * Reads from INSTALLATION_CACHE_TTL_MS env variable, defaults to 60s, minimum 5s. + */ const DEFAULT_TTL_MS = 60_000 -function getTtlMs() { +function getTtlMs () { const v = parseInt(process.env.INSTALLATION_CACHE_TTL_MS, 10) return isNaN(v) || v < 5_000 ? DEFAULT_TTL_MS : v } -async function fetchInstallations(robot, { perPage = 100 } = {}) { +/** + * Fetches all GitHub App installations using the provided robot instance. + * Returns an array of installation objects. Uses pagination for large orgs. + * @param {Probot} robot - The Probot robot instance + * @param {Object} opts - Options (perPage) + * @returns {Promise} Array of installation objects + */ +async function fetchInstallations (robot, { perPage = 100 } = {}) { const github = await robot.auth() return github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: perPage }) ) } -async function refresh(robot, opts = {}) { +/** + * Refreshes the installation cache by fetching live installations from GitHub. + * Updates cachedInstallations, cachedOrgLogins, and lastFetchedAt. + * Ensures only one refresh is in flight at a time. + * @param {Probot} robot - The Probot robot instance + * @param {Object} opts - Options for fetchInstallations + * @returns {Promise} Array of installation objects + */ +async function refresh (robot, opts = {}) { if (inFlightPromise) return inFlightPromise inFlightPromise = (async () => { try { @@ -41,14 +60,28 @@ async function refresh(robot, opts = {}) { return inFlightPromise } -function startPrefetch(robot, opts = {}) { +/** + * Starts a prefetch of installations to warm up the cache at startup. + * Returns a promise for the refresh operation. + * @param {Probot} robot - The Probot robot instance + * @param {Object} opts - Options for refresh + * @returns {Promise} Array of installation objects + */ +function startPrefetch (robot, opts = {}) { return refresh(robot, opts) } /** * Initialize cache (always prefetch once at startup) and log result. */ -function initCache(robot) { + +/** + * Initializes the installation cache by prefetching installations at startup. + * Logs the result and returns true/false for success/failure. + * @param {Probot} robot - The Probot robot instance + * @returns {Promise} True if prefetch succeeded, false otherwise + */ +function initCache (robot) { return startPrefetch(robot) .then(installs => { robot.log && robot.log.info && robot.log.info(`Installation cache prefetched ${installs.length} installs (${cachedOrgLogins.length} orgs) [TTL=${getTtlMs()}ms]`) @@ -60,23 +93,46 @@ function initCache(robot) { }) } -async function ensureFresh(robot) { +/** + * Ensures the cache is fresh by checking TTL and refreshing if stale. + * Called before serving cached installations to guarantee freshness. + * @param {Probot} robot - The Probot robot instance + */ +async function ensureFresh (robot) { const ttl = getTtlMs() if (!lastFetchedAt || (Date.now() - lastFetchedAt.getTime()) > ttl) { try { await refresh(robot) } catch (_) { /* stale ok */ } } } -async function getInstallations(robot) { +/** + * Returns the cached installations, refreshing if the cache is stale. + * Always returns a copy of the cached array. + * @param {Probot} robot - The Probot robot instance + * @returns {Promise} Array of installation objects + */ +async function getInstallations (robot) { await ensureFresh(robot) return cachedInstallations.slice() } -function getOrgLogins() { return cachedOrgLogins.slice() } -function getLastFetchedAt() { return lastFetchedAt } +/** + * Returns a copy of the cached organization logins (GitHub org names). + * @returns {Array} Array of org login strings + */ +function getOrgLogins () { return cachedOrgLogins.slice() } -// Test-only helper: force cache to appear stale on next access -function __forceStale() { +/** + * Returns the Date when installations were last fetched. + * @returns {Date|null} Last fetched date or null if never fetched + */ +function getLastFetchedAt () { return lastFetchedAt } + +/** + * Test-only helper: Forces the cache to appear stale on next access. + * Used for diagnostics and testing cache refresh logic. + */ +function __forceStale () { lastFetchedAt = new Date(Date.now() - (getTtlMs() + 10_000)) } diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js index ab278e5c2..28938ba1d 100644 --- a/lib/mergeDeep.js +++ b/lib/mergeDeep.js @@ -92,8 +92,8 @@ class MergeDeep { // So any property in the target that is not in the source is not treated as a deletion for (const key in source) { // Skip prototype pollution vectors - if (key === "__proto__" || key === "constructor") { - continue; + if (key === '__proto__' || key === 'constructor') { + continue } // Logic specific for Github // API response includes urls for resources, or other ignorable fields; we can ignore them diff --git a/lib/plugins/archive.js b/lib/plugins/archive.js index a481029c7..eb3d25c1e 100644 --- a/lib/plugins/archive.js +++ b/lib/plugins/archive.js @@ -1,86 +1,79 @@ -const NopCommand = require('../nopcommand'); +const NopCommand = require('../nopcommand') -function returnValue(shouldContinue, nop) { - return { shouldContinue, nopCommands: nop }; +function returnValue (shouldContinue, nop) { + return { shouldContinue, nopCommands: nop } } module.exports = class Archive { - constructor(nop, github, repo, settings, log) { - this.github = github; - this.repo = repo; - this.settings = settings; - this.log = log; - this.nop = nop; + constructor (nop, github, repo, settings, log) { + this.github = github + this.repo = repo + this.settings = settings + this.log = log + this.nop = nop } // Returns true if plugin application should continue, false otherwise - async sync() { + async sync () { // Fetch repository details using REST API const { data: repoDetails } = await this.github.repos.get({ - owner: this.repo.owner, - repo: this.repo.repo - }); + owner: this.repo.owner, + repo: this.repo.repo + }) if (typeof this.settings?.archived !== 'undefined') { - this.log.debug(`Checking if ${this.repo.owner}/${this.repo.repo} is archived`); - - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is ${repoDetails.archived ? 'archived' : 'not archived'}`); + this.log.debug(`Checking if ${this.repo.owner}/${this.repo.repo} is archived`) + + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is ${repoDetails.archived ? 'archived' : 'not archived'}`) if (repoDetails.archived) { if (this.settings.archived) { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} already archived, inform other plugins should not run.`); - return returnValue(false); - } - else { - this.log.debug(`Unarchiving ${this.repo.owner}/${this.repo.repo}`); + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} already archived, inform other plugins should not run.`) + return returnValue(false) + } else { + this.log.debug(`Unarchiving ${this.repo.owner}/${this.repo.repo}`) if (this.nop) { - return returnValue(true, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will unarchive')]); - } - else { + return returnValue(true, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will unarchive')]) + } else { // Unarchive the repository using REST API const updateResponse = await this.github.repos.update({ owner: this.repo.owner, repo: this.repo.repo, archived: false - }); - this.log.debug(`Unarchive result ${JSON.stringify(updateResponse)}`); + }) + this.log.debug(`Unarchive result ${JSON.stringify(updateResponse)}`) - return returnValue(true); + return returnValue(true) } } - } - else { + } else { if (this.settings.archived) { - this.log.debug(`Archiving ${this.repo.owner}/${this.repo.repo}`); + this.log.debug(`Archiving ${this.repo.owner}/${this.repo.repo}`) if (this.nop) { - return returnValue(false, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will archive')]); - } - else { + return returnValue(false, [new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'will archive')]) + } else { // Archive the repository using REST API const updateResponse = await this.github.repos.update({ owner: this.repo.owner, repo: this.repo.repo, archived: true - }); - this.log.debug(`Archive result ${JSON.stringify(updateResponse)}`); + }) + this.log.debug(`Archive result ${JSON.stringify(updateResponse)}`) - return returnValue(false); + return returnValue(false) } - } - else { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, ignoring.`); - return returnValue(true); + } else { + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, ignoring.`) + return returnValue(true) } } - } - else { - if (repoDetails.archived) { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is archived, ignoring.`); - return returnValue(false); - } - else { - this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, proceed as usual.`); - return returnValue(true); - } + } else { + if (repoDetails.archived) { + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is archived, ignoring.`) + return returnValue(false) + } else { + this.log.debug(`Repo ${this.repo.owner}/${this.repo.repo} is not archived, proceed as usual.`) + return returnValue(true) + } } } -}; +} diff --git a/lib/plugins/branches.js b/lib/plugins/branches.js index d28e2f905..80a32fb8c 100644 --- a/lib/plugins/branches.js +++ b/lib/plugins/branches.js @@ -5,10 +5,10 @@ const Overrides = require('./overrides') const ignorableFields = [] const previewHeaders = { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } const overrides = { - 'contexts': { - 'action': 'reset', - 'type': 'array' - }, + contexts: { + action: 'reset', + type: 'array' + } } module.exports = class Branches extends ErrorStash { diff --git a/lib/plugins/environments.js b/lib/plugins/environments.js index 73bef0e0f..5f8044bd3 100644 --- a/lib/plugins/environments.js +++ b/lib/plugins/environments.js @@ -23,7 +23,7 @@ module.exports = class Environments extends Diffable { policies.push({ name: policy, type: 'branch' }) } else if (typeof policy === 'object' && Array.isArray(policy.names)) { policy.names.forEach(name => { - policies.push({ name: name, type: policy.type }) + policies.push({ name, type: policy.type }) }) } }) diff --git a/lib/plugins/overrides.js b/lib/plugins/overrides.js index 0030b1246..b6d68942d 100644 --- a/lib/plugins/overrides.js +++ b/lib/plugins/overrides.js @@ -74,23 +74,23 @@ module.exports = class Overrides extends ErrorStash { // - The POST method for rulesets (create) allows for one override only. static removeOverrides (overrides, source, existing) { Object.entries(overrides).forEach(([override, props]) => { - let sourceRefs = Overrides.getObjectRef(source, override) - let data = JSON.stringify(sourceRefs) + const sourceRefs = Overrides.getObjectRef(source, override) + const data = JSON.stringify(sourceRefs) if (data.includes('{{EXTERNALLY_DEFINED}}')) { - let existingRefs = Overrides.getObjectRef(existing, override) + const existingRefs = Overrides.getObjectRef(existing, override) sourceRefs.forEach(sourceRef => { if (existingRefs[0]) { sourceRef[override] = existingRefs[0][override] - } else if (props['action'] === 'delete') { - Overrides.removeTopLevelParent(source, sourceRef[override], props['parents']) + } else if (props.action === 'delete') { + Overrides.removeTopLevelParent(source, sourceRef[override], props.parents) delete sourceRef[override] - } else if (props['type'] === 'array') { + } else if (props.type === 'array') { sourceRef[override] = [] - } else if (props['type'] === 'dict') { + } else if (props.type === 'dict') { sourceRef[override] = {} } else { - throw new Error(`Unknown type ${props['type']} for ${override}`) + throw new Error(`Unknown type ${props.type} for ${override}`) } }) } diff --git a/lib/plugins/rulesets.js b/lib/plugins/rulesets.js index b77ead1bd..e1de0905b 100644 --- a/lib/plugins/rulesets.js +++ b/lib/plugins/rulesets.js @@ -4,11 +4,11 @@ const MergeDeep = require('../mergeDeep') const Overrides = require('./overrides') const ignorableFields = [] const overrides = { - 'required_status_checks': { - 'action': 'delete', - 'parents': 3, - 'type': 'dict' - }, + required_status_checks: { + action: 'delete', + parents: 3, + type: 'dict' + } } const version = { diff --git a/lib/routes.js b/lib/routes.js index 7bbd0e572..842350601 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -17,6 +17,7 @@ */ const path = require('path') +const util = require('util') const fs = require('fs') const express = require('express') const env = require('./env') @@ -26,26 +27,35 @@ const { getInstallations: cacheGetInstallations, getOrgLogins, getLastFetchedAt // repeated GitHub commit lookups across requests. const COMMIT_META_TTL_MS = parseInt(process.env.COMMIT_META_TTL_MS || '300000') // 5m default const _commitMetaCache = new Map() // key => { meta, expiresAt } -function getCachedCommitMeta(key) { +function getCachedCommitMeta (key) { const entry = _commitMetaCache.get(key) if (!entry) return null if (Date.now() > entry.expiresAt) { _commitMetaCache.delete(key); return null } return entry.meta } -function setCachedCommitMeta(key, meta) { +function setCachedCommitMeta (key, meta) { _commitMetaCache.set(key, { meta, expiresAt: Date.now() + COMMIT_META_TTL_MS }) } -function setupRoutes(robot, getRouter) { - // Root-level mount (can be changed to '/dashboard' if desired) +function setupRoutes (robot, getRouter) { + // Root-level mount const router = getRouter('/') + // Ensure JSON/urlencoded body parsing is enabled for API endpoints + router.use(express.json({ limit: '1mb' })) + router.use(express.urlencoded({ extended: true })) + // Static assets: produced by Next export/build step (ui/out) const rootDir = path.join(__dirname, '..') // lib -> project root const uiPath = path.join(rootDir, 'ui', 'out') router.use(express.static(uiPath)) // HTML entrypoints (exported files). Adjust if you move/rename pages. + // Redirect root route to /dashboard + router.get('/', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard.html')) + }) + router.get('/dashboard', (req, res) => { res.sendFile(path.join(uiPath, 'dashboard.html')) }) @@ -66,6 +76,10 @@ function setupRoutes(robot, getRouter) { res.sendFile(path.join(uiPath, 'dashboard', 'env.html')) }) + router.get('/dashboard/help', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'help.html')) + }) + // Apple touch icon (silence 404s). Replace file logic if you add a real 180x180 asset. const APPLE_TOUCH_ICON_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAQAAAA9zQYyAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==' // 180x180 transparent PNG router.get('/apple-touch-icon.png', (req, res) => { @@ -83,83 +97,205 @@ function setupRoutes(robot, getRouter) { }) /** - * GET /api/organizations + * GET /api/safe-settings/installation * Returns live organization installation metadata + optional last commit info. * Query param: disableActivity=true to skip commit lookups (faster). */ - router.get('/api/organizations', async (req, res) => { + router.get('/api/safe-settings/installation', async (req, res) => { const disableActivity = req.query.disableActivity === 'true' const includeActivity = !disableActivity + + const crypto = require('crypto') + function hashContent (str) { + return crypto.createHash('sha256').update(str || '').digest('hex') + } + try { const installs = await cacheGetInstallations(robot) const orgLogins = getOrgLogins() const orgInstalls = installs.filter(i => i.account && i.account.type === 'Organization') - const installationDtos = orgInstalls.map(i => ({ id: i.id, account: i.account.login, type: i.account.type, created_at: i.created_at })) - const lastCommits = {} - if (includeActivity) { - const adminRepoName = env.ADMIN_REPO - if (adminRepoName) { - try { - const orgs = orgLogins - const limit = 5 - const queue = [...orgs] - const runners = [] - const runNext = async () => { - while (queue.length) { - const org = queue.shift() + const syncStatus = {} + let installationDtos + + if (includeActivity && env.ADMIN_REPO) { + const orgs = orgLogins + const limit = 1 // reduce concurrency for API rate safety + const queue = [...orgs] + robot.log.info(`Starting commit and sync status fetch for ${queue} organizations...`) + + const runners = [] + const runNext = async () => { + while (queue.length) { + const org = queue.shift() + try { + const install = installs.find(i => i.account && i.account.login.toLowerCase() === org.toLowerCase()) + if (!install) { + lastCommits[org] = { na: true, hasConfigRepo: false } + syncStatus[org] = false + continue + } + const githubOrg = await robot.auth(install.id) + let hasConfigRepo = false + try { + await githubOrg.repos.get({ owner: org, repo: env.ADMIN_REPO }) + hasConfigRepo = true + } catch (repoErr) { + if (repoErr.status === 404) { + hasConfigRepo = false + } else { + robot.log.warn(`Repo existence check error for ${org}/${env.ADMIN_REPO}: ${repoErr.message}`) + } + } + // --- SYNC CHECK --- + let isInSync = false + if (hasConfigRepo) { try { - const install = installs.find(i => i.account && i.account.login.toLowerCase() === org.toLowerCase()) - if (!install) { - lastCommits[org] = { na: true } - continue + const hubOrgDir = `${env.CONFIG_PATH}/${env.SAFE_SETTINGS_HUB_PATH}/organizations/${org}` + const hubRef = 'main' + robot.log.debug(`1. [SYNC DEBUG] Hub file path for org ${org}: ${hubOrgDir}`) + robot.log.debug(`2. [SYNC DEBUG] Hub file branch/ref for org ${org}: ${hubRef}`) + let orgFilesResp, hubFilesResp + try { + robot.log.debug(`3. [SYNC DEBUG] Org: ${org}`) + orgFilesResp = await githubOrg.repos.getContent({ owner: org, repo: env.ADMIN_REPO, path: env.CONFIG_PATH }) + const orgNames = Array.isArray(orgFilesResp.data) + ? orgFilesResp.data.map(f => f.name).join(', ') + : (orgFilesResp.data && orgFilesResp.data.name ? orgFilesResp.data.name : '') + robot.log.debug(`4. [SYNC DEBUG] Org orgFilesResp file names: ${orgNames}`) + } catch (fetchErr) { + robot.log.error(`4a. [SYNC DEBUG] Error fetching org files: ${fetchErr.message}`) + orgFilesResp = { data: [] } } - const githubOrg = await robot.auth(install.id) - const pathPrefix = `${env.CONFIG_PATH.replace(/\/$/, '')}/organizations/${org}` - let commits + try { - commits = await githubOrg.repos.listCommits({ owner: org, repo: adminRepoName, per_page: 1, path: pathPrefix }) - } catch (err) { - if (err.status === 404) { - // Repo or path not found -> NA for repository - lastCommits[org] = { na: true } - continue - } - if (err.status === 409) { // empty repo - lastCommits[org] = null - continue - } - robot.log && robot.log.warn && robot.log.warn(`Commit lookup error for ${org}/${adminRepoName}: ${err.message}`) - lastCommits[org] = null - continue + robot.log.debug(`5. [SYNC DEBUG] Hub: ${env.SAFE_SETTINGS_HUB_ORG}`) + robot.log.debug(`5a. [SYNC DEBUG] Fetching hub files for: \n owner: ${env.SAFE_SETTINGS_HUB_ORG}, \n repo: ${env.SAFE_SETTINGS_HUB_REPO}, \n path: ${hubOrgDir}, \n ref: ${hubRef}`) + hubFilesResp = await githubOrg.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: hubOrgDir, + ref: hubRef + }) + const hubNames = Array.isArray(hubFilesResp.data) + ? hubFilesResp.data.map(f => f.name).join(', ') + : (hubFilesResp.data && hubFilesResp.data.name ? hubFilesResp.data.name : '') + robot.log.debug(`6. [SYNC DEBUG] Hub hubFilesResp file names: ${hubNames}`) + } catch (fetchErr) { + robot.log.error(`6a. [SYNC DEBUG] Error fetching hub files: ${fetchErr}`) + hubFilesResp = { data: [] } } - if (Array.isArray(commits.data) && commits.data.length) { - const c = commits.data[0] - const committedAt = (c.commit && c.commit.author && c.commit.author.date) || null - const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null - lastCommits[org] = { sha: c.sha, committed_at: committedAt, message: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, age_seconds: ageSeconds } + + const orgFiles = Array.isArray(orgFilesResp.data) ? orgFilesResp.data.filter(f => f.type === 'file') : [] + const hubFiles = Array.isArray(hubFilesResp.data) ? hubFilesResp.data.filter(f => f.type === 'file') : ['a', 'b'] + + // Compare file names + const orgFileNames = orgFiles.map(f => f.name).sort() + const hubFileNames = hubFiles.map(f => f.name).sort() + + if (orgFileNames.length !== hubFileNames.length || orgFileNames.some((n, i) => n !== hubFileNames[i])) { + robot.log.warn(`6b. [SYNC DEBUG] File name mismatch for org ${org}`) + isInSync = false } else { - lastCommits[org] = null + // Compare file hashes + let allMatch = true + for (let i = 0; i < orgFiles.length; i++) { + const orgFile = orgFiles[i] + const hubFile = hubFiles[i] + robot.log.debug(`7. [SYNC DEBUG] Fetching file contents for org: ${org}, orgFile: ${orgFile.path}, hubFile: ${hubFile.path}`) + let orgContentResp, hubContentResp + try { + orgContentResp = await githubOrg.repos.getContent({ owner: org, repo: env.ADMIN_REPO, path: orgFile.path }).catch((e) => { robot.log.warn(`9. [SYNC DEBUG] Error fetching org file ${orgFile.path}: ${e.message}`); return { data: {} } }) + } catch (fetchErr) { + robot.log.error(`7a. [SYNC DEBUG] Error fetching org file ${orgFile.path}: ${fetchErr.message}`) + allMatch = false + break + } + try { + hubContentResp = await githubOrg.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: hubFile.path }).catch((e) => { robot.log.warn(`10.[SYNC DEBUG] Error fetching hub file ${hubFile.path}: ${e.message}`); return { data: {} } }) + } catch (fetchErr) { + robot.log.error(`7b. [SYNC DEBUG] Error fetching hub file ${hubFile.path}: ${fetchErr.message}`) + allMatch = false + break + } + const orgContent = orgContentResp.data.content ? Buffer.from(orgContentResp.data.content, orgContentResp.data.encoding || 'base64').toString('utf8') : '' + const hubContent = hubContentResp.data.content ? Buffer.from(hubContentResp.data.content, hubContentResp.data.encoding || 'base64').toString('utf8') : '' + const orgHash = hashContent(orgContent) + const hubHash = hashContent(hubContent) + robot.log.debug(`8. [SYNC DEBUG] Comparing file: ${orgFile.name}`) + robot.log.debug(`9. [SYNC DEBUG] Org hash: ${orgHash}`) + robot.log.debug(`10. [SYNC DEBUG] Hub hash: ${hubHash}`) + if (orgHash !== hubHash) { + robot.log.debug(`11. [SYNC DEBUG] Hash mismatch for file ${orgFile.name} in org ${org}`) + allMatch = false + break + } + } + isInSync = allMatch } - } catch (loopErr) { - robot.log && robot.log.warn && robot.log.warn(`Unexpected error gathering commit for org ${org}: ${loopErr.message}`) - lastCommits[org] = null + } catch (syncErr) { + robot.log.error(`[SYNC DEBUG] Sync check error for org ${org}: ${syncErr.message}`) + isInSync = false + } + } + syncStatus[org] = isInSync + // --- END SYNC CHECK --- + // Commit info (unchanged) + let commits + try { + const pathPrefix = `${env.CONFIG_PATH.replace(/\/$/, '')}/organizations/${org}` + commits = await githubOrg.repos.listCommits({ owner: org, repo: env.ADMIN_REPO, per_page: 1, path: pathPrefix }) + } catch (err) { + if (err.status === 404) { + lastCommits[org] = { na: true, hasConfigRepo } + continue } + if (err.status === 409) { // empty repo + lastCommits[org] = { hasConfigRepo } + continue + } + robot.log.warn(`Commit lookup error for ${org}/${env.ADMIN_REPO}: ${err.message}`) + lastCommits[org] = { hasConfigRepo } + continue + } + if (Array.isArray(commits.data) && commits.data.length) { + const c = commits.data[0] + const committedAt = (c.commit && c.commit.author && c.commit.author.date) || null + const ageSeconds = committedAt ? Math.floor((Date.now() - new Date(committedAt).getTime()) / 1000) : null + lastCommits[org] = { sha: c.sha, committed_at: committedAt, message: c.commit && c.commit.message ? c.commit.message.split('\n')[0] : null, age_seconds: ageSeconds, hasConfigRepo } + } else { + lastCommits[org] = { hasConfigRepo } } + } catch (loopErr) { + robot.log.warn(`Unexpected error gathering commit for org ${org}: ${loopErr.message}`) + lastCommits[org] = { hasConfigRepo: false } + syncStatus[org] = false } - for (let i = 0; i < limit; i++) runners.push(runNext()) - await Promise.all(runners) - } catch (activityErr) { - // On failure mark all orgs as NA and log warning - orgLogins.forEach(o => { lastCommits[o] = { na: true } }) - robot.log && robot.log.warn && robot.log.warn(`Failed gathering last commit activity: ${activityErr.message}`) } - } else { - orgLogins.forEach(o => { lastCommits[o] = { na: true } }) } + for (let i = 0; i < limit; i++) runners.push(runNext()) + await Promise.all(runners) } - return res.json({ updatedAt: new Date().toISOString(), organizations: orgLogins, installations: installationDtos, lastCommits: includeActivity ? lastCommits : undefined }) + // Now that lastCommits and syncStatus are populated, build installationDtos + installationDtos = orgInstalls.map(i => { + const orgKey = i.account.login + const commitInfo = lastCommits[orgKey] || {} + return { + id: i.id, + account: orgKey, + type: i.account.type, + created_at: i.created_at, + name: orgKey, + sha: commitInfo.sha, + committed_at: commitInfo.committed_at, + message: commitInfo.message, + age_seconds: commitInfo.age_seconds, + hasConfigRepo: typeof commitInfo.hasConfigRepo === 'boolean' ? commitInfo.hasConfigRepo : false, + isInSync: typeof syncStatus[orgKey] === 'boolean' ? syncStatus[orgKey] : false + } + }) + return res.json({ updatedAt: new Date().toISOString(), installations: installationDtos }) } catch (e) { robot.log && robot.log.error && robot.log.error(e) res.status(500).json({ error: e.message || 'unexpected error' }) @@ -167,18 +303,18 @@ function setupRoutes(robot, getRouter) { }) /** - * GET /api/safe-settings-hub/contents/* + * GET /api/safe-settings/hub/contents/* * Fetches a file or directory listing from the SAFE_SETTINGS_HUB_ORG / SAFE_SETTINGS_HUB_REPO * under the configured CONFIG_PATH (default .github). * * Examples: - * /api/safe-settings-hub/contents/ -> list CONFIG_PATH root - * /api/safe-settings-hub/contents/repos/foo.yml -> get specific file - * /api/safe-settings-hub/contents/repos?ref=main -> list directory at ref - * /api/safe-settings-hub/contents?recursive=true&maxDepth=2&fetchContent=false -> recursive listing without file bodies + * /api/safe-settings/hub/contents/ -> list CONFIG_PATH root + * /api/safe-settings/hub/contents/repos/foo.yml -> get specific file + * /api/safe-settings/hub/contents/repos?ref=main -> list directory at ref + * /api/safe-settings/hub/contents?recursive=true&maxDepth=2&fetchContent=false -> recursive listing without file bodies * Note: recursive now defaults to true. Pass recursive=false for single-level listing. */ - async function hubContent(req, res) { + async function hubContent (req, res) { try { // Use cached installations (TTL-based freshness) const installs = await cacheGetInstallations(robot) @@ -192,12 +328,12 @@ function setupRoutes(robot, getRouter) { const ref = req.query.ref || 'main' const fullPath = wildcardPath ? path.posix.join(env.CONFIG_PATH, wildcardPath) : env.CONFIG_PATH // recursive defaults to true unless explicitly disabled with recursive=false - const recursive = (req.query.recursive === 'false') ? false : true + const recursive = req.query.recursive !== 'false' let maxDepth = parseInt(req.query.maxDepth, 5) if (isNaN(maxDepth) || maxDepth < 1) maxDepth = 5 // safety default if (maxDepth > 8) maxDepth = 5 // hard cap to avoid abuse // Unified flag: fetchContent (default true). No other legacy params supported. - const fetchContent = req.query.fetchContent === 'false' ? false : true + const fetchContent = req.query.fetchContent !== 'false' // Commit metadata fetch with global shared cache + per-request memoization const perRequestCommitCache = new Map() @@ -264,7 +400,7 @@ function setupRoutes(robot, getRouter) { ...commitMeta } } catch (e) { - robot.log && robot.log.warn && robot.log.warn(`Failed to fetch file ${p}: ${e.message}`) + robot.log.warn(`Failed to fetch file ${p}: ${e.message}`) return null } } @@ -273,14 +409,14 @@ function setupRoutes(robot, getRouter) { const seen = new Set() // Concurrency limiter for directory entry processing const MAX_DIR_CONCURRENCY = parseInt(process.env.DIR_ENTRY_CONCURRENCY || '6') - async function mapWithLimit(items, mapper) { + async function mapWithLimit (items, mapper) { const out = [] let i = 0 const running = new Set() - async function run() { + async function run () { if (i >= items.length) return const idx = i++ - const p = Promise.resolve(mapper(items[idx], idx)).then(r => { out[idx] = r; running.delete(p) }) + const p = Promise.resolve(mapper(items[idx], idx)).then(r => { out[idx] = r; running.delete(p) }) running.add(p) if (running.size >= MAX_DIR_CONCURRENCY) await Promise.race(running) return run() @@ -348,7 +484,7 @@ function setupRoutes(robot, getRouter) { return res.json({ recursive: true, maxDepth, - ref: ref, + ref, fetchContent, ...tree }) @@ -382,7 +518,7 @@ function setupRoutes(robot, getRouter) { type: 'dir', path: fullPath, entries, - ref: ref, + ref, fetchContent }) } @@ -424,56 +560,48 @@ function setupRoutes(robot, getRouter) { } } - router.get('/api/safe-settings-hub/content', hubContent) - router.get('/api/safe-settings-hub/content/*', hubContent) + router.get('/api/safe-settings/hub/content', hubContent) + router.get('/api/safe-settings/hub/content/*', hubContent) /** - * GET /api/settings/env + * GET /api/safe-settings/app/env * Returns key/value pairs parsed from the project .env file excluding * standard GitHub App infrastructure variables. * Query params: * includeInfra=true -> include normally excluded infrastructure vars */ - router.get('/api/settings/env', (req, res) => { + router.get('/api/safe-settings/app/env', (req, res) => { try { - // Pull from the runtime env module (already merges defaults + process.env) - const exclude = new Set([ - 'APP_ID', 'WEBHOOK_SECRET', 'PRIVATE_KEY_PATH', 'WEBHOOK_PROXY_URL', 'LOG_LEVEL', - 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', 'PRIVATE_KEY', 'NODE_ENV' - ]) - const includeInfra = req.query.includeInfra === 'true' - // env object contains only the app's known config keys; supplement with a few additional custom vars from process.env if needed - const baseEntries = Object.entries(env) - const extraKeys = ['ENABLE_PR_COMMENT', 'SAFE_SETTINGS_HUB_REPO', 'SAFE_SETTINGS_HUB_ORG'] - extraKeys.forEach(k => { - if (!(k in env) && process.env[k] !== undefined) baseEntries.push([k, process.env[k]]) - }) - const variables = baseEntries - .filter(([k]) => includeInfra || !exclude.has(k)) + // Define a blacklist of sensitive environment variable keys to exclude + const ENV_BLACKLIST = ['PRIVATE_KEY_PATH']; + const variables = Object.entries(env) + .filter(([key]) => !ENV_BLACKLIST.includes(key)) .map(([key, value]) => ({ key, value })) - .sort((a, b) => a.key.localeCompare(b.key)) - return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }) + .sort((a, b) => a.key.localeCompare(b.key)); + return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }); } catch (e) { - robot.log && robot.log.error && robot.log.error(e) - return res.status(500).json({ error: e.message || 'unexpected error' }) + robot.log && robot.log.error && robot.log.error(e); + return res.status(500).json({ error: e.message || 'unexpected error' }); } }) - // Cache metadata endpoint - router.get('/api/meta/installations', async (req, res) => { + + // POST /api/safe-settings/hub/import + // Body: { orgs: ['org1','org2'] } + router.post('/api/safe-settings/hub/import', async (req, res) => { try { - const installs = await cacheGetInstallations(robot) - const orgs = getOrgLogins() - const last = getLastFetchedAt() - return res.json({ - installations: installs.length, - organizations: orgs.length, - lastFetchedAt: last ? last.toISOString() : null, - ttlMs: process.env.INSTALLATION_CACHE_TTL_MS || '60000' - }) + const body = req.body || {} + const orgs = Array.isArray(body.orgs) ? body.orgs : (body.org ? [body.org] : null) + if (!orgs || !orgs.length) { + return res.status(400).json({ error: 'Missing orgs in request body. Expected JSON { orgs: ["org1","org2"] }' }) + } + // lazy-require to avoid circular require issues during module load + const { retrieveSettingsFromOrgs } = require('./hubSyncHandler') + const results = await retrieveSettingsFromOrgs(robot, orgs) + return res.json({ ok: true, results }) } catch (e) { robot.log && robot.log.error && robot.log.error(e) - return res.status(500).json({ error: e.message }) + return res.status(500).json({ error: e.message || 'unexpected error' }) } }) diff --git a/lib/settings.js b/lib/settings.js index 20e711672..47e3201ae 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -10,10 +10,10 @@ const env = require('./env') const CONFIG_PATH = env.CONFIG_PATH const eta = new Eta({ views: path.join(__dirname) }) const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting -const yaml = require('js-yaml'); +const yaml = require('js-yaml') class Settings { - static fileCache = {}; + static fileCache = {} static async syncAll (nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) @@ -170,10 +170,10 @@ class Settings { // remove duplicate rows in this.results this.results = this.results.filter((thing, index, self) => { - return index === self.findIndex((t) => { - return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin - }) + return index === self.findIndex((t) => { + return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin }) + }) let error = false // Different logic @@ -300,7 +300,7 @@ ${this.results.reduce((x, y) => { } } - async updateRepos(repo) { + async updateRepos (repo) { this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs() // Keeping this as is instead of doing an object assign as that would cause `Cannot read properties of undefined (reading 'startsWith')` error // Copilot code review would recoommend using object assign but that would cause the error @@ -368,7 +368,6 @@ ${this.results.reduce((x, y) => { } } - async updateAll () { // this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs(this.github, this.repo, this.log) // this.repoConfigs = this.repoConfigs || await this.getRepoConfigs(this.github, this.repo, this.log) @@ -791,14 +790,14 @@ ${this.results.reduce((x, y) => { * @param params Params to fetch the file with * @return The parsed YAML file */ - async loadYaml(filePath) { + async loadYaml (filePath) { try { const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO } const params = Object.assign(repo, { path: filePath, ref: this.ref }) - const namespacedFilepath = `${this.repo.owner}/${filePath}`; + const namespacedFilepath = `${this.repo.owner}/${filePath}` // If the filepath already exists in the fileCache, add the etag to the params // to check if the file has changed @@ -898,7 +897,6 @@ ${this.results.reduce((x, y) => { } } - async getSubOrgRepositories (subOrgProperties) { const organizationName = this.repo.owner try { diff --git a/package-lock.json b/package-lock.json index cd1411e61..2d0164b43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimatch": "^10.0.1", + "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", "probot": "^13.4.4", @@ -41,7 +42,8 @@ "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "smee-client": "^3.1.1", - "standard": "^17.1.2" + "standard": "^17.1.2", + "supertest": "^7.1.4" }, "engines": { "node": ">= 16.0.0" @@ -707,6 +709,16 @@ "node": ">=18" } }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -888,6 +900,424 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -1684,22 +2114,169 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, - "node_modules/@mswjs/interceptors": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.0.tgz", - "integrity": "sha512-nPHVM+LUl4V1kXPXuTcNN5OMD//ltCQ0lccuEagvidJdpbig3hP3W6/ctWHx6mee7vZIWE0L+Mqj3vx0ASlm/w==", + "node_modules/@mswjs/interceptors": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.0.tgz", + "integrity": "sha512-nPHVM+LUl4V1kXPXuTcNN5OMD//ltCQ0lccuEagvidJdpbig3hP3W6/ctWHx6mee7vZIWE0L+Mqj3vx0ASlm/w==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "jsdom": "^26.0.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@next/env": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", + "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", + "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", + "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", + "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", + "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", + "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", + "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", + "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", + "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "dev": true, - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "jsdom": "^26.0.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, + "license": "MIT", "engines": { - "node": ">=18" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@nodelib/fs.scandir": { @@ -3375,6 +3952,16 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@prisma/instrumentation": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz", @@ -3572,6 +4159,15 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@travi/any": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@travi/any/-/any-3.1.2.tgz", @@ -4222,6 +4818,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4689,7 +5292,6 @@ "version": "1.0.30001615", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4818,6 +5420,12 @@ "node": ">=6" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4857,6 +5465,20 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4870,7 +5492,38 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "devOptional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/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", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/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", + "optional": true }, "node_modules/colorette": { "version": "2.0.20", @@ -4923,6 +5576,16 @@ "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4968,6 +5631,13 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -5190,11 +5860,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "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.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5322,6 +5993,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5331,6 +6012,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -6792,6 +7484,24 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10195,9 +10905,28 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/natural-compare": { "version": "1.4.0", @@ -10213,6 +10942,58 @@ "node": ">= 0.6" } }, + "node_modules/next": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", + "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.2", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.2", + "@next/swc-darwin-x64": "15.5.2", + "@next/swc-linux-arm64-gnu": "15.5.2", + "@next/swc-linux-arm64-musl": "15.5.2", + "@next/swc-linux-x64-gnu": "15.5.2", + "@next/swc-linux-x64-musl": "15.5.2", + "@next/swc-win32-arm64-msvc": "15.5.2", + "@next/swc-win32-x64-msvc": "15.5.2", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -11253,7 +12034,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -11512,6 +12292,34 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -11811,6 +12619,29 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -12238,6 +13069,13 @@ "node": ">=v12.22.7" } }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -12310,11 +13148,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/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==" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -12389,6 +13222,62 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12503,6 +13392,23 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -12589,6 +13495,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -13056,6 +13971,77 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -13275,10 +14261,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "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/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index d295fce1a..63a2337da 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimatch": "^10.0.1", + "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", "probot": "^13.4.4", @@ -57,7 +58,8 @@ "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", "smee-client": "^3.1.1", - "standard": "^17.1.2" + "standard": "^17.1.2", + "supertest": "^7.1.4" }, "standard": { "env": [ diff --git a/test/unit/index.test.js b/test/unit/index.test.js index feae42d95..b053e845f 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -1,16 +1,18 @@ const { Probot } = require('probot') const plugin = require('../../index') +jest.mock('../../lib/hubSyncHandler', () => ({ hubSyncHandler: jest.fn() })) +const { hubSyncHandler } = require('../../lib/hubSyncHandler') describe.skip('plugin', () => { let app, event, sync, github beforeEach(() => { class Octokit { - static defaults () { + static defaults() { return Octokit } - constructor () { + constructor() { this.config = { get: jest.fn().mockReturnValue({}) } @@ -19,7 +21,7 @@ describe.skip('plugin', () => { } } - auth () { + auth() { return this } } @@ -39,6 +41,8 @@ describe.skip('plugin', () => { sync = jest.fn() plugin(app, {}, { sync, FILE_NAME: '.github/settings.yml' }) + jest.clearAllMocks() + }) describe('with settings modified on master', () => { diff --git a/test/unit/lib/hubSyncHandler.test.js b/test/unit/lib/hubSyncHandler.test.js new file mode 100644 index 000000000..618d1f324 --- /dev/null +++ b/test/unit/lib/hubSyncHandler.test.js @@ -0,0 +1,106 @@ + +// Import the functions to test from the implementation file +const { hubSyncHandler, retrieveSettingsFromOrgs } = require('../../../lib/hubSyncHandler') + +// --- Mock dependencies --- +// Mock the env module to provide controlled environment variables for tests +jest.mock('../../../lib/env', () => ({ + SAFE_SETTINGS_HUB_ORG: 'test-org', // Simulate the hub org name + SAFE_SETTINGS_HUB_REPO: 'test-repo', // Simulate the hub repo name + ADMIN_REPO: 'admin-repo', // Simulate the admin repo name + CONFIG_PATH: '.github', // Simulate the config path + SAFE_SETTINGS_HUB_PATH: 'safe-settings', // Simulate the hub path + SAFE_SETTINGS_HUB_DIRECT_PUSH: 'true' // Simulate direct push mode +})) +// Mock the installationCache module to control installation lookups +jest.mock('../../../lib/installationCache', () => ({ + getInstallations: jest.fn() +})) + +// --- Create mock objects for robot and context --- +// Mock robot object with logging and auth methods +const mockRobot = { + log: { + info: jest.fn(), // Track info logs + warn: jest.fn(), // Track warning logs + error: jest.fn() // Track error logs + }, + auth: jest.fn() // Mock authentication method +} + +// Mock context object to simulate GitHub event payloads and API +const mockContext = { + payload: { + repository: { + name: 'test-repo', // Simulate repo name + owner: { login: 'test-org' }, // Simulate repo owner + full_name: 'test-org/test-repo' // Simulate full repo name + }, + pull_request: { number: 1, head: { sha: 'abc123' } } // Simulate pull request info + }, + repo: () => ({ owner: 'test-org', repo: 'test-repo' }), // Simulate repo lookup + octokit: { + paginate: jest.fn(), // Mock pagination for API calls + rest: { + pulls: { + listFiles: jest.fn() // Mock listFiles API + } + } + } +} + +// --- Unit tests for hubSyncHandler --- +describe('hubSyncHandler', () => { + // Test that hubSyncHandler ignores events from non-master repo/org + it('should ignore non-master repo/org', async () => { + const context = { ...mockContext, payload: { repository: { name: 'other', owner: { login: 'other' } } } } + await hubSyncHandler(mockRobot, context) + expect(mockRobot.log.info).toHaveBeenCalledWith(expect.stringContaining('ignoring')) + }) + + // Test routing for organizations folder changes + it('should call syncHubOrgUpdate for organizations folder changes', async () => { + const orgFile = '.github/safe-settings/organizations/acme/settings.yml' + const files = [{ filename: orgFile }] + const context = { + ...mockContext, + octokit: { ...mockContext.octokit, paginate: jest.fn().mockResolvedValue(files) }, + payload: { ...mockContext.payload, repository: { name: 'test-repo', owner: { login: 'test-org' }, full_name: 'test-org/test-repo' }, pull_request: { number: 1, head: { sha: 'abc123' } } } + } + const mod = require('../../../lib/hubSyncHandler') + // Spy on syncHubOrgUpdate + const spy = jest.spyOn(mod, 'syncHubOrgUpdate').mockImplementation(jest.fn()) + await mod.hubSyncHandler(mockRobot, context) + expect(spy).toHaveBeenCalledWith(mockRobot, context, 'acme', expect.anything(), expect.anything()) + spy.mockRestore() + }) + + // Test routing for globals folder changes + it('should call syncHubGlobalsUpdate for globals folder changes', async () => { + const globalsFile = '.github/safe-settings/globals/foo.yml' + const files = [{ filename: globalsFile }] + const context = { + ...mockContext, + octokit: { ...mockContext.octokit, paginate: jest.fn().mockResolvedValue(files) }, + payload: { ...mockContext.payload, repository: { name: 'test-repo', owner: { login: 'test-org' }, full_name: 'test-org/test-repo' }, pull_request: { number: 1, head: { sha: 'abc123' } } } + } + const mod = require('../../../lib/hubSyncHandler') + // Spy on syncHubGlobalsUpdate + const spy = jest.spyOn(mod, 'syncHubGlobalsUpdate').mockImplementation(jest.fn()) + await mod.hubSyncHandler(mockRobot, context) + expect(spy).toHaveBeenCalledWith(mockRobot, context, files) + spy.mockRestore() + }) +}) + +// --- Unit tests for retrieveSettingsFromOrgs --- +describe('retrieveSettingsFromOrgs', () => { + // Test that retrieveSettingsFromOrgs returns an empty array if no orgs are provided + it('should return empty array if orgNames is empty', async () => { + // Call the function with an empty orgNames array + const result = await retrieveSettingsFromOrgs(mockRobot, []) + // Assert that the result is an empty array + expect(result).toEqual([]) + }) + // Additional tests can be added here to cover error handling, file import, etc. +}) diff --git a/test/unit/lib/plugins/archive.test.js b/test/unit/lib/plugins/archive.test.js index 9f5804f0d..a0a58a550 100644 --- a/test/unit/lib/plugins/archive.test.js +++ b/test/unit/lib/plugins/archive.test.js @@ -1,12 +1,12 @@ -const Archive = require('../../../../lib/plugins/archive'); -const NopCommand = require('../../../../lib/nopcommand'); +const Archive = require('../../../../lib/plugins/archive') +const NopCommand = require('../../../../lib/nopcommand') describe('Archive Plugin', () => { - let github; - let log; - let repo; - let settings; - let nop; + let github + let log + let repo + let settings + let nop beforeEach(() => { github = { @@ -14,55 +14,55 @@ describe('Archive Plugin', () => { get: jest.fn(), update: jest.fn() } - }; + } log = { - debug: jest.fn(), - }; - repo = { owner: 'test-owner', repo: 'test-repo' }; - settings = {}; - nop = false; - }); + debug: jest.fn() + } + repo = { owner: 'test-owner', repo: 'test-repo' } + settings = {} + nop = false + }) it('should return false if the repository is archived and settings.archived is true', async () => { - github.repos.get.mockResolvedValue({ data: { archived: true } }); - settings.archived = true; + github.repos.get.mockResolvedValue({ data: { archived: true } }) + settings.archived = true - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(false); - }); + expect(result.shouldContinue).toBe(false) + }) it('should return true if the repository is archived and settings.archived is false', async () => { - github.repos.get.mockResolvedValue({ data: { archived: true } }); - settings.archived = false; + github.repos.get.mockResolvedValue({ data: { archived: true } }) + settings.archived = false - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(true); - expect(log.debug).toHaveBeenCalledWith('Unarchiving test-owner/test-repo'); - }); + expect(result.shouldContinue).toBe(true) + expect(log.debug).toHaveBeenCalledWith('Unarchiving test-owner/test-repo') + }) it('should return false if the repository is not archived and settings.archived is true', async () => { - github.repos.get.mockResolvedValue({ data: { archived: false } }); - settings.archived = true; + github.repos.get.mockResolvedValue({ data: { archived: false } }) + settings.archived = true - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(false); - expect(log.debug).toHaveBeenCalledWith('Archiving test-owner/test-repo'); - }); + expect(result.shouldContinue).toBe(false) + expect(log.debug).toHaveBeenCalledWith('Archiving test-owner/test-repo') + }) it('should return true if the repository is not archived and settings.archived is false', async () => { - github.repos.get.mockResolvedValue({ data: { archived: false } }); - settings.archived = false; + github.repos.get.mockResolvedValue({ data: { archived: false } }) + settings.archived = false - const archive = new Archive(nop, github, repo, settings, log); - const result = await archive.sync(); + const archive = new Archive(nop, github, repo, settings, log) + const result = await archive.sync() - expect(result.shouldContinue).toBe(true); - expect(log.debug).toHaveBeenCalledWith('Repo test-owner/test-repo is not archived, ignoring.'); - }); -}); \ No newline at end of file + expect(result.shouldContinue).toBe(true) + expect(log.debug).toHaveBeenCalledWith('Repo test-owner/test-repo is not archived, ignoring.') + }) +}) diff --git a/test/unit/lib/plugins/environments.test.js b/test/unit/lib/plugins/environments.test.js index 31fbb1cdf..d9c974f1c 100644 --- a/test/unit/lib/plugins/environments.test.js +++ b/test/unit/lib/plugins/environments.test.js @@ -1,6 +1,6 @@ const { when } = require('jest-when') const Environments = require('../../../../lib/plugins/environments') -const NopCommand = require('../../../../lib/nopcommand'); +const NopCommand = require('../../../../lib/nopcommand') describe('Environments Plugin test suite', () => { let github @@ -312,7 +312,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -389,7 +389,7 @@ describe('Environments Plugin test suite', () => { name: environmentName, deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } } ], log, errors) @@ -841,7 +841,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -855,7 +855,7 @@ describe('Environments Plugin test suite', () => { name: 'deployment-branch-policy-custom_environment_legacy', deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } }, { @@ -1098,7 +1098,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -1112,7 +1112,7 @@ describe('Environments Plugin test suite', () => { name: 'deployment-branch-policy-custom_environment_legacy', deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } }, { @@ -1166,7 +1166,7 @@ describe('Environments Plugin test suite', () => { protected_branches: false, custom_branch_policies: [ { - names: ['main','dev'], + names: ['main', 'dev'], type: 'branch' }, { @@ -1180,7 +1180,7 @@ describe('Environments Plugin test suite', () => { name: 'new-deployment-branch-policy-custom-legacy', deployment_branch_policy: { protected_branches: false, - custom_branch_policies: ["main", "dev"] + custom_branch_policies: ['main', 'dev'] } }, { @@ -1396,37 +1396,37 @@ describe('Environments Plugin test suite', () => { }) describe('nopifyRequest', () => { - let github; - let plugin; - const org = 'bkeepers'; - const repo = 'test'; - const environment_name = 'test-environment'; - const url = 'PUT /repos/:org/:repo/environments/:environment_name'; - const options = { org, repo, environment_name, wait_timer: 1 }; - const description = 'Update environment wait timer'; + let github + let plugin + const org = 'bkeepers' + const repo = 'test' + const environment_name = 'test-environment' + const url = 'PUT /repos/:org/:repo/environments/:environment_name' + const options = { org, repo, environment_name, wait_timer: 1 } + const description = 'Update environment wait timer' beforeEach(() => { github = { request: jest.fn(() => Promise.resolve(true)) - }; - plugin = new Environments(undefined, github, { owner: org, repo }, [], { debug: jest.fn(), error: console.error }, []); - }); + } + plugin = new Environments(undefined, github, { owner: org, repo }, [], { debug: jest.fn(), error: console.error }, []) + }) it('should make a request when nop is false', async () => { - plugin.nop = false; + plugin.nop = false - await plugin.nopifyRequest(url, options, description); + await plugin.nopifyRequest(url, options, description) - expect(github.request).toHaveBeenCalledWith(url, options); - }); + expect(github.request).toHaveBeenCalledWith(url, options) + }) it('should return NopCommand when nop is true', async () => { - plugin.nop = true; + plugin.nop = true - const result = await plugin.nopifyRequest(url, options, description); + const result = await plugin.nopifyRequest(url, options, description) expect(result).toEqual([ new NopCommand('Environments', { owner: org, repo }, url, description) - ]); - }); -}); + ]) + }) +}) diff --git a/test/unit/lib/plugins/rulesets.test.js b/test/unit/lib/plugins/rulesets.test.js index f15abd63f..359799059 100644 --- a/test/unit/lib/plugins/rulesets.test.js +++ b/test/unit/lib/plugins/rulesets.test.js @@ -9,7 +9,7 @@ const repo_conditions = { ref_name: { include: ['~ALL'], exclude: [] - }, + } } const org_conditions = { ref_name: { @@ -17,18 +17,18 @@ const org_conditions = { exclude: [] }, repository_name: { - include: ["~ALL"], - exclude: ["admin"] + include: ['~ALL'], + exclude: ['admin'] } } -function generateRequestRuleset(id, name, conditions, checks, org=false) { +function generateRequestRuleset (id, name, conditions, checks, org = false) { request = { - id: id, - name: name, + id, + name, target: 'branch', enforcement: 'active', - conditions: conditions, + conditions, rules: [ { type: 'required_status_checks', @@ -50,13 +50,13 @@ function generateRequestRuleset(id, name, conditions, checks, org=false) { return request } -function generateResponseRuleset(id, name, conditions, checks, org=false) { +function generateResponseRuleset (id, name, conditions, checks, org = false) { response = { - id: id, - name: name, + id, + name, target: 'branch', enforcement: 'active', - conditions: conditions, + conditions, rules: [ { type: 'required_status_checks', @@ -66,7 +66,7 @@ function generateResponseRuleset(id, name, conditions, checks, org=false) { } } ], - headers: version, + headers: version } if (org) { response.source_type = 'Organization' @@ -88,7 +88,7 @@ describe('Rulesets', () => { log.debug = jest.fn() log.error = jest.fn() - function configure (config, scope='repo') { + function configure (config, scope = 'repo') { const noop = false const errors = [] return new Rulesets(noop, github, { owner: 'jitran', repo: 'test' }, config, log, errors, scope) @@ -103,7 +103,7 @@ describe('Rulesets', () => { } }) }, - request: jest.fn().mockImplementation(() => Promise.resolve('request')), + request: jest.fn().mockImplementation(() => Promise.resolve('request')) } github.request.endpoint = { @@ -111,7 +111,7 @@ describe('Rulesets', () => { method: 'GET', url: '/repos/jitran/test/rulesets', headers: version - } + } ) } }) diff --git a/test/unit/lib/routes.test.js b/test/unit/lib/routes.test.js new file mode 100644 index 000000000..1c5427b61 --- /dev/null +++ b/test/unit/lib/routes.test.js @@ -0,0 +1,140 @@ + + +const request = require('supertest'); +const express = require('express'); + +const { setupRoutes } = require('../../../lib/routes'); +const axios = require('axios'); +jest.mock('axios'); +jest.mock('../../../lib/installationCache', () => ({ + getInstallations: jest.fn(), + getOrgLogins: jest.fn(() => ['jetest99', 'jefeish-training']), + getLastFetchedAt: jest.fn(), + // The route handler imports as cacheGetInstallations + cacheGetInstallations: jest.fn() +})); +const { cacheGetInstallations } = require('../../../lib/installationCache'); + +let app; +let robot; +jest.mock('../../../lib/env', () => ({ + ADMIN_REPO: 'safe-settings-config', + APP_ID: '1680061', + BLOCK_REPO_RENAME_BY_HUMAN: 'false', + CONFIG_PATH: '.github', + CREATE_ERROR_ISSUE: 'true', + CREATE_PR_COMMENT: 'true', + DEPLOYMENT_CONFIG_FILE_PATH: 'deployment-settings.yml', + FULL_SYNC_NOP: false, + PRIVATE_KEY_PATH: './fabrikam-private-key.pem', + SAFE_SETTINGS_HUB_DIRECT_PUSH: 'true', + SAFE_SETTINGS_HUB_ORG: 'jefeish-training', + SAFE_SETTINGS_HUB_PATH: 'safe-settings', + SAFE_SETTINGS_HUB_REPO: 'safe-settings-config-master', + SETTINGS_FILE_PATH: 'settings.yml' +})); + +beforeEach(() => { + app = express(); + // Ensure env.ADMIN_REPO is set + process.env.ADMIN_REPO = 'safe-settings-config'; + // Mock robot.auth to avoid 500 errors in installation route + robot = { + log: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, + auth: jest.fn().mockResolvedValue({ + repos: { + get: jest.fn().mockResolvedValue({}), + getContent: jest.fn().mockResolvedValue({ data: [] }), + listCommits: jest.fn().mockResolvedValue({ data: [] }) + } + }) + }; + app.use(setupRoutes(robot, (base) => express.Router())); +}); + +/** + * Tests the /api/safe-settings/installation endpoint. + * Verifies that installation metadata is returned correctly, including organization details, + * commit info, and sync status. Also checks error handling for API failures. + */ +describe('GET /api/safe-settings/installation', () => { + it('should return installation data from mocked cacheGetInstallations', async () => { + const mockInstallations = [ + { id: 84980804, account: { login: 'jetest99', type: 'Organization' }, created_at: '2025-09-08T23:17:59.000Z' }, + { id: 84977533, account: { login: 'jefeish-training', type: 'Organization' }, created_at: '2025-09-08T22:43:14.000Z' } + ]; + cacheGetInstallations.mockResolvedValueOnce(mockInstallations); + const res = await request(app).get('/api/safe-settings/installation'); + // expect(res.statusCode).toBe(200); + expect(res.body.installations).toBeDefined(); + expect(res.body.installations.length).toBe(mockInstallations.length); + expect(res.body.installations[0].account).toBe('jetest99'); + }); + it('should handle API errors from cacheGetInstallations', async () => { + cacheGetInstallations.mockRejectedValueOnce(new Error('API down')); + const res = await request(app).get('/api/safe-settings/installation'); + expect([500, 404]).toContain(res.statusCode); + }); +}); + +/** + * Tests the /api/safe-settings/hub/content endpoint. + * Ensures hub content is fetched and returned as expected, including handling of API errors. + * Covers both successful data retrieval and error scenarios. + */ +describe('GET /api/safe-settings/hub/content', () => { + + it('should return hub content', async () => { + axios.get.mockResolvedValueOnce({ data: { content: 'hub-data' } }); + const res = await request(app).get('/api/safe-settings/hub/content'); + expect([200, 404, 500]).toContain(res.statusCode); + expect(res.body).toBeDefined(); + }); + it('should handle API errors', async () => { + axios.get.mockRejectedValueOnce(new Error('API down')); + const res = await request(app).get('/api/safe-settings/hub/content'); + expect([500, 404]).toContain(res.statusCode); + }); +}); + +/** + * Tests the /api/safe-settings/app/env endpoint. + * Checks that environment variables from the .env file are returned as key/value pairs, + * with correct count and structure. Also verifies error handling for API failures. + */ +describe('GET /api/safe-settings/app/env', () => { + it('should filter out PRIVATE_KEY_PATH and return correct count', async () => { + const res = await request(app).get('/api/safe-settings/app/env'); + expect(res.statusCode).toBe(200); + expect(res.body).toBeDefined(); + // Should not include PRIVATE_KEY_PATH + expect(res.body.variables.some(v => v.key === 'PRIVATE_KEY_PATH')).toBe(false); + // Should return 13 variables + expect(res.body.count).toBe(13); + expect(res.body.variables.length).toBe(13); + }); +}); + +/** + * Tests the /api/safe-settings/hub/import endpoint. + * Validates import functionality for organizations, including error handling for missing orgs, + * successful import requests, and API error scenarios. + */ +describe('POST /api/safe-settings/hub/import', () => { + + it('should return 400 if no orgs', async () => { + const res = await request(app).post('/api/safe-settings/hub/import').send({}); + expect(res.statusCode).toBe(400); + expect(res.body.error).toMatch(/Missing orgs/); + }); + it('should process import with orgs', async () => { + axios.post.mockResolvedValueOnce({ data: { success: true } }); + const res = await request(app).post('/api/safe-settings/hub/import').send({ orgs: ['org1'] }); + expect([200, 201, 500]).toContain(res.statusCode); + }); + it('should handle API errors', async () => { + axios.post.mockRejectedValueOnce(new Error('API down')); + const res = await request(app).post('/api/safe-settings/hub/import').send({ orgs: ['org1'] }); + expect([500, 404]).toContain(res.statusCode); + }); +}); diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index 39aac216d..3e9954364 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -16,9 +16,9 @@ describe('Settings Tests', () => { let mockSubOrg let subOrgConfig - function createSettings(config) { + function createSettings (config) { const settings = new Settings(false, stubContext, mockRepo, config, mockRef, mockSubOrg) - return settings; + return settings } beforeEach(() => { @@ -51,7 +51,7 @@ repository: # A comma-separated list of topics to set on the repository topics: - frontend - `).toString('base64'); + `).toString('base64') mockOctokit.repos = { getContent: jest.fn().mockResolvedValue({ data: { content } }) } @@ -82,8 +82,6 @@ repository: } } - - mockRepo = { owner: 'test', repo: 'test-repo' } mockRef = 'main' mockSubOrg = 'frontend' @@ -264,14 +262,13 @@ repository: - frontend `) - }) it("Should load configMap for suborgs'", async () => { - //mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) + // mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) mockSubOrg = undefined settings = createSettings(stubConfig) - jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: "frontend", path: ".github/suborgs/frontend.yml" }]) + jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: 'frontend', path: '.github/suborgs/frontend.yml' }]) jest.spyOn(settings, 'loadYaml').mockImplementation(() => subOrgConfig) jest.spyOn(settings, 'getReposForTeam').mockImplementation(() => [{ name: 'repo-test' }]) jest.spyOn(settings, 'getSubOrgRepositories').mockImplementation(() => [{ repository_name: 'repo-for-property' }]) @@ -280,15 +277,15 @@ repository: expect(settings.loadConfigMap).toHaveBeenCalledTimes(1) // Get own properties of subOrgConfigs - const ownProperties = Object.getOwnPropertyNames(subOrgConfigs); + const ownProperties = Object.getOwnPropertyNames(subOrgConfigs) expect(ownProperties.length).toEqual(3) }) it("Should throw an error when a repo is found in multiple suborgs configs'", async () => { - //mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) + // mockSubOrg = jest.fn().mockReturnValue(['suborg1', 'suborg2']) mockSubOrg = undefined settings = createSettings(stubConfig) - jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: "frontend", path: ".github/suborgs/frontend.yml" }, { name: "backend", path: ".github/suborgs/backend.yml" }]) + jest.spyOn(settings, 'loadConfigMap').mockImplementation(() => [{ name: 'frontend', path: '.github/suborgs/frontend.yml' }, { name: 'backend', path: '.github/suborgs/backend.yml' }]) jest.spyOn(settings, 'loadYaml').mockImplementation(() => subOrgConfig) jest.spyOn(settings, 'getReposForTeam').mockImplementation(() => [{ name: 'repo-test' }]) jest.spyOn(settings, 'getSubOrgRepositories').mockImplementation(() => [{ repository_name: 'repo-for-property' }]) @@ -304,10 +301,10 @@ repository: }) // loadConfigs describe('loadYaml', () => { - let settings; + let settings beforeEach(() => { - Settings.fileCache = {}; + Settings.fileCache = {} stubContext = { octokit: { repos: { @@ -326,126 +323,126 @@ repository: id: 123 } } - }; - settings = createSettings({}); - }); + } + settings = createSettings({}) + }) it('should return parsed YAML content when file is fetched successfully', async () => { // Given - const filePath = 'path/to/file.yml'; - const content = Buffer.from('key: value').toString('base64'); + const filePath = 'path/to/file.yml' + const content = Buffer.from('key: value').toString('base64') jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: { content }, headers: { etag: 'etag123' } - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toEqual({ key: 'value' }); + expect(result).toEqual({ key: 'value' }) expect(Settings.fileCache[`${mockRepo.owner}/${filePath}`]).toEqual({ etag: 'etag123', data: { content } - }); - }); + }) + }) it('should return cached content when file has not changed (304 response)', async () => { // Given - const filePath = 'path/to/file.yml'; - const content = Buffer.from('key: value').toString('base64'); - Settings.fileCache[`${mockRepo.owner}/${filePath}`] = { etag: 'etag123', data: { content } }; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 304 }); + const filePath = 'path/to/file.yml' + const content = Buffer.from('key: value').toString('base64') + Settings.fileCache[`${mockRepo.owner}/${filePath}`] = { etag: 'etag123', data: { content } } + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 304 }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toEqual({ key: 'value' }); + expect(result).toEqual({ key: 'value' }) expect(settings.github.repos.getContent).toHaveBeenCalledWith( expect.objectContaining({ headers: { 'If-None-Match': 'etag123' } }) - ); - }); + ) + }) it('should not return cached content when the cache is for another org', async () => { // Given - const filePath = 'path/to/file.yml'; - const content = Buffer.from('key: value').toString('base64'); - const wrongContent = Buffer.from('wrong: content').toString('base64'); - Settings.fileCache['another-org/path/to/file.yml'] = { etag: 'etag123', data: { wrongContent } }; + const filePath = 'path/to/file.yml' + const content = Buffer.from('key: value').toString('base64') + const wrongContent = Buffer.from('wrong: content').toString('base64') + Settings.fileCache['another-org/path/to/file.yml'] = { etag: 'etag123', data: { wrongContent } } jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: { content }, headers: { etag: 'etag123' } - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toEqual({ key: 'value' }); + expect(result).toEqual({ key: 'value' }) }) it('should return null when the file path is a folder', async () => { // Given - const filePath = 'path/to/folder'; + const filePath = 'path/to/folder' jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: [] - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeNull(); - }); + expect(result).toBeNull() + }) it('should return null when the file is a symlink or submodule', async () => { // Given - const filePath = 'path/to/symlink'; + const filePath = 'path/to/symlink' jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ data: { content: null } - }); + }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeUndefined(); - }); + expect(result).toBeUndefined() + }) it('should handle 404 errors gracefully and return null', async () => { // Given - const filePath = 'path/to/nonexistent.yml'; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 404 }); + const filePath = 'path/to/nonexistent.yml' + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 404 }) // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeNull(); - }); + expect(result).toBeNull() + }) it('should throw an error for non-404 exceptions when not in nop mode', async () => { // Given - const filePath = 'path/to/error.yml'; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); + const filePath = 'path/to/error.yml' + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')) // When / Then - await expect(settings.loadYaml(filePath)).rejects.toThrow('Unexpected error'); - }); + await expect(settings.loadYaml(filePath)).rejects.toThrow('Unexpected error') + }) it('should log and append NopCommand for non-404 exceptions in nop mode', async () => { // Given - const filePath = 'path/to/error.yml'; - settings.nop = true; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); - jest.spyOn(settings, 'appendToResults'); + const filePath = 'path/to/error.yml' + settings.nop = true + jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')) + jest.spyOn(settings, 'appendToResults') // When - const result = await settings.loadYaml(filePath); + const result = await settings.loadYaml(filePath) // Then - expect(result).toBeUndefined(); + expect(result).toBeUndefined() expect(settings.appendToResults).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ @@ -455,7 +452,7 @@ repository: }) }) ]) - ); - }); - }); + ) + }) + }) }) // Settings Tests diff --git a/ui/README.md b/ui/README.md index 4f3cc28d3..e8de83917 100644 --- a/ui/README.md +++ b/ui/README.md @@ -38,3 +38,32 @@ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/bui This directory contains example API routes for the headless API app. For more details, see [route.js file convention](https://nextjs.org/docs/app/api-reference/file-conventions/route). + +--- + +```mermaid + +sequenceDiagram + participant User + participant OrganizationsTable.jsx + participant HubOrgGraph.jsx + participant Next.js API Proxy + participant Backend (Express) + participant GitHub API + + User->>OrganizationsTable.jsx: Loads Organization page + OrganizationsTable.jsx->>Next.js API Proxy: GET /api/safe-settings/installation + Next.js API Proxy->>Backend (Express): GET /api/safe-settings/installation + Backend (Express)->>GitHub API: Fetch org installations, repo status, commit info, sync status + GitHub API-->>Backend (Express): Returns org data + Backend (Express)-->>Next.js API Proxy: Returns installations array + Next.js API Proxy-->>OrganizationsTable.jsx: Returns installations array + OrganizationsTable.jsx->>HubOrgGraph.jsx: Passes org data (hasConfigRepo, isInSync) + HubOrgGraph.jsx->>Next.js API Proxy: (if fetching own data) GET /api/safe-settings/installation + Next.js API Proxy->>Backend (Express): GET /api/safe-settings/installation + Backend (Express)->>GitHub API: (repeat fetch if needed) + GitHub API-->>Backend (Express): Returns org data + Backend (Express)-->>Next.js API Proxy: Returns installations array + Next.js API Proxy-->>HubOrgGraph.jsx: Returns installations array + User->>OrganizationsTable.jsx: Interacts with table/graph (tooltips, legend, etc.) +``` \ No newline at end of file diff --git a/ui/favico.ico b/ui/favico.ico deleted file mode 100644 index bf67019abb1784abbeba87886cf3e5769b53534c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ diff --git a/ui/favico.png b/ui/favico.png deleted file mode 100644 index bf67019abb1784abbeba87886cf3e5769b53534c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ diff --git a/ui/favicon.svg b/ui/favicon.svg new file mode 100644 index 000000000..17791cf9b --- /dev/null +++ b/ui/favicon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/next.config.js b/ui/next.config.js index d5d2d2c15..428ac9ab8 100644 --- a/ui/next.config.js +++ b/ui/next.config.js @@ -5,6 +5,15 @@ const nextConfig = { eslint: { ignoreDuringBuilds: true, }, + async redirects() { + return [ + { + source: '/', + destination: '/dashboard', + permanent: false, + }, + ]; + }, }; module.exports = nextConfig; diff --git a/ui/package-lock.json b/ui/package-lock.json index 8ab1771e6..9f7381e78 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,7 +10,11 @@ "dependencies": { "@primer/octicons-react": "^19.15.5", "bootstrap": "^5.3.7", - "next": "15.4.7" + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "next": "^15.4.7", + "swr": "^2.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1487,6 +1491,69 @@ "node": ">= 8" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1609,6 +1676,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -4619,6 +4695,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwindcss": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", @@ -4917,6 +5006,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index bc0097754..cfa48ab9d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack --port 3001", "build": "next build", "export": "next export", "start": "next start" @@ -11,7 +11,11 @@ "dependencies": { "@primer/octicons-react": "^19.15.5", "bootstrap": "^5.3.7", - "next": "15.4.7" + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "next": "^15.4.7", + "swr": "^2.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg index 7762395be..17791cf9b 100644 --- a/ui/public/favicon.svg +++ b/ui/public/favicon.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/public/shield.png b/ui/public/shield.png deleted file mode 100644 index a8e4b6ec7bcbde72dcd715b6269b22e48c668976..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24289 zcmXtgV|-oT^Yy*4bz?TRjmEaq*iIVTjcu!8o1|f5+qTWdcJkcs@AH4&oO9mY*?acP znl)?ojZ{{YLPo$x0002UGScFz000md{9OSD1Acb=3f=?1fG(<1qJXMN!V>`C3qVF( zMBUTitOMSYP%BmFrLC#Fbs>qN`uh7&Em1Y8nG^Px=)dxTc7K3jc#U2TxEY_p#@}EL z{kD8GH+|SiLr4jrdhv0krIA8-;7M7`$Y1|4Y(6R^oqMWy$o;!0_qiP7Z7N?#YEW-7 z(~YcnJ4t3(=xF*kUul?s?*WxWo+vC2uXnR>itj8tff5Bxt$Vy)fAMQ;bbv4`$2~?? zn6T(EcimkczP;=Dg*9%C1E;F38+Uxe=W2U*!{@OTx@1Y3miy`abuos%vD)YUV-2-G zhmH3fH%S)NY1Ykep)oL64Fp?i1QXgowe13%daXK9pV`eIIWX-l-Ld0SW-Mg#`-gcxm_?|I^CNGUY_Kli@*MoLIa_CgL+nL?n+PGp^sh$x3oxUPY%!7d4Q

    xNbM`PG=BC2pI)-gc#?ujEY|nz)%svQ;+C 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 M07*qoM6N<$g22I`vj6}9 diff --git a/ui/shield.png b/ui/shield.png index bf67019abb1784abbeba87886cf3e5769b53534c..93d8476ff0cf5d83c97b703afaffa0921f446cae 100644 GIT binary patch literal 4883 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 literal 3961 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 T%E8+J00000NkvXXu0mjfmfmB+ diff --git a/ui/shield.svg b/ui/shield.svg new file mode 100644 index 000000000..17791cf9b --- /dev/null +++ b/ui/shield.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx index fd7eedd15..aab69cfe1 100644 --- a/ui/src/app/components/EnvVariables.jsx +++ b/ui/src/app/components/EnvVariables.jsx @@ -19,7 +19,7 @@ export default function EnvVariables() { const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - fetch(`/api/settings/env${includeInfra ? '?includeInfra=true' : ''}`) + fetch(`/api/safe-settings/app/env${includeInfra ? '?includeInfra=true' : ''}`) .then(r => { if (!r.ok) { throw new Error(`Unable to retrieve environment variables (HTTP ${r.status}). Please try again later.`); @@ -98,23 +98,7 @@ export default function EnvVariables() { setSearch(e.target.value)} /> -

    - -
    - setIncludeInfra(e.target.checked)} /> - -
    -
    - setRevealAll(e.target.checked)} /> - -
    -
    -
    -
    - - -
    -
    + {/* Removed options and buttons section for a cleaner environment page UI */} {loading &&
    Loading…
    } diff --git a/ui/src/app/components/HubOrgGraph.jsx b/ui/src/app/components/HubOrgGraph.jsx new file mode 100644 index 000000000..bd3b42e3b --- /dev/null +++ b/ui/src/app/components/HubOrgGraph.jsx @@ -0,0 +1,140 @@ +'use client'; +import { useEffect, useRef } from "react"; +import useSWR from "swr"; + +const fetcher = (...args) => fetch(...args).then(res => res.json()); + +export default function HubOrgGraph({ width = 640, height = 320 }) { + const vizRef = useRef(null); + const { data, error } = useSWR("/api/safe-settings/installation", fetcher); + const orgs = Array.isArray(data?.installations) + ? data.installations.filter(i => i.type === "Organization") + : []; + const orgCount = orgs.length; + + useEffect(() => { + if (typeof window === "undefined" || !data) return; + Promise.all([ + import("d3-selection"), + import("d3-force"), + import("d3-drag") + ]).then(([d3Selection, d3Force, d3Drag]) => { + const select = d3Selection.select; + const forceSimulation = d3Force.forceSimulation; + const forceLink = d3Force.forceLink; + const forceManyBody = d3Force.forceManyBody; + const forceCenter = d3Force.forceCenter; + const drag = d3Drag.drag; + // Dynamic graph data: 1 HUB, N ORGs + const nodes = [ { id: "Hub", group: 1, label: "Hub", color: "#0a2540" } ]; + if (orgs.length > 0) { + orgs.forEach((org, i) => { + const orgKey = org.account; + const hasConfigRepo = org.hasConfigRepo === true; + nodes.push({ id: orgKey, group: 2, label: "ORG", color: hasConfigRepo ? "#2ea44f" : "#6a737d", tooltip: org.account }); + }); + } else { + for (let i = 1; i <= orgCount; i++) { + nodes.push({ id: `ORG${i}`, group: 2, label: "ORG", color: "#6a737d", tooltip: `ORG${i}` }); + } + } + const links = []; + if (orgs.length > 0) { + orgs.forEach((org, i) => { + const orgKey = org.account; + links.push({ source: "Hub", target: orgKey }); + }); + } else { + for (let i = 1; i <= orgCount; i++) { + links.push({ source: "Hub", target: `ORG${i}` }); + } + } + select(vizRef.current).selectAll("svg").remove(); + const svg = select(vizRef.current) + .append("svg") + .attr("width", width) + .attr("height", height); + const simulation = forceSimulation(nodes) + .force("link", forceLink(links).id(d => d.id).distance(120)) + .force("charge", forceManyBody().strength(-400)) + .force("center", forceCenter(width / 2, height / 2)); + const link = svg.append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line") + .attr("stroke-width", 2); + const node = svg.append("g") + .attr("stroke", "#fff") + .attr("stroke-width", 2) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("r", 24) + .attr("fill", d => d.group === 1 ? d.color : d.color || "#6f42c1") + .call(drag() + .on("start", (event, d) => { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; d.fy = d.y; + }) + .on("drag", (event, d) => { + d.fx = event.x; d.fy = event.y; + }) + .on("end", (event, d) => { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; d.fy = null; + }) + ); + node.append("title") + .text(d => d.group === 2 ? d.tooltip : "Hub"); + const label = svg.append("g") + .selectAll("text") + .data(nodes) + .join("text") + .attr("text-anchor", "middle") + .attr("dy", ".35em") + .attr("font-size", 16) + .attr("font-family", "sans-serif") + .attr("fill", d => d.group === 1 ? "#fff" : "#fff") + .text(d => d.label) + .each(function(d) { + d3Selection.select(this) + .append("title") + .text(d.group === 2 ? d.tooltip : "Hub"); + }); + simulation.on("tick", () => { + link + .attr("x1", d => d.source.x) + .attr("y1", d => d.source.y) + .attr("x2", d => d.target.x) + .attr("y2", d => d.target.y); + node + .attr("cx", d => d.x) + .attr("cy", d => d.y); + label + .attr("x", d => d.x) + .attr("y", d => d.y); + }); + }); + }, [width, height, orgCount, data]); + + if (error) return
    Error loading organization graph.
    ; + if (!data) return
    Loading organization graph...
    ; + + return ( +
    +
    +
    + + + Has safe-settings admin repo + + + + No safe-settings admin repo + +
    +
    + ); +} diff --git a/ui/src/app/components/OrganizationsTable.jsx b/ui/src/app/components/OrganizationsTable.jsx index 56bd12c41..f6e394ae5 100644 --- a/ui/src/app/components/OrganizationsTable.jsx +++ b/ui/src/app/components/OrganizationsTable.jsx @@ -1,73 +1,117 @@ -'use client'; +"use client"; -import React, { useState, useMemo, useEffect } from 'react'; -import { ChevronUpIcon, ChevronDownIcon, SearchIcon } from '@primer/octicons-react'; -import { useHydrated } from '../hooks/useHydrated'; +import React, { useState, useMemo, useEffect, useRef } from "react"; +import { + ChevronUpIcon, + ChevronDownIcon, + SearchIcon, + InfoIcon, +} from "@primer/octicons-react"; +import { useHydrated } from "../hooks/useHydrated"; -// Mock organizations used when /api/organizations returns 404 +// Mock organizations used when /api/safe-settings/installation returns 404 const MOCK_ORGS = [ - { id: 1, name: 'mock-org-one', lastSyncDate: new Date(Date.now() - 3600 * 1000).toISOString(), lastSyncMessage: 'Initial mock sync', lastSyncSha: 'abcdef1', ageSeconds: 3600 }, - { id: 2, name: 'example-inc', lastSyncDate: new Date(Date.now() - 7200 * 1000).toISOString(), lastSyncMessage: 'Second mock sync', lastSyncSha: 'abcdef2', ageSeconds: 7200 }, - { id: 3, name: 'demo-labs', lastSyncDate: null, lastSyncMessage: null, lastSyncSha: null, ageSeconds: null, na: true } + { + id: 1, + name: "mock-org-one", + lastSyncDate: new Date(Date.now() - 3600 * 1000).toISOString(), + lastSyncMessage: "Initial mock sync", + lastSyncSha: "abcdef1", + ageSeconds: 3600, + }, + { + id: 2, + name: "example-inc", + lastSyncDate: new Date(Date.now() - 7200 * 1000).toISOString(), + lastSyncMessage: "Second mock sync", + lastSyncSha: "abcdef2", + ageSeconds: 7200, + }, + { + id: 3, + name: "demo-labs", + lastSyncDate: null, + lastSyncMessage: null, + lastSyncSha: null, + ageSeconds: null, + na: true, + }, ]; const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, setSearchTerm] = useState(""); const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [fetched, setFetched] = useState([]); const hydrated = useHydrated(); + const [selectedIds, setSelectedIds] = useState(() => new Set()); + const headerCheckboxRef = useRef(null); + const [retrievingFiles, setRetrievingFiles] = useState(false); + const [retrieveMessage, setRetrieveMessage] = useState(null); + const [retrieveError, setRetrieveError] = useState(null); + const [retrieveResults, setRetrieveResults] = useState(null); // Fetch real organizations from backend API on client hydration useEffect(() => { if (!hydrated) return; // avoid SSR mismatch let cancelled = false; setLoading(true); - fetch('/api/organizations') - .then(r => { + + fetch("/api/safe-settings/installation") + .then((r) => { if (!r.ok) { - throw new Error(`Unable to retrieve organizations (HTTP ${r.status}). Please try again later.`); + throw new Error( + `Unable to retrieve organizations (HTTP ${r.status}). Please try again later.` + ); } return r.json(); }) - .then(json => { + .then((json) => { if (!json || cancelled) return; - const lastCommits = json.lastCommits || {} - const mapped = (json.installations || []).map(i => { - const lc = lastCommits[i.account]; - return { - id: i.id, - name: i.account, - lastSyncDate: lc && lc.committed_at ? lc.committed_at : null, - lastSyncSha: lc && lc.sha ? lc.sha : null, - lastSyncMessage: lc && lc.message ? lc.message : null, - ageSeconds: lc && typeof lc.age_seconds === 'number' ? lc.age_seconds : null, - na: lc && lc.na === true - }; - }); + const mapped = (json.installations || []).map((i) => ({ + id: i.id, + name: i.account, + lastSyncDate: i.committed_at || null, + lastSyncSha: i.sha || null, + lastSyncMessage: i.message || null, + ageSeconds: typeof i.age_seconds === "number" ? i.age_seconds : null, + hasConfigRepo: + typeof i.hasConfigRepo === "boolean" ? i.hasConfigRepo : false, + isInSync: typeof i.isInSync === "boolean" ? i.isInSync : false, + })); setFetched(mapped); setError(null); }) - .catch(e => { if (!cancelled) setError(e.message); }) - .finally(() => { if (!cancelled) setLoading(false); }); - return () => { cancelled = true; }; + .catch((e) => { + if (!cancelled) setError(e.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; }, [hydrated]); - const data = fetched.length > 0 ? fetched : (propOrganizations.length > 0 ? propOrganizations : []); + const data = + fetched.length > 0 + ? fetched + : propOrganizations.length > 0 + ? propOrganizations + : []; // Format date for display with hydration-safe approach const formatLastSync = (org) => { - if (org.na) return NA; if (!org.lastSyncDate) return ; const dateObj = new Date(org.lastSyncDate); let ageSec = org.ageSeconds; - if (hydrated && (ageSec == null)) { + if (hydrated && ageSec == null) { ageSec = Math.floor((Date.now() - dateObj.getTime()) / 1000); } const rel = (() => { - if (ageSec == null) return ''; - if (ageSec < 60) return '0m'; + if (ageSec == null) return ""; + if (ageSec < 60) return "0m"; const mTotal = Math.floor(ageSec / 60); if (mTotal < 60) return `${mTotal}m`; const hTotal = Math.floor(mTotal / 60); @@ -79,17 +123,35 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { const remH = hTotal % 24; return remH ? `${dTotal}d ${remH}h` : `${dTotal}d`; })(); - const fullStamp = `${dateObj.getFullYear()}-${String(dateObj.getMonth()+1).padStart(2,'0')}-${String(dateObj.getDate()).padStart(2,'0')} ${String(dateObj.getHours()).padStart(2,'0')}:${String(dateObj.getMinutes()).padStart(2,'0')}:${String(dateObj.getSeconds()).padStart(2,'0')}`; - const tooltip = [fullStamp, org.lastSyncMessage, org.lastSyncSha ? `SHA: ${org.lastSyncSha.slice(0,7)}` : null] + const fullStamp = `${dateObj.getFullYear()}-${String( + dateObj.getMonth() + 1 + ).padStart(2, "0")}-${String(dateObj.getDate()).padStart(2, "0")} ${String( + dateObj.getHours() + ).padStart(2, "0")}:${String(dateObj.getMinutes()).padStart( + 2, + "0" + )}:${String(dateObj.getSeconds()).padStart(2, "0")}`; + const tooltip = [ + fullStamp, + org.lastSyncMessage, + org.lastSyncSha ? `SHA: ${org.lastSyncSha.slice(0, 7)}` : null, + ] .filter(Boolean) - .join('\n'); - return {rel}; + .join("\n"); + return ( + + {rel} + + ); + }; + const lastSyncColStyle = { + width: "170px", + fontVariantNumeric: "tabular-nums", }; - const lastSyncColStyle = { width: '170px', fontVariantNumeric: 'tabular-nums' }; // Filter organizations based on search term const filteredData = useMemo(() => { - return data.filter(org => + return data.filter((org) => org.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [data, searchTerm]); @@ -103,16 +165,16 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { let bValue = b[sortConfig.key]; // Convert dates to timestamps for comparison - if (sortConfig.key === 'lastSyncDate') { + if (sortConfig.key === "lastSyncDate") { aValue = new Date(aValue).getTime(); bValue = new Date(bValue).getTime(); } if (aValue < bValue) { - return sortConfig.direction === 'asc' ? -1 : 1; + return sortConfig.direction === "asc" ? -1 : 1; } if (aValue > bValue) { - return sortConfig.direction === 'asc' ? 1 : -1; + return sortConfig.direction === "asc" ? 1 : -1; } return 0; }); @@ -120,30 +182,99 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { // Handle column sorting const handleSort = (key) => { - setSortConfig(prevConfig => { + setSortConfig((prevConfig) => { if (prevConfig.key === key) { - if (prevConfig.direction === 'asc') { - return { key, direction: 'desc' }; - } else if (prevConfig.direction === 'desc') { + if (prevConfig.direction === "asc") { + return { key, direction: "desc" }; + } else if (prevConfig.direction === "desc") { return { key: null, direction: null }; } } - return { key, direction: 'asc' }; + return { key, direction: "asc" }; }); }; // Render sort icon const renderSortIcon = (columnKey) => { if (sortConfig.key !== columnKey) { - return ; + return ( + + ↕ + + ); } - if (sortConfig.direction === 'asc') { + if (sortConfig.direction === "asc") { return ; } - if (sortConfig.direction === 'desc') { + if (sortConfig.direction === "desc") { return ; } - return ; + return ( + + ↕ + + ); + }; + + // Keep header checkbox indeterminate when some but not all rows are selected + useEffect(() => { + if (!headerCheckboxRef || !headerCheckboxRef.current) return; + const selectableCount = sortedData.filter((o) => !o.synced).length; + headerCheckboxRef.current.indeterminate = + selectedIds.size > 0 && selectedIds.size < selectableCount; + }, [selectedIds, sortedData]); + + // Prune selection when the displayed dataset changes (remove ids that no longer exist) + useEffect(() => { + setSelectedIds((prev) => { + const allowed = new Set( + sortedData.filter((o) => !o.synced).map((o) => o.id) + ); + const next = new Set([...prev].filter((id) => allowed.has(id))); + if (next.size === prev.size) return prev; + return next; + }); + }, [sortedData]); + + // Retrieve files for selected organizations + const retrieveFilesForSelected = async () => { + if (selectedIds.size === 0) return; + // map selected ids back to organization names using the current sorted/filtered dataset + const orgNames = sortedData + .filter((o) => selectedIds.has(o.id)) + .map((o) => o.name); + if (orgNames.length === 0) return; + setRetrieveResults(null); + setRetrieveMessage(null); + setRetrieveError(null); + setRetrievingFiles(true); + try { + const res = await fetch("/api/safe-settings/hub/import", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ orgs: orgNames }), + }); + if (!res.ok) throw new Error(`Request failed (HTTP ${res.status})`); + const json = await res.json().catch(() => ({})); + if (Array.isArray(json.results)) { + setRetrieveResults(json.results); + const created = json.results.filter((r) => r.pr).length; + const skipped = json.results.filter((r) => r.skipped).map((r) => r.org); + const errors = json.results.filter((r) => r.error).length; + const parts = []; + if (created) + parts.push(`${created} PR${created > 1 ? "s" : ""} created`); + if (skipped.length) parts.push(`Skipped: ${skipped.join(", ")}`); + if (errors) parts.push(`${errors} error${errors > 1 ? "s" : ""}`); + setRetrieveMessage(parts.join(" • ") || "Retrieval completed"); + } else { + setRetrieveMessage(json.message || "Retrieval requested"); + } + } catch (e) { + setRetrieveError(e.message || String(e)); + } finally { + setRetrievingFiles(false); + } }; return ( @@ -166,67 +297,263 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => {
    - - Showing {sortedData.length} of {data.length} organizations - +
    + + + + +
    + {/* Reserved message area: keeps layout stable when messages appear */} +
    + {retrieveResults ? ( + retrieveResults.map((r) => ( +
    + {r.pr ? ( +
    + Imported {r.org}:{" "} + + {r.pr} + +
    + ) : r.skipped ? ( +
    + Skipping {r.org}: already present in hub +
    + ) : r.error ? ( +
    + {r.org}: {r.error} +
    + ) : null} +
    + )) + ) : ( + <> + {retrieveMessage && ( +
    {retrieveMessage}
    + )} + {retrieveError && ( +
    {retrieveError}
    + )} + + )} +
    + {/* Table */}
    - + + + {loading && ( - + )} {!loading && error && ( - + )} - {!loading && !error && sortedData.length > 0 ? ( - sortedData.map((org) => ( - - - - - )) - ) : ( - !loading && !error && ( - - - - ) - )} + {!loading && !error && sortedData.length > 0 + ? sortedData.map((org) => { + return ( + + + + + + + + ); + }) + : !loading && + !error && ( + + + + )}
    handleSort('name')} + + {/* compute selectable rows so header/select-all ignores already-imported orgs */} + { + const selectableCount = sortedData.filter( + (o) => !o.synced + ).length; + return ( + selectableCount > 0 && + selectedIds.size === selectableCount + ); + }, [sortedData, selectedIds])} + onChange={() => { + // toggle all selectable (non-synced) rows + setSelectedIds((prev) => { + const selectable = sortedData + .filter((o) => !o.synced) + .map((o) => o.id); + if (prev.size === selectable.length) return new Set(); + return new Set(selectable); + }); + }} + aria-label="Select all organizations" + /> + handleSort("name")} + > +
    +
    Organization Name
    +
    {renderSortIcon("name")}
    + + Showing {sortedData.length} of {data.length} organizations + +
    +
    - Organization Name - {renderSortIcon('name')} + Config Repo + + In Sync handleSort('lastSyncDate')} + style={{ cursor: "pointer", ...lastSyncColStyle }} + onClick={() => handleSort("lastSyncDate")} > - Last Safe-settings Sync - {renderSortIcon('lastSyncDate')} + Last Sync + {renderSortIcon("lastSyncDate")}
    Loading organizations… + Loading organizations… +
    Error: {error} +
    + {error} +
    +
    - {org.name} - - {formatLastSync(org)} -
    - {searchTerm ? `No organizations found matching "${searchTerm}"` : 'No organizations available'} -
    + + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(org.id)) next.delete(org.id); + else next.add(org.id); + return next; + }) + } + aria-label={`Select ${org.name}`} + disabled={org.synced === true} + style={ + org.synced + ? { opacity: 0.45, cursor: "not-allowed" } + : {} + } + /> + + {org.name} + {org.synced && ( + Imported + )} + + {org.hasConfigRepo ? ( + + ✓ + + ) : ( + + NA + + )} + + {org.isInSync ? ( + + ✓ + + ) : ( + + ✗ + + )} + + {formatLastSync(org)} +
    + {searchTerm + ? `No organizations found matching "${searchTerm}"` + : "No organizations available"} +
    @@ -238,8 +565,11 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { {searchTerm && `Filtered by: "${searchTerm}"`} {sortConfig.key && ( - • Sorted by: {sortConfig.key === 'name' ? 'Organization Name' : 'Last Safe-settings Sync'} - ({sortConfig.direction === 'asc' ? 'A-Z' : 'Z-A'}) + • Sorted by:{" "} + {sortConfig.key === "name" + ? "Organization Name" + : "Last Safe-settings Sync"} + ({sortConfig.direction === "asc" ? "A-Z" : "Z-A"}) )} diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx index 3a40154c2..ada553614 100644 --- a/ui/src/app/components/Safe-settings-hubContent.jsx +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -6,18 +6,6 @@ import { useHydrated } from '../hooks/useHydrated'; // Match the left index width and reuse for the search input const LEFT_COL_WIDTH = 320; -const MOCK_TREE = { - name: '.github', - path: '.github', - type: 'dir', - lastCommitAt: new Date().toISOString(), - entries: [ - { name: 'CODEOWNERS', path: '.github/CODEOWNERS', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'add CODEOWNERS' }, - { name: 'workflows', path: '.github/workflows', type: 'dir', lastCommitAt: new Date().toISOString(), entries: [ - { name: 'ci.yml', path: '.github/workflows/ci.yml', type: 'file', lastCommitAt: new Date().toISOString(), lastCommitMessage: 'ci: add' } - ] } - ] -}; export default function SafeSettingsHubContent3b() { const hydrated = useHydrated(); @@ -32,13 +20,13 @@ export default function SafeSettingsHubContent3b() { const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - fetch('/api/safe-settings-hub/content?fetchContent=true') + fetch('/api/safe-settings/hub/content?fetchContent=true') .then(r => { if (!r.ok) throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status})`); return r.json(); }) .then(json => { setRootTree(json); setLastFetchedAt(new Date()); }) - .catch(() => setRootTree(MOCK_TREE)) + .catch((error) => { setError("Unable to load content. Please try again later."); setRootTree(null); }) .finally(() => setLoading(false)); }; @@ -48,10 +36,16 @@ export default function SafeSettingsHubContent3b() { if (!node) return null; const term = search.toLowerCase(); const matches = (n) => !term || (n.name && n.name.toLowerCase().includes(term)) || (n.path && n.path.toLowerCase().includes(term)); - if (node.type === 'file') return matches(node) ? node : null; + if (node.type === 'file') { + return matches(node) ? node : null; + } if (node.type === 'dir') { - const children = (node.entries || []).map(filterTree).filter(Boolean); - if (matches(node) || children.length) return { ...node, entries: children }; + const filteredEntries = (node.entries || []) + .map(child => filterTree(child)) + .filter(Boolean); + if (matches(node) || filteredEntries.length > 0) { + return { ...node, entries: filteredEntries }; + } return null; } return null; diff --git a/ui/src/app/components/TitleBar.css b/ui/src/app/components/TitleBar.css index d10a7ea76..df856eff7 100644 --- a/ui/src/app/components/TitleBar.css +++ b/ui/src/app/components/TitleBar.css @@ -4,7 +4,7 @@ .title-header { background: #333; color: #fff; - min-height: 60px; /* Ensure consistent height */ + min-height: 40px; /* Ensure consistent height */ } /* Theme-specific header styles */ @@ -22,7 +22,7 @@ body.dark-theme .title-header { /* Navigation bar - consistent height and styling */ .title-nav { - min-height: 48px; /* Consistent nav height */ + min-height: 40px; /* Consistent nav height */ border-bottom: 1px solid var(--border-color, #dee2e6) !important; background: var(--bg-secondary, #f6f8fa); /* Default light theme background */ } @@ -102,7 +102,7 @@ body.dark-theme .nav-link-custom { /* Navigation menu items */ .nav-link.menu-hover { border-radius: 5px !important; - margin: 10px 10px 10px 10px !important; + margin: 10px 10px 9px 10px !important; padding: 5px 10px !important; transition: background 0.15s, color 0.15s; border: 1px solid transparent !important; /* Invisible border to maintain box model */ diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index 818f4a356..f24b10862 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -1,9 +1,16 @@ "use client"; import { usePathname } from "next/navigation"; import React from "react"; -import { GlobeIcon, GearIcon, ListUnorderedIcon, SunIcon, MoonIcon } from "@primer/octicons-react"; -import { useTheme } from './ThemeContext'; -import './TitleBar.css'; +import { + GlobeIcon, + GearIcon, + ListUnorderedIcon, + SunIcon, + MoonIcon, + NoteIcon, +} from "@primer/octicons-react"; +import { useTheme } from "./ThemeContext"; +import "./TitleBar.css"; export default function TitleBar() { const pathname = usePathname(); @@ -12,31 +19,52 @@ export default function TitleBar() { // Always render the TitleBar structure to prevent layout shift return ( <> -
    +
    - - + + - Safe-Settings Dashboard + + Safe-Settings Hub Dashboard + - +
    + +
    diff --git a/ui/src/app/dashboard/help/page.jsx b/ui/src/app/dashboard/help/page.jsx new file mode 100644 index 000000000..866aa7fae --- /dev/null +++ b/ui/src/app/dashboard/help/page.jsx @@ -0,0 +1,35 @@ +'use client'; + +import TitleBar from "../../components/TitleBar"; +import Link from "next/link"; +import HubOrgGraph from "../../components/HubOrgGraph"; + +export default function HelpPage() { + return ( +
    + +
    +

    Dashboard & Hub - Help

    +

    Quick guidance for the Safe-Settings Dashboard and Hub.

    + +

    +

    What is the Safe-Settings Dashboard

    +

    + This UI provides status information for the Safe-Settings Hub feature. It is a read-first reporting and status tool that displays configuration state and import/sync status. +

    +

    How to get started

    +

    + The Organizations page lists every Org where the Safe-Settings Hub is installed. You can use the Retrieve Settings button to perform an initial import from the selected organizations' config repositories. It reads files from the configured CONFIG_PATH in each organization's config repo and commits them into a single branch in the hub repository, then opens a pull request for review. This is intended for initial population or one-time imports — the action will skip organizations that already have content in the hub path. +

    +

    How to edit configuration

    +

    + The dashboard is not a content editor. To change configuration you should edit files in your admin repository and follow the normal GitHub workflow: commit changes, open a pull request, get required approvers to review, and merge. After the PR is merged the dashboard will reflect the updated state. +

    +
    +

    + If you need more help, check the repository documentation or contact the maintainers. +

    +
    +
    + ); +} diff --git a/ui/src/app/dashboard/organizations/page.jsx b/ui/src/app/dashboard/organizations/page.jsx index e5712bf0e..596f9299f 100644 --- a/ui/src/app/dashboard/organizations/page.jsx +++ b/ui/src/app/dashboard/organizations/page.jsx @@ -11,7 +11,7 @@ export default function OrganizationsPage() { Organizations

    - List all the installations of the App and the last time Safe-settings configurations were synced. + List all the Organizations where the Safe-Settings App is installed and the last time Safe-settings configurations were synced.

    diff --git a/ui/src/app/dashboard/page.jsx b/ui/src/app/dashboard/page.jsx index 92cd1ac12..7e4787fc5 100644 --- a/ui/src/app/dashboard/page.jsx +++ b/ui/src/app/dashboard/page.jsx @@ -1,4 +1,5 @@ import TitleBar from "../components/TitleBar"; +import { AlertIcon, ArrowRightIcon, CheckCircleIcon, GitCommitIcon, GitPullRequestIcon, GitMergeIcon, EyeIcon } from "@primer/octicons-react"; export default function DashboardPage() { return ( @@ -7,6 +8,17 @@ export default function DashboardPage() {

    Welcome to the Safe-Settings Hub Dashboard

    Select a menu item above to get started.

    +

    + This dashboard is a read-first reporting interface that displays configuration state and sync activity status for the Safe-Settings Hub.
    +
    It is not intended as the workflow for editing Safe-Settings Hub configuration content.

    + +
    To make changes, please use the standard GitHub process for content updates:


    + Commit        + Pull Request        + Approve        + Merge         + +

    ); diff --git a/ui/src/app/dashboard/safe-settings-hub/page.jsx b/ui/src/app/dashboard/safe-settings-hub/page.jsx index a8bbe5810..56a4ef355 100644 --- a/ui/src/app/dashboard/safe-settings-hub/page.jsx +++ b/ui/src/app/dashboard/safe-settings-hub/page.jsx @@ -12,7 +12,7 @@ export default function SafeSettingsHubConfigPage() {

    Listing files maintained by the Safe-Settings Global configuration (all ORG's). - Files are retrieved from `/api/safe-settings-hub/content`. + Files are retrieved from `/api/safe-settings/hub/content`.


    diff --git a/ui/src/app/dashboard/settings/page.jsx b/ui/src/app/dashboard/settings/page.jsx deleted file mode 100644 index e63d76620..000000000 --- a/ui/src/app/dashboard/settings/page.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import TitleBar from "../../components/TitleBar"; - -export default function SettingsPage() { - return ( -
    - -
    -

    Settings

    -

    Settings options will go here.

    -
    -
    - ); -} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index 46564ec39..09a592e7c 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -43,7 +43,7 @@ body.dark-theme { --bg-secondary: #444444; --bg-accent: #30363d; --text-primary: #f0f6fc; - --text-secondary: #e3e3e3; + --text-secondary: #b3b3b3; } /* Global Theme Styles */ @@ -56,32 +56,32 @@ body { /* Theme-specific body styles using data-theme */ [data-theme="light"] body, body.light-theme { - background: #fff; - color: #24292f; + background: #fff !important; + color: var(--text-primary) !important; } [data-theme="dark"] body, body.dark-theme { - background: rgb(45, 46, 47); - color: #f6f8fa; + background: rgb(24, 24, 24) !important; + color: var(--text-primary) !important; } /* Global Main Element Theme */ [data-theme="light"] main, body.light-theme main { - background: #fff; - color: #24292f; + background: #fff !important; + color: var(--text-primary) !important; } [data-theme="dark"] main, body.dark-theme main { /* background: #161b22; */ - color: #f6f8fa; + color: var(--text-primary) !important; } [data-theme="light"] .nav-link, body.light-theme .nav-link { - color: #24292f; + color: var(--text-primary) !important; } [data-theme="dark"] .nav-link, @@ -106,7 +106,7 @@ body.light-theme .nav-tabs { main { color: var(--text-primary) !important; /* padding: 1rem; */ - border-radius: 12px; + border-radius: 12px !important; /* margin-top: 1rem; */ } @@ -147,11 +147,11 @@ main { /* Global Font Utility Classes */ .dark-font { - color: #f6f8fa; + color: var(--text-primary) !important; } .light-font { - color: #24292f; + color: var(--text-primary) !important; } /* Organizations Table Styles */ diff --git a/ui/src/app/route.js b/ui/src/app/route.js deleted file mode 100644 index af7296c8e..000000000 --- a/ui/src/app/route.js +++ /dev/null @@ -1,7 +0,0 @@ -const { NextResponse } = require('next/server'); - -export async function GET() { - return NextResponse.json({ message: 'Hello world!' }); -} - -export const dynamic = 'force-static'; From dad3fe837ead99e862946d9f3aad899e0542a074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Wed, 24 Sep 2025 15:19:09 -0400 Subject: [PATCH 04/22] Add ui screen --- hubSyncHandler.log | 754 +++++++++++++++++++++++++++++ lib/hubSyncHandler.js | 492 ++++++++++--------- lib/routes.js | 48 ++ package-lock.json | 41 +- package.json | 2 +- safe-settings.log | 54 +++ ui/public/favicon.ico | 0 ui/src/app/api/logs/route.js | 28 ++ ui/src/app/components/TitleBar.jsx | 46 +- ui/src/app/dashboard/logs/page.jsx | 110 +++++ ui/src/app/globals.css | 8 + 11 files changed, 1306 insertions(+), 277 deletions(-) create mode 100644 hubSyncHandler.log create mode 100644 safe-settings.log create mode 100644 ui/public/favicon.ico create mode 100644 ui/src/app/api/logs/route.js create mode 100644 ui/src/app/dashboard/logs/page.jsx diff --git a/hubSyncHandler.log b/hubSyncHandler.log new file mode 100644 index 000000000..84e9592ce --- /dev/null +++ b/hubSyncHandler.log @@ -0,0 +1,754 @@ +2025-09-11T15:47:52.340Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-11T15:47:52.339Z [INFO] Received 'pull_request.closed' event: 47 +2025-09-11T15:47:52.636Z [INFO] Files changed in PR #47: .github/safe-settings/globals/suborg.yml +2025-09-11T15:47:52.637Z [INFO] Syncing safe settings for 'globals/'. +2025-09-11T15:47:52.636Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-11T15:47:52.864Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-11T15:47:53.106Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-11T15:47:53.106Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-11T15:47:53.106Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml +2025-09-11T15:47:53.434Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:47:53.680Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:47:53.681Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-11T15:47:53.681Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) +2025-09-11T15:47:54.039Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:47:54.273Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:47:54.273Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) +2025-09-11T15:47:54.273Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-11T15:47:54.585Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:47:54.886Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:47:54.886Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) +2025-09-11T15:47:54.886Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-11T15:47:55.093Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-11T15:47:55.093Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-11T15:47:55.511Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:47:55.758Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:47:55.759Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-11T15:47:55.759Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) +2025-09-11T15:47:55.933Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-11T15:47:55.933Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-11T15:47:56.108Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. +2025-09-11T15:47:59.386Z [DEBUG] Pull_request REopened ! +2025-09-11T15:47:59.386Z [DEBUG] Is Admin repo event false +2025-09-11T15:47:59.386Z [DEBUG] Not working on the Admin repo, returning... +2025-09-11T15:49:09.315Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-11T15:49:09.315Z [DEBUG] Branch Protection edited by a Human +2025-09-11T15:49:09.315Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-11T15:49:09.553Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2025-09-11T15:49:24.081Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-11T15:49:24.081Z [INFO] Received 'pull_request.closed' event: 47 +2025-09-11T15:49:24.356Z [INFO] Files changed in PR #47: .github/safe-settings/globals/suborg.yml +2025-09-11T15:49:24.356Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-11T15:49:24.357Z [INFO] Syncing safe settings for 'globals/'. +2025-09-11T15:49:24.617Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-11T15:49:24.814Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-11T15:49:24.814Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml +2025-09-11T15:49:24.814Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-11T15:49:25.155Z [DEBUG] Is Admin repo event false +2025-09-11T15:49:25.155Z [DEBUG] Not working on the Admin repo, returning... +2025-09-11T15:49:25.341Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:49:25.565Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T15:49:25.566Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-11T15:49:25.566Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) +2025-09-11T15:49:25.935Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:49:26.172Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T15:49:26.173Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-11T15:49:26.173Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) +2025-09-11T15:49:26.524Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:49:26.777Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T15:49:26.777Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-11T15:49:26.777Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) +2025-09-11T15:49:26.964Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-11T15:49:26.964Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-11T15:49:27.285Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:49:27.487Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T15:49:27.487Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-11T15:49:27.487Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) +2025-09-11T15:49:27.661Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-11T15:49:27.661Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-11T15:49:27.830Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. +2025-09-11T15:50:54.611Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:50:54.611Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:55.683Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:55.683Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:50:56.556Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:50:56.556Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:57.768Z [DEBUG] Repository member edited by Bot +2025-09-11T15:50:57.768Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:51:20.652Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:51:20.652Z [DEBUG] Branch Protection edited by Bot +2025-09-11T15:51:24.560Z [DEBUG] Not triggered by Safe-settings... +2025-09-11T15:51:24.559Z [DEBUG] Check run was created! +2025-09-11T15:51:35.514Z [DEBUG] Branch Protection edited by Bot +2025-09-11T15:51:35.514Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-11T15:51:36.562Z [DEBUG] Not triggered by Safe-settings... +2025-09-11T15:51:36.562Z [DEBUG] Check run was created! +2025-09-11T15:53:20.953Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-11T15:53:22.397Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-11T15:53:22.397Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-11T15:53:22.397Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-11T15:53:22.628Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:53:22.628Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:22.628Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-11T15:53:22.859Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-11T15:53:22.859Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:53:23.452Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-11T15:53:23.452Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-11T15:53:23.452Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-11T15:53:23.691Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:53:23.691Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-11T15:53:23.691Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:23.944Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-11T15:53:23.944Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:53:24.491Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-11T15:53:24.491Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-11T15:53:24.491Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-11T15:53:24.733Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:24.733Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:53:24.733Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-11T15:53:25.050Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-11T15:53:25.050Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:53:25.516Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:53:25.516Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-11T15:53:25.516Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-11T15:53:25.516Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:53:25.985Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:53:25.985Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-11T15:53:25.985Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:53:26.589Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-11T15:53:26.589Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-11T15:53:26.589Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-11T15:53:26.825Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-11T15:53:26.825Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:53:26.825Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:53:27.058Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-11T15:53:27.058Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:55:44.833Z [INFO] Retrieving settings from org: jetest99 +2025-09-11T15:55:45.087Z [INFO] Skipping jetest99: already present in hub +2025-09-11T15:55:45.087Z [INFO] Retrieving settings from org: jefeish-training +2025-09-11T15:55:45.298Z [INFO] Retrieving settings from org: jefeish-test1 +2025-09-11T15:55:45.298Z [INFO] Skipping jefeish-training: already present in hub +2025-09-11T15:55:45.551Z [INFO] Retrieving settings from org: copilot-for-emus +2025-09-11T15:55:45.551Z [INFO] Skipping jefeish-test1: already present in hub +2025-09-11T15:55:46.001Z [INFO] Retrieving settings from org: jefeish-migration-test +2025-09-11T15:55:46.292Z [INFO] Retrieving settings from org: decyjphr-training +2025-09-11T15:55:46.292Z [INFO] Skipping jefeish-migration-test: already present in hub +2025-09-11T15:55:46.556Z [INFO] Skipping decyjphr-training: already present in hub +2025-09-11T15:55:46.556Z [INFO] Retrieving settings from org: decyjphr-emu +2025-09-11T15:56:33.309Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-11T15:56:34.976Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-11T15:56:34.976Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-11T15:56:34.976Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-11T15:56:35.221Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-11T15:56:35.221Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:35.221Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:35.434Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-11T15:56:35.434Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:36.047Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-11T15:56:36.047Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-11T15:56:36.047Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-11T15:56:36.273Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:36.273Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:36.273Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-11T15:56:36.514Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:36.514Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-11T15:56:37.018Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-11T15:56:37.018Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-11T15:56:37.018Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-11T15:56:37.239Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:37.239Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-11T15:56:37.239Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:37.445Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-11T15:56:37.445Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:37.914Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-11T15:56:37.914Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:37.914Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-11T15:56:37.914Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:38.412Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:38.412Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:38.412Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-11T15:56:38.977Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-11T15:56:38.977Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-11T15:56:38.977Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-11T15:56:39.247Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:39.247Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:39.247Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-11T15:56:39.484Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:39.485Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-11T15:56:51.776Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-11T15:56:53.217Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-11T15:56:53.217Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-11T15:56:53.217Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-11T15:56:53.436Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:53.436Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:53.436Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-11T15:56:53.666Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-11T15:56:53.666Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:54.354Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-11T15:56:54.354Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-11T15:56:54.354Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-11T15:56:54.566Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:54.566Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-11T15:56:54.566Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-11T15:56:54.792Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:54.792Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-11T15:56:55.340Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-11T15:56:55.340Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-11T15:56:55.340Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-11T15:56:55.563Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:55.563Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:55.563Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-11T15:56:55.807Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:55.808Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-11T15:56:56.233Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-11T15:56:56.233Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:56.233Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-11T15:56:56.233Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-11T15:56:56.688Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-11T15:56:56.688Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:56.688Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-11T15:56:57.315Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-11T15:56:57.315Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-11T15:56:57.314Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-11T15:56:57.525Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-11T15:56:57.525Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-11T15:56:57.525Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-11T15:56:57.745Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-11T15:56:57.745Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-12T01:30:19.210Z [DEBUG] Check run was created! +2025-09-12T01:30:19.210Z [DEBUG] Not triggered by Safe-settings... +2025-09-13T22:42:46.364Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-13T22:42:48.651Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-13T22:42:48.651Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-13T22:42:48.651Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-13T22:42:48.902Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:48.902Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:42:48.902Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-13T22:42:49.124Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:42:49.124Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-13T22:42:49.899Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-13T22:42:49.899Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-13T22:42:49.899Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-13T22:42:50.157Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:50.157Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:42:50.157Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-13T22:42:50.373Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-13T22:42:50.373Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:42:51.186Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-13T22:42:51.186Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-13T22:42:51.186Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-13T22:42:51.533Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:42:51.533Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-13T22:42:51.533Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:51.901Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:42:51.901Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-13T22:42:52.487Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:42:52.487Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:42:52.487Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-13T22:42:52.487Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-13T22:42:52.978Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:42:52.979Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:42:52.978Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-13T22:42:53.877Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-13T22:42:53.877Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-13T22:42:53.877Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-13T22:42:54.131Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:42:54.131Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:42:54.131Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-13T22:42:54.397Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:42:54.397Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-13T22:43:30.372Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-13T22:43:34.138Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-13T22:43:34.138Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-13T22:43:34.138Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-13T22:43:34.350Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:34.350Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-13T22:43:34.350Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:43:34.574Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:43:34.574Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-13T22:43:35.156Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-13T22:43:35.156Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-13T22:43:35.156Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-13T22:43:35.390Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-13T22:43:35.390Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:35.390Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-13T22:43:35.778Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:43:35.778Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-13T22:43:36.334Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-13T22:43:36.334Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-13T22:43:36.334Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-13T22:43:36.548Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:36.548Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:43:36.548Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-13T22:43:36.780Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:43:36.780Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-13T22:43:37.236Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:43:37.236Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e +2025-09-13T22:43:37.236Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-13T22:43:37.236Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-13T22:43:37.666Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:43:37.666Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-13T22:43:37.666Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-13T22:43:38.247Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-13T22:43:38.247Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-13T22:43:38.247Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-13T22:43:38.457Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-13T22:43:38.457Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-13T22:43:38.457Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-13T22:43:38.763Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-13T22:43:38.763Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-16T15:47:20.575Z [INFO] Received 'pull_request.closed' event: 10 +2025-09-16T15:47:20.577Z [INFO] Pull request.closed is not from master admin repo/org (jefeish-training/safe-settings-config-master), ignoring +2025-09-16T15:47:20.664Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... +2025-09-16T15:47:20.664Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:47:20.966Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:47:21.579Z [DEBUG] Is Admin repo event true +2025-09-16T15:47:21.579Z [DEBUG] Working on the default branch, returning... +2025-09-16T15:47:22.242Z [DEBUG] Branch Protection edited by Bot +2025-09-16T15:47:22.242Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2025-09-16T15:47:22.349Z [DEBUG] Not working on the default branch, returning... +2025-09-16T15:47:23.391Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:47:23.391Z [DEBUG] Check run was created! +2025-09-16T15:47:37.290Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:47:37.290Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:47:37.290Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:47:37.812Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:48:45.963Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:48:45.963Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:48:45.963Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:48:46.215Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:50:41.987Z [DEBUG] Check run was created! +2025-09-16T15:50:41.988Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:50:48.685Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:50:48.685Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:50:48.685Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:50:48.941Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:51:28.479Z [DEBUG] Check run was created! +2025-09-16T15:51:28.479Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:51:31.307Z [DEBUG] Check run was created! +2025-09-16T15:51:31.307Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:51:50.461Z [DEBUG] Check run was created! +2025-09-16T15:51:50.461Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:51:51.381Z [DEBUG] Check run was created! +2025-09-16T15:51:51.381Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:35.618Z [DEBUG] Branch Protection edited by a Human +2025-09-16T15:58:35.618Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-16T15:58:35.618Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2025-09-16T15:58:36.135Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-16T15:58:45.832Z [DEBUG] Check run was created! +2025-09-16T15:58:45.832Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:45.867Z [DEBUG] Check run was created! +2025-09-16T15:58:45.867Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:46.449Z [DEBUG] Is Admin repo event false +2025-09-16T15:58:46.449Z [DEBUG] Not working on the Admin repo, returning... +2025-09-16T15:58:46.724Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:46.724Z [DEBUG] Check run was created! +2025-09-16T15:58:49.218Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T15:58:49.218Z [DEBUG] Check run was created! +2025-09-16T16:01:29.319Z [DEBUG] Check run was created! +2025-09-16T16:01:29.319Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:13:27.125Z [DEBUG] Not working on the Admin repo, returning... +2025-09-16T17:13:27.125Z [DEBUG] Is Admin repo event false +2025-09-16T17:13:28.760Z [DEBUG] Check run was created! +2025-09-16T17:13:28.760Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:13:28.848Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:13:28.848Z [DEBUG] Check run was created! +2025-09-16T17:13:28.918Z [DEBUG] Check run was created! +2025-09-16T17:13:28.918Z [DEBUG] Not triggered by Safe-settings... +2025-09-16T17:14:33.551Z [DEBUG] Check run was created! +2025-09-16T17:14:33.551Z [DEBUG] Not triggered by Safe-settings... +2025-09-18T14:09:14.208Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-18T14:09:14.208Z [INFO] Received 'pull_request.closed' event: 48 +2025-09-18T14:09:14.585Z [INFO] Files changed in PR #48: .github/safe-settings/globals/manifest.yml +2025-09-18T14:09:14.585Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-18T14:09:14.586Z [INFO] Syncing safe settings for 'globals/'. +2025-09-18T14:09:14.924Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-18T14:09:15.216Z [DEBUG] Skipping sync for manifest.yml (should only exist in hub) +2025-09-18T14:09:15.672Z [DEBUG] Check run was created! +2025-09-18T14:09:15.672Z [DEBUG] Not triggered by Safe-settings... +2025-09-18T14:09:23.527Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:09:23.526Z [DEBUG] Is Admin repo event false +2025-09-18T14:13:27.105Z [DEBUG] Is Admin repo event false +2025-09-18T14:13:27.105Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:13:56.822Z [DEBUG] Is Admin repo event false +2025-09-18T14:13:56.822Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:13:56.822Z [DEBUG] Pull_request opened ! +2025-09-18T14:14:06.314Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-18T14:14:06.314Z [INFO] Received 'pull_request.closed' event: 49 +2025-09-18T14:14:07.042Z [INFO] Files changed in PR #49: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:07.043Z [INFO] Syncing safe settings for organization: jefeish-training +2025-09-18T14:14:07.042Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). +2025-09-18T14:14:07.043Z [INFO] Organization: jefeish-training, Destination Repo: safe-settings-config, Destination Folder: .github +2025-09-18T14:14:07.042Z [INFO] Orgs updated in PR #49: jefeish-training +2025-09-18T14:14:07.043Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' +2025-09-18T14:14:07.043Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2025-09-18T14:14:07.333Z [INFO] DEBUG: PR #49 contains 1 changed file(s) +2025-09-18T14:14:07.333Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2025-09-18T14:14:07.333Z [INFO] DEBUG: first file object = { + "sha": "8f345e9e4d6701accc0d39f587d00950c9a17ed5", + "filename": ".github/safe-settings/organizations/jefeish-training/settings.yml", + "status": "modified", + "additions": 175, + "deletions": 175, + "changes": 350, + "blob_url": "https://github.com/jefeish-training/safe-settings-config-master/blob/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml", + "raw_url": "https://github.com/jefeish-training/safe-settings-config-master/raw/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml", + "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml?ref=ee5e72b1fcb62dda5d16fd244fe36bb072589455", + "patch": "@@ -222,178 +222,178 @@ validator:\n \n # Rulesets\n # See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options\n-rulesets:\n- - name: Template\n- # The target of the ruleset. Can be one of:\n- # - branch\n- # - tag\n- target: branch\n- # The enforcement level of the ruleset. `evaluate` allows admins to test\n- # rules before enforcing them.\n- # - disabled\n- # - active\n- # - evaluate\n- enforcement: active\n-\n- # The actors that can bypass the rules in this ruleset\n- bypass_actors:\n- - actor_id: number\n- # type: The type of actor that can bypass a ruleset\n- # - RepositoryRole\n- # - Team\n- # - Integration\n- # - OrganizationAdmin\n- actor_type: Team\n- # When the specified actor can bypass the ruleset. `pull_request`\n- # means that an actor can only bypass rules on pull requests.\n- # - always\n- # - pull_request\n- bypass_mode: pull_request\n-\n- - actor_id: 1\n- actor_type: OrganizationAdmin\n- bypass_mode: always\n-\n- - actor_id: 7898\n- actor_type: RepositoryRole\n- bypass_mode: always\n-\n- - actor_id: 210920\n- actor_type: Integration\n- bypass_mode: always\n-\n- conditions:\n- # Parameters for a repository ruleset ref name condition\n- ref_name:\n- # Array of ref names or patterns to include. One of these\n- # patterns must match for the condition to pass. Also accepts\n- # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n- # include all branches.\n- include: [\"~DEFAULT_BRANCH\"]\n-\n- # Array of ref names or patterns to exclude. The condition\n- # will not pass if any of these patterns match.\n- exclude: [\"refs/heads/oldmaster\"]\n-\n- # This condition only exists at the org level (remove for suborg and repo level rulesets)\n- repository_name:\n- # Array of repository names or patterns to include.\n- # One of these patterns must match for the condition\n- # to pass. Also accepts `~ALL` to include all\n- # repositories.\n- include: [\"test*\"]\n- # Array of repository names or patterns to exclude. The\n- # condition will not pass if any of these patterns\n- # match.\n- exclude: [\"test\", \"test1\"]\n- # Whether renaming of target repositories is\n- # prevented.\n- protected: true\n-\n- # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n- rules:\n- - type: creation\n- - type: update\n- parameters:\n- # Branch can pull changes from its upstream repository\n- update_allows_fetch_and_merge: true\n- - type: deletion\n- - type: required_linear_history\n- - type: required_signatures\n-\n- - type: required_deployments\n- parameters:\n- required_deployment_environments: [\"staging\"]\n-\n- - type: pull_request\n- parameters:\n- # Reviewable commits pushed will dismiss previous pull\n- # request review approvals.\n- dismiss_stale_reviews_on_push: true\n- # Require an approving review in pull requests that modify\n- # files that have a designated code owner\n- require_code_owner_review: true\n- # Whether the most recent reviewable push must be approved\n- # by someone other than the person who pushed it.\n- require_last_push_approval: true\n- # The number of approving reviews that are required before a\n- # pull request can be merged.\n- required_approving_review_count: 1\n- # All conversations on code must be resolved before a pull\n- # request can be merged.\n- required_review_thread_resolution: true\n-\n- # Choose which status checks must pass before branches can be merged\n- # into a branch that matches this rule. When enabled, commits must\n- # first be pushed to another branch, then merged or pushed directly\n- # to a branch that matches this rule after status checks have\n- # passed.\n- - type: required_status_checks\n- parameters:\n- # Whether pull requests targeting a matching branch must be\n- # tested with the latest code. This setting will not take\n- # effect unless at least one status check is enabled.\n- strict_required_status_checks_policy: true\n- required_status_checks:\n- - context: CodeQL\n- integration_id: 1234\n- - context: GHAS Compliance\n- integration_id: 1234\n-\n- # Choose which workflows must pass before branches can be merged.\n- - type: workflows\n- parameters:\n- workflows:\n- - path: .github/workflows/example.yml\n- # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n- # in the browser console of the repository to get the repository_id\n- repository_id: 123456\n- # One of the following:\n- # Branch or tag\n- ref: refs/heads/main\n- # Commit SHA\n- sha: 1234567890abcdef\n-\n- - type: commit_message_pattern\n- parameters:\n- name: test commit_message_pattern\n- # required:\n- # - operator\n- # - pattern\n- negate: true\n- operator: starts_with\n- # The operator to use for matching.\n- # - starts_with\n- # - ends_with\n- # - contains\n- # - regex\n- pattern: skip*\n- # The pattern to match with.\n-\n- - type: commit_author_email_pattern\n- parameters:\n- name: test commit_author_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: committer_email_pattern\n- parameters:\n- name: test committer_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: branch_name_pattern\n- parameters:\n- name: test branch_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n-\n- - type: \"tag_name_pattern\"\n- parameters:\n- name: test tag_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n+# rulesets:\n+# - name: Template\n+# # The target of the ruleset. Can be one of:\n+# # - branch\n+# # - tag\n+# target: branch\n+# # The enforcement level of the ruleset. `evaluate` allows admins to test\n+# # rules before enforcing them.\n+# # - disabled\n+# # - active\n+# # - evaluate\n+# enforcement: active\n+\n+# # The actors that can bypass the rules in this ruleset\n+# bypass_actors:\n+# - actor_id: number\n+# # type: The type of actor that can bypass a ruleset\n+# # - RepositoryRole\n+# # - Team\n+# # - Integration\n+# # - OrganizationAdmin\n+# actor_type: Team\n+# # When the specified actor can bypass the ruleset. `pull_request`\n+# # means that an actor can only bypass rules on pull requests.\n+# # - always\n+# # - pull_request\n+# bypass_mode: pull_request\n+\n+# - actor_id: 1\n+# actor_type: OrganizationAdmin\n+# bypass_mode: always\n+\n+# - actor_id: 7898\n+# actor_type: RepositoryRole\n+# bypass_mode: always\n+\n+# - actor_id: 210920\n+# actor_type: Integration\n+# bypass_mode: always\n+\n+# conditions:\n+# # Parameters for a repository ruleset ref name condition\n+# ref_name:\n+# # Array of ref names or patterns to include. One of these\n+# # patterns must match for the condition to pass. Also accepts\n+# # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n+# # include all branches.\n+# include: [\"~DEFAULT_BRANCH\"]\n+\n+# # Array of ref names or patterns to exclude. The condition\n+# # will not pass if any of these patterns match.\n+# exclude: [\"refs/heads/oldmaster\"]\n+\n+# # This condition only exists at the org level (remove for suborg and repo level rulesets)\n+# repository_name:\n+# # Array of repository names or patterns to include.\n+# # One of these patterns must match for the condition\n+# # to pass. Also accepts `~ALL` to include all\n+# # repositories.\n+# include: [\"test*\"]\n+# # Array of repository names or patterns to exclude. The\n+# # condition will not pass if any of these patterns\n+# # match.\n+# exclude: [\"test\", \"test1\"]\n+# # Whether renaming of target repositories is\n+# # prevented.\n+# protected: true\n+\n+# # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n+# rules:\n+# - type: creation\n+# - type: update\n+# parameters:\n+# # Branch can pull changes from its upstream repository\n+# update_allows_fetch_and_merge: true\n+# - type: deletion\n+# - type: required_linear_history\n+# - type: required_signatures\n+\n+# - type: required_deployments\n+# parameters:\n+# required_deployment_environments: [\"staging\"]\n+\n+# - type: pull_request\n+# parameters:\n+# # Reviewable commits pushed will dismiss previous pull\n+# # request review approvals.\n+# dismiss_stale_reviews_on_push: true\n+# # Require an approving review in pull requests that modify\n+# # files that have a designated code owner\n+# require_code_owner_review: true\n+# # Whether the most recent reviewable push must be approved\n+# # by someone other than the person who pushed it.\n+# require_last_push_approval: true\n+# # The number of approving reviews that are required before a\n+# # pull request can be merged.\n+# required_approving_review_count: 1\n+# # All conversations on code must be resolved before a pull\n+# # request can be merged.\n+# required_review_thread_resolution: true\n+\n+# # Choose which status checks must pass before branches can be merged\n+# # into a branch that matches this rule. When enabled, commits must\n+# # first be pushed to another branch, then merged or pushed directly\n+# # to a branch that matches this rule after status checks have\n+# # passed.\n+# - type: required_status_checks\n+# parameters:\n+# # Whether pull requests targeting a matching branch must be\n+# # tested with the latest code. This setting will not take\n+# # effect unless at least one status check is enabled.\n+# strict_required_status_checks_policy: true\n+# required_status_checks:\n+# - context: CodeQL\n+# integration_id: 1234\n+# - context: GHAS Compliance\n+# integration_id: 1234\n+\n+# # Choose which workflows must pass before branches can be merged.\n+# - type: workflows\n+# parameters:\n+# workflows:\n+# - path: .github/workflows/example.yml\n+# # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n+# # in the browser console of the repository to get the repository_id\n+# repository_id: 123456\n+# # One of the following:\n+# # Branch or tag\n+# ref: refs/heads/main\n+# # Commit SHA\n+# sha: 1234567890abcdef\n+\n+# - type: commit_message_pattern\n+# parameters:\n+# name: test commit_message_pattern\n+# # required:\n+# # - operator\n+# # - pattern\n+# negate: true\n+# operator: starts_with\n+# # The operator to use for matching.\n+# # - starts_with\n+# # - ends_with\n+# # - contains\n+# # - regex\n+# pattern: skip*\n+# # The pattern to match with.\n+\n+# - type: commit_author_email_pattern\n+# parameters:\n+# name: test commit_author_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: committer_email_pattern\n+# parameters:\n+# name: test committer_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: branch_name_pattern\n+# parameters:\n+# name: test branch_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"\n+\n+# - type: \"tag_name_pattern\"\n+# parameters:\n+# name: test tag_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"" +} +2025-09-18T14:14:07.333Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:07.334Z [INFO] DEBUG: FILE[0] raw={"sha":"8f345e9e4d6701accc0d39f587d00950c9a17ed5","filename":".github/safe-settings/organizations/jefeish-training/settings.yml","status":"modified","additions":175,"deletions":175,"changes":350,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml?ref=ee5e72b1fcb62dda5d16fd244fe36bb072589455","patch":"@@ -222,178 +222,178 @@ validator:\n \n # Rulesets\n # See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options\n-rulesets:\n- - name: Template\n- # The target of the ruleset. Can be one of:\n- # - branch\n- # - tag\n- target: branch\n- # The enforcement level of the ruleset. `evaluate` allows admins to test\n- # rules before enforcing them.\n- # - disabled\n- # - active\n- # - evaluate\n- enforcement: active\n-\n- # The actors that can bypass the rules in this ruleset\n- bypass_actors:\n- - actor_id: number\n- # type: The type of actor that can bypass a ruleset\n- # - RepositoryRole\n- # - Team\n- # - Integration\n- # - OrganizationAdmin\n- actor_type: Team\n- # When the specified actor can bypass the ruleset. `pull_request`\n- # means that an actor can only bypass rules on pull requests.\n- # - always\n- # - pull_request\n- bypass_mode: pull_request\n-\n- - actor_id: 1\n- actor_type: OrganizationAdmin\n- bypass_mode: always\n-\n- - actor_id: 7898\n- actor_type: RepositoryRole\n- bypass_mode: always\n-\n- - actor_id: 210920\n- actor_type: Integration\n- bypass_mode: always\n-\n- conditions:\n- # Parameters for a repository ruleset ref name condition\n- ref_name:\n- # Array of ref names or patterns to include. One of these\n- # patterns must match for the condition to pass. Also accepts\n- # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n- # include all branches.\n- include: [\"~DEFAULT_BRANCH\"]\n-\n- # Array of ref names or patterns to exclude. The condition\n- # will not pass if any of these patterns match.\n- exclude: [\"refs/heads/oldmaster\"]\n-\n- # This condition only exists at the org level (remove for suborg and repo level rulesets)\n- repository_name:\n- # Array of repository names or patterns to include.\n- # One of these patterns must match for the condition\n- # to pass. Also accepts `~ALL` to include all\n- # repositories.\n- include: [\"test*\"]\n- # Array of repository names or patterns to exclude. The\n- # condition will not pass if any of these patterns\n- # match.\n- exclude: [\"test\", \"test1\"]\n- # Whether renaming of target repositories is\n- # prevented.\n- protected: true\n-\n- # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n- rules:\n- - type: creation\n- - type: update\n- parameters:\n- # Branch can pull changes from its upstream repository\n- update_allows_fetch_and_merge: true\n- - type: deletion\n- - type: required_linear_history\n- - type: required_signatures\n-\n- - type: required_deployments\n- parameters:\n- required_deployment_environments: [\"staging\"]\n-\n- - type: pull_request\n- parameters:\n- # Reviewable commits pushed will dismiss previous pull\n- # request review approvals.\n- dismiss_stale_reviews_on_push: true\n- # Require an approving review in pull requests that modify\n- # files that have a designated code owner\n- require_code_owner_review: true\n- # Whether the most recent reviewable push must be approved\n- # by someone other than the person who pushed it.\n- require_last_push_approval: true\n- # The number of approving reviews that are required before a\n- # pull request can be merged.\n- required_approving_review_count: 1\n- # All conversations on code must be resolved before a pull\n- # request can be merged.\n- required_review_thread_resolution: true\n-\n- # Choose which status checks must pass before branches can be merged\n- # into a branch that matches this rule. When enabled, commits must\n- # first be pushed to another branch, then merged or pushed directly\n- # to a branch that matches this rule after status checks have\n- # passed.\n- - type: required_status_checks\n- parameters:\n- # Whether pull requests targeting a matching branch must be\n- # tested with the latest code. This setting will not take\n- # effect unless at least one status check is enabled.\n- strict_required_status_checks_policy: true\n- required_status_checks:\n- - context: CodeQL\n- integration_id: 1234\n- - context: GHAS Compliance\n- integration_id: 1234\n-\n- # Choose which workflows must pass before branches can be merged.\n- - type: workflows\n- parameters:\n- workflows:\n- - path: .github/workflows/example.yml\n- # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n- # in the browser console of the repository to get the repository_id\n- repository_id: 123456\n- # One of the following:\n- # Branch or tag\n- ref: refs/heads/main\n- # Commit SHA\n- sha: 1234567890abcdef\n-\n- - type: commit_message_pattern\n- parameters:\n- name: test commit_message_pattern\n- # required:\n- # - operator\n- # - pattern\n- negate: true\n- operator: starts_with\n- # The operator to use for matching.\n- # - starts_with\n- # - ends_with\n- # - contains\n- # - regex\n- pattern: skip*\n- # The pattern to match with.\n-\n- - type: commit_author_email_pattern\n- parameters:\n- name: test commit_author_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: committer_email_pattern\n- parameters:\n- name: test committer_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: branch_name_pattern\n- parameters:\n- name: test branch_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n-\n- - type: \"tag_name_pattern\"\n- parameters:\n- name: test tag_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n+# rulesets:\n+# - name: Template\n+# # The target of the ruleset. Can be one of:\n+# # - branch\n+# # - tag\n+# target: branch\n+# # The enforcement level of the ruleset. `evaluate` allows admins to test\n+# # rules before enforcing them.\n+# # - disabled\n+# # - active\n+# # - evaluate\n+# enforcement: active\n+\n+# # The actors that can bypass the rules in this ruleset\n+# bypass_actors:\n+# - actor_id: number\n+# # type: The type of actor that can bypass a ruleset\n+# # - RepositoryRole\n+# # - Team\n+# # - Integration\n+# # - OrganizationAdmin\n+# actor_type: Team\n+# # When the specified actor can bypass the ruleset. `pull_request`\n+# # means that an actor can only bypass rules on pull requests.\n+# # - always\n+# # - pull_request\n+# bypass_mode: pull_request\n+\n+# - actor_id: 1\n+# actor_type: OrganizationAdmin\n+# bypass_mode: always\n+\n+# - actor_id: 7898\n+# actor_type: RepositoryRole\n+# bypass_mode: always\n+\n+# - actor_id: 210920\n+# actor_type: Integration\n+# bypass_mode: always\n+\n+# conditions:\n+# # Parameters for a repository ruleset ref name condition\n+# ref_name:\n+# # Array of ref names or patterns to include. One of these\n+# # patterns must match for the condition to pass. Also accepts\n+# # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n+# # include all branches.\n+# include: [\"~DEFAULT_BRANCH\"]\n+\n+# # Array of ref names or patterns to exclude. The condition\n+# # will not pass if any of these patterns match.\n+# exclude: [\"refs/heads/oldmaster\"]\n+\n+# # This condition only exists at the org level (remove for suborg and repo level rulesets)\n+# repository_name:\n+# # Array of repository names or patterns to include.\n+# # One of these patterns must match for the condition\n+# # to pass. Also accepts `~ALL` to include all\n+# # repositories.\n+# include: [\"test*\"]\n+# # Array of repository names or patterns to exclude. The\n+# # condition will not pass if any of these patterns\n+# # match.\n+# exclude: [\"test\", \"test1\"]\n+# # Whether renaming of target repositories is\n+# # prevented.\n+# protected: true\n+\n+# # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n+# rules:\n+# - type: creation\n+# - type: update\n+# parameters:\n+# # Branch can pull changes from its upstream repository\n+# update_allows_fetch_and_merge: true\n+# - type: deletion\n+# - type: required_linear_history\n+# - type: required_signatures\n+\n+# - type: required_deployments\n+# parameters:\n+# required_deployment_environments: [\"staging\"]\n+\n+# - type: pull_request\n+# parameters:\n+# # Reviewable commits pushed will dismiss previous pull\n+# # request review approvals.\n+# dismiss_stale_reviews_on_push: true\n+# # Require an approving review in pull requests that modify\n+# # files that have a designated code owner\n+# require_code_owner_review: true\n+# # Whether the most recent reviewable push must be approved\n+# # by someone other than the person who pushed it.\n+# require_last_push_approval: true\n+# # The number of approving reviews that are required before a\n+# # pull request can be merged.\n+# required_approving_review_count: 1\n+# # All conversations on code must be resolved before a pull\n+# # request can be merged.\n+# required_review_thread_resolution: true\n+\n+# # Choose which status checks must pass before branches can be merged\n+# # into a branch that matches this rule. When enabled, commits must\n+# # first be pushed to another branch, then merged or pushed directly\n+# # to a branch that matches this rule after status checks have\n+# # passed.\n+# - type: required_status_checks\n+# parameters:\n+# # Whether pull requests targeting a matching branch must be\n+# # tested with the latest code. This setting will not take\n+# # effect unless at least one status check is enabled.\n+# strict_required_status_checks_policy: true\n+# required_status_checks:\n+# - context: CodeQL\n+# integration_id: 1234\n+# - context: GHAS Compliance\n+# integration_id: 1234\n+\n+# # Choose which workflows must pass before branches can be merged.\n+# - type: workflows\n+# parameters:\n+# workflows:\n+# - path: .github/workflows/example.yml\n+# # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n+# # in the browser console of the repository to get the repository_id\n+# repository_id: 123456\n+# # One of the following:\n+# # Branch or tag\n+# ref: refs/heads/main\n+# # Commit SHA\n+# sha: 1234567890abcdef\n+\n+# - type: commit_message_pattern\n+# parameters:\n+# name: test commit_message_pattern\n+# # required:\n+# # - operator\n+# # - pattern\n+# negate: true\n+# operator: starts_with\n+# # The operator to use for matching.\n+# # - starts_with\n+# # - ends_with\n+# # - contains\n+# # - regex\n+# pattern: skip*\n+# # The pattern to match with.\n+\n+# - type: commit_author_email_pattern\n+# parameters:\n+# name: test commit_author_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: committer_email_pattern\n+# parameters:\n+# name: test committer_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: branch_name_pattern\n+# parameters:\n+# name: test branch_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"\n+\n+# - type: \"tag_name_pattern\"\n+# parameters:\n+# name: test tag_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\""} +2025-09-18T14:14:07.334Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-training/settings.yml" length=65 +2025-09-18T14:14:07.334Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:07.334Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-training +2025-09-18T14:14:07.335Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-training +2025-09-18T14:14:07.536Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #49 to jefeish-training/safe-settings-config@main under .github (directPush=true) +2025-09-18T14:14:09.113Z [INFO] Committed .github/settings.yml to jefeish-training/safe-settings-config@main +2025-09-18T14:14:09.113Z [INFO] Changes pushed directly to jefeish-training/safe-settings-config@main +2025-09-18T14:14:10.074Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... +2025-09-18T14:14:10.074Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2025-09-18T14:14:10.392Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2025-09-18T14:14:12.340Z [DEBUG] Is Admin repo event true +2025-09-18T14:14:12.340Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:14:49.484Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-18T14:14:51.219Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-18T14:14:51.219Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-18T14:14:51.219Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-18T14:14:51.485Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:51.485Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:14:51.485Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-18T14:14:51.745Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:14:51.745Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-18T14:14:52.466Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-18T14:14:52.465Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-18T14:14:52.466Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-18T14:14:52.707Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:14:52.707Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-18T14:14:52.707Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:52.991Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:14:52.992Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-18T14:14:53.724Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-18T14:14:53.724Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-18T14:14:53.724Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-18T14:14:53.987Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:53.987Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-18T14:14:53.987Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:14:54.285Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:14:54.285Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:14:54.869Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-18T14:14:54.869Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:14:54.869Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-18T14:14:54.869Z [DEBUG] 9. [SYNC DEBUG] Org hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:14:55.440Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:14:55.440Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-18T14:14:55.440Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:14:56.225Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-18T14:14:56.225Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-18T14:14:56.225Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-18T14:14:56.469Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:14:56.469Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:14:56.469Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-18T14:14:56.735Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:14:56.735Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-18T14:15:07.249Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... +2025-09-18T14:15:08.957Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test +2025-09-18T14:15:08.957Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test +2025-09-18T14:15:08.957Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main +2025-09-18T14:15:09.220Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-migration-test, + ref: main +2025-09-18T14:15:09.220Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:15:09.220Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:09.484Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test +2025-09-18T14:15:09.484Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:15:10.150Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2025-09-18T14:15:10.150Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2025-09-18T14:15:10.150Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2025-09-18T14:15:10.394Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml +2025-09-18T14:15:10.394Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-test1, + ref: main +2025-09-18T14:15:10.394Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:10.648Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:15:10.648Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2025-09-18T14:15:11.505Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training +2025-09-18T14:15:11.505Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main +2025-09-18T14:15:11.505Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training +2025-09-18T14:15:11.761Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:11.761Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:15:11.761Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jefeish-training, + ref: main +2025-09-18T14:15:12.100Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:15:12.100Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml +2025-09-18T14:15:13.786Z [DEBUG] 9. [SYNC DEBUG] Org hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:15:13.786Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml +2025-09-18T14:15:13.786Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 +2025-09-18T14:15:13.786Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml +2025-09-18T14:15:14.195Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:15:14.195Z [DEBUG] Is Admin repo event false +2025-09-18T14:15:14.354Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:15:14.354Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml +2025-09-18T14:15:14.354Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 +2025-09-18T14:15:15.049Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 +2025-09-18T14:15:15.049Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 +2025-09-18T14:15:15.049Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main +2025-09-18T14:15:15.316Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml +2025-09-18T14:15:15.316Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2025-09-18T14:15:15.316Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: + owner: jefeish-training, + repo: safe-settings-config-master, + path: .github/safe-settings/organizations/jetest99, + ref: main +2025-09-18T14:15:15.620Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2025-09-18T14:15:15.620Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 +2025-09-18T14:21:33.248Z [INFO] Received 'pull_request.closed' event: 50 +2025-09-18T14:21:33.248Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-18T14:21:34.144Z [DEBUG] Not working on the Admin repo, returning... +2025-09-18T14:21:34.144Z [DEBUG] Is Admin repo event false +2025-09-18T14:21:34.212Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-18T14:21:34.212Z [INFO] Files changed in PR #50: .github/safe-settings/globals/repo.yml +2025-09-18T14:21:34.212Z [INFO] Syncing safe settings for 'globals/'. +2025-09-18T14:21:34.524Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-18T14:21:34.737Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/repo.yml +2025-09-18T14:21:34.737Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-18T14:21:34.737Z [DEBUG] Rule 'global-defaults' matches file 'repo.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-18T14:21:35.425Z [DEBUG] Checking existence of .github/repo.yml in jetest99/safe-settings-config +2025-09-18T14:21:35.732Z [INFO] File .github/repo.yml not found in jetest99/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:35.733Z [INFO] Syncing repo.yml to jetest99 (mergeStrategy=merge) +2025-09-18T14:21:36.704Z [INFO] Committed .github/repo.yml to jetest99/safe-settings-config@main +2025-09-18T14:21:36.704Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-18T14:21:36.704Z [INFO] Changes pushed directly to jetest99/safe-settings-config@main +2025-09-18T14:21:37.083Z [DEBUG] Checking existence of .github/repo.yml in jefeish-training/safe-settings-config +2025-09-18T14:21:37.357Z [INFO] File .github/repo.yml not found in jefeish-training/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:37.357Z [INFO] Syncing repo.yml to jefeish-training (mergeStrategy=merge) +2025-09-18T14:21:37.740Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:38.344Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-18T14:21:38.344Z [INFO] Changes pushed directly to jefeish-training/safe-settings-config@main +2025-09-18T14:21:38.343Z [INFO] Committed .github/repo.yml to jefeish-training/safe-settings-config@main +2025-09-18T14:21:38.554Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:38.554Z [DEBUG] Is Admin repo event true +2025-09-18T14:21:39.149Z [DEBUG] Checking existence of .github/repo.yml in jefeish-test1/safe-settings-config +2025-09-18T14:21:39.418Z [INFO] File .github/repo.yml not found in jefeish-test1/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:39.418Z [INFO] Syncing repo.yml to jefeish-test1 (mergeStrategy=merge) +2025-09-18T14:21:39.482Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:40.470Z [INFO] Committed .github/repo.yml to jefeish-test1/safe-settings-config@main +2025-09-18T14:21:40.471Z [INFO] Changes pushed directly to jefeish-test1/safe-settings-config@main +2025-09-18T14:21:40.471Z [DEBUG] Preparing to sync file 'repo.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-18T14:21:40.690Z [DEBUG] Is Admin repo event true +2025-09-18T14:21:40.690Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:40.962Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-18T14:21:40.961Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-18T14:21:41.228Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:41.575Z [DEBUG] Checking existence of .github/repo.yml in jefeish-migration-test/safe-settings-config +2025-09-18T14:21:41.832Z [INFO] File .github/repo.yml not found in jefeish-migration-test/safe-settings-config (this is fine for both merge strategies) +2025-09-18T14:21:41.832Z [INFO] Syncing repo.yml to jefeish-migration-test (mergeStrategy=merge) +2025-09-18T14:21:42.191Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:42.191Z [DEBUG] Is Admin repo event true +2025-09-18T14:21:42.736Z [INFO] Committed .github/repo.yml to jefeish-migration-test/safe-settings-config@main +2025-09-18T14:21:42.736Z [DEBUG] Preparing to sync file 'repo.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-18T14:21:42.736Z [INFO] Changes pushed directly to jefeish-migration-test/safe-settings-config@main +2025-09-18T14:21:43.197Z [DEBUG] Preparing to sync file 'repo.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-18T14:21:43.197Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-18T14:21:43.647Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. +2025-09-18T14:21:43.751Z [DEBUG] No changes in '.github/settings.yml' detected, returning... +2025-09-18T14:21:54.154Z [DEBUG] Working on the default branch, returning... +2025-09-18T14:21:54.154Z [DEBUG] Is Admin repo event true diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js index 8ef694afe..24d5b5496 100644 --- a/lib/hubSyncHandler.js +++ b/lib/hubSyncHandler.js @@ -1,5 +1,62 @@ +const { minimatch } = require('minimatch') const env = require('./env') const { getInstallations } = require('./installationCache') +const yaml = require('js-yaml') +const path = require('path') +const fs = require('fs') +const os = require('os') +const util = require('util') + +/** + * Attach a file-backed logger to robot.log that mirrors all log calls to a file. + * It preserves the original behavior and appends each log line to a file, trimming + * the file to the last `maxLines` entries (default 1000). + * + * Usage: call attachFileLogger(robot, { filePath: '/tmp/safe-settings.log', maxLines: 1000 }) + */ +function attachFileLogger (robot, options = {}) { + if (!robot || !robot.log) return + if (robot.log.__fileLoggerAttached) return + const filePath = options.filePath || process.env.SAFE_SETTINGS_LOG_FILE || path.join(process.cwd(), 'hubSyncHandler.log') + const maxLines = Number(options.maxLines || process.env.SAFE_SETTINGS_LOG_FILE_MAX_LINES || 1000) + const methods = ['info', 'warn', 'debug', 'error', 'fatal', 'trace', 'notice'] + + methods.forEach(method => { + const orig = (robot.log && robot.log[method]) ? robot.log[method].bind(robot.log) : (...args) => { /* no-op */ } + robot.log[method] = (...args) => { + // call original logger so console output still occurs + try { orig(...args) } catch (e) { /* swallow */ } + + // Build a single-line message representation + try { + const msg = args.map(a => (typeof a === 'string' ? a : util.inspect(a, { depth: 2 }))).join(' ') + const line = `${new Date().toISOString()} [${method.toUpperCase()}] ${msg}` + // append and then trim to last `maxLines` + fs.appendFile(filePath, line + os.EOL, err => { + if (err) { + try { orig(`Failed to append log to ${filePath}: ${err.message}`) } catch (e) { /* swallow */ } + return + } + // trim asynchronously + fs.promises.readFile(filePath, 'utf8').then(data => { + const lines = data.split(/\r?\n/) + // Remove a possible trailing empty line created by join + if (lines.length && lines[lines.length - 1] === '') lines.pop() + if (lines.length > maxLines) { + const tail = lines.slice(-maxLines) + return fs.promises.writeFile(filePath, tail.join(os.EOL) + os.EOL, 'utf8') + } + return Promise.resolve() + }).catch(() => { /* don't break logging on trim failures */ }) + }) + } catch (e) { + try { orig(`Failed to write log to ${filePath}: ${e && e.message ? e.message : e}`) } catch (e) { /* swallow */ } + } + } + }) + + robot.log.__fileLoggerAttached = true +} /** * Get authenticated octokit client for an org installation @@ -16,6 +73,36 @@ async function getOrgInstallation (robot, orgName) { return await robot.auth(install.id) } + +// Helper to create a branch if not direct push +async function createBranchIfNeeded(githubClient, owner, repo, baseBranch, branchName, directPush, logger) { + if (!directPush) { + try { + const baseRef = await githubClient.rest.git.getRef({ owner, repo, ref: `heads/${baseBranch}` }) + const baseSha = baseRef.data.object.sha + await githubClient.rest.git.createRef({ owner, repo, ref: `refs/heads/${branchName}`, sha: baseSha }) + logger.info(`Created branch ${branchName} in ${owner}/${repo}`) + } catch (err) { + if (err.status === 422) { + logger.warn(`Branch ${branchName} already exists, continuing`) + } else { + throw err + } + } + } +} + +// Helper to create or update a file in a repo +async function createOrUpdateFile(githubClient, params, logger) { + try { + await githubClient.rest.repos.createOrUpdateFileContents(params) + logger.info(`Committed ${params.path} to ${params.owner}/${params.repo}@${params.branch}`) + } catch (err) { + logger.error(`Failed to sync file ${params.path}: ${err.message}`) + throw err + } +} + /** * Sync changed safe-settings organization files from the master admin PR * into the target organization's admin repository. @@ -26,9 +113,9 @@ async function getOrgInstallation (robot, orgName) { * @param {string} destinationFolder Base folder in destination repo where content lives (e.g. .github or .github/safe-settings) */ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationFolder) { + attachFileLogger(robot) try { robot.log.info(`Syncing safe settings for organization: ${orgName}`) - robot.log.info(`Organization: ${orgName}, Destination Repo: ${destRepo}, Destination Folder: ${destinationFolder}`) const pr = context.payload.pull_request if (!pr) { @@ -37,27 +124,16 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF } const { owner: srcOwner, repo: srcRepo } = context.repo() const pull_number = pr.number - - // Source base path where org folders live inside master admin repo - - // 'safe-settings' is the standard sub-folder path const configRoot = env.CONFIG_PATH || '.github/' const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, '') robot.log.info(`DEBUG: sourceBase='${sourceBase}'`) - - // Debug info: log env and computed paths robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`) - - // List changed files in PR const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } ) - robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`) if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) - - // Dump file objects for debugging filename issues if (files.length) { try { robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`) @@ -74,7 +150,6 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF } }) } - const orgPrefix = `${sourceBase}/${orgName}/` robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`) @@ -82,13 +157,11 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`) if (!relevant.length) { robot.log.info(`No files for org ${orgName} in PR #${pull_number}`) - // Detailed per-file checks to help debug matching files.forEach(f => { const exact = f.filename === `${sourceBase}/${orgName}` const pref = f.filename.startsWith(orgPrefix) robot.log.info(`MATCH CHECK: file='${f.filename}' exact=${exact} prefix=${pref}`) }) - // Also show alternate check using CONFIG_PATH + '/organizations' const altBase = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/organizations` const altPrefix = `${altBase}/${orgName}/` files.forEach(f => { @@ -98,88 +171,53 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF }) return } - - // Destination info const destOwner = orgName - // ensure destBase uses the configured CONFIG_PATH (fallback to '.github') and normalize trailing slash const destBase = (destinationFolder || env.CONFIG_PATH || '.github').replace(/\/$/, '') const destBaseBranch = 'main' const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') - - // Find installation for destination org to auth (reusable helper) const githubDest = await getOrgInstallation(robot, destOwner) if (!githubDest) { robot.log.warn(`Installation for destination org ${destOwner} not found; cannot sync`) return } - robot.log.info(`Syncing from ${srcOwner}/${srcRepo} PR #${pull_number} to ${destOwner}/${destRepo}@${destBaseBranch} under ${destBase} (directPush=${directPush})`) - - // Create branch if not direct push const timestamp = Date.now() const branchName = directPush ? destBaseBranch : `safe-settings-sync/pr-${pull_number}-${orgName}-${timestamp}` - if (!directPush) { - try { - const baseRef = await githubDest.rest.git.getRef({ owner: destOwner, repo: destRepo, ref: `heads/${destBaseBranch}` }) - const baseSha = baseRef.data.object.sha - await githubDest.rest.git.createRef({ owner: destOwner, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) - robot.log.info(`Created branch ${branchName} in ${destOwner}/${destRepo}`) - } catch (err) { - if (err.status === 422) { - robot.log.warn(`Branch ${branchName} already exists, continuing`) - } else { - throw err - } - } - } - + await createBranchIfNeeded(githubDest, destOwner, destRepo, destBaseBranch, branchName, directPush, robot.log) for (const f of relevant) { let relative if (f.filename === `${sourceBase}/${orgName}`) { - // top directory marker encountered (unlikely in changed files list) - skip continue } else { relative = f.filename.slice(orgPrefix.length) } - // place only the changed file under the configured CONFIG_PATH (e.g. '.github/') const destPath = `${destBase}/${relative}`.replace(/\/+/g, '/') + const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }) + const data = srcContentResp.data + if (Array.isArray(data)) { + continue + } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') + const encoded = Buffer.from(fileContent, 'utf8').toString('base64') + let existingSha try { - const srcContentResp = await context.octokit.rest.repos.getContent({ owner: srcOwner, repo: srcRepo, path: f.filename, ref: pr.head.sha }) - const data = srcContentResp.data - if (Array.isArray(data)) { - // Skip directories; individual files will appear separately in changed files list - continue - } - const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') - const encoded = Buffer.from(fileContent, 'utf8').toString('base64') - - // Check existing file for sha - let existingSha - try { - const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }) - if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha - } catch (getErr) { - if (getErr.status !== 404) throw getErr // ignore missing - } - - await githubDest.rest.repos.createOrUpdateFileContents({ - owner: destOwner, - repo: destRepo, - path: destPath, - message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, - content: encoded, - branch: branchName, - sha: existingSha, - committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, - author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } - }) - robot.log.info(`Committed ${destPath} to ${destOwner}/${destRepo}@${branchName}`) - } catch (fileErr) { - robot.log.error(`Failed to sync file ${f.filename}: ${fileErr.message}`) - throw fileErr + const destGet = await githubDest.rest.repos.getContent({ owner: destOwner, repo: destRepo, path: destPath, ref: destBaseBranch }) + if (!Array.isArray(destGet.data)) existingSha = destGet.data.sha + } catch (getErr) { + if (getErr.status !== 404) throw getErr } + await createOrUpdateFile(githubDest, { + owner: destOwner, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` : `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}`, + content: encoded, + branch: branchName, + sha: existingSha, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }, robot.log) } - if (!directPush) { try { const prTitle = `Sync safe-settings from ${srcOwner}/${srcRepo} PR #${pull_number}` @@ -205,6 +243,7 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF * @param {import('probot').Context} context */ async function hubSyncHandler (robot, context) { + attachFileLogger(robot) const { payload } = context const { repository, pull_request } = payload || {} robot.log.info(`Received 'pull_request.closed' event: ${pull_request && pull_request.number}`) @@ -238,12 +277,12 @@ async function hubSyncHandler (robot, context) { const orgsChanged = files.some(f => /\/organizations\//.test(f.filename)) if (globalsChanged) { - robot.log.info('Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).') + robot.log.debug('Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...).') await module.exports.syncHubGlobalsUpdate(robot, context, files) } if (orgsChanged) { - robot.log.info('Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...).') + robot.log.debug('Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...).') // Only sync updates in organization subfolders, not files directly in organizations folder const baseSettingsPath = `${(env.CONFIG_PATH || '.github').replace(/\/$/, '')}/${env.SAFE_SETTINGS_HUB_PATH}/organizations` const normalizedBase = baseSettingsPath.replace(/\/$/, '') @@ -277,210 +316,166 @@ async function hubSyncHandler (robot, context) { * @param {Array} files - Array of changed file objects from PR */ async function syncHubGlobalsUpdate (robot, context, files) { - robot.log.info('syncHubGlobalsUpdate: Processing globals folder changes.') - // Step 1: Load manifest.yml rules from the hub repo - const yaml = require('js-yaml') - const util = require('util') + attachFileLogger(robot) + robot.log.info(`Syncing safe settings for 'globals/'.`) const manifestPath = `${env.CONFIG_PATH}/${env.SAFE_SETTINGS_HUB_PATH}/globals/manifest.yml` let manifest try { - // Get manifest.yml from the hub repo (default branch: main) const resp = await context.octokit.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: env.SAFE_SETTINGS_HUB_REPO, path: manifestPath, ref: 'main' }) - const manifestContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') manifest = yaml.load(manifestContent) - robot.log.info('Loaded manifest.yml rules from hub repo:' + JSON.stringify(manifest, null, 2)) + robot.log.debug('Loaded manifest.yml rules from hub repo:' + JSON.stringify(manifest, null, 2)) } catch (err) { robot.log.error('Failed to load manifest.yml from hub repo:' + err.message) return } - // Step 2: Determine which update to sync where - // Find changed files in the globals folder const changedGlobals = files.filter(f => /\/globals\//.test(f.filename)) if (!changedGlobals.length) { robot.log.info('No changed files in globals folder.') return } - - // For each changed file, match against manifest rules + // Pre-filter rules for each file, and precompute orgs for each rule + const installs = await getInstallations(robot) + const orgLogins = installs.filter(i => i.account && i.account.type === 'Organization').map(i => i.account.login) + // Precompute matching rules for each fileName in changedGlobals + const fileNameToMatchingRules = {}; + for (const fileObj of changedGlobals) { + const fileName = fileObj.filename.split('/').pop(); + fileNameToMatchingRules[fileName] = (manifest.rules || []).filter(rule => + (rule.files || []).some(pattern => minimatch(fileName, pattern)) + ); + } for (const fileObj of changedGlobals) { - const fileName = fileObj.filename.split('/').pop() - // Prevent manifest.yml from being synced to organizations + const fileName = fileObj.filename.split('/').pop(); if (fileName === 'manifest.yml') { - robot.log.info(`Skipping sync for manifest.yml (should only exist in hub)`) - continue + robot.log.debug(`Skipping sync for manifest.yml (should only exist in hub)`); + continue; } - robot.log.info(`Evaluating globals file: ${fileObj.filename}`) - for (const rule of manifest.rules || []) { - // Check if file matches rule.files (glob match, simple * and exact) - const matchesFile = (rule.files || []).some(pattern => { - if (pattern === fileName) return true - if (pattern.startsWith('*') && fileName.endsWith(pattern.slice(1))) return true - if (pattern.endsWith('*') && fileName.startsWith(pattern.slice(0, -1))) return true - if (pattern.includes('*')) { - // Simple contains match for * - const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') - return regex.test(fileName) - } - return false - }) - if (!matchesFile) continue - - // Determine target orgs - const targets = rule.targets || [] - robot.log.info(`Rule '${rule.name}' matches file '${fileName}'. Targets: ${targets.join(', ')}`) - // Step 3: handle mergeStrategy and actual sync - const mergeStrategy = rule.mergeStrategy || 'merge' - for (const orgPattern of targets) { - // For demo, treat '*' as all orgs, otherwise match orgs by pattern - let orgsToSync = [] + robot.log.debug(`Evaluating globals file: ${fileObj.filename}`); + // Use precomputed matching rules + const matchingRules = fileNameToMatchingRules[fileName]; + for (const rule of matchingRules) { + const mergeStrategy = rule.mergeStrategy || 'merge'; + // Precompute orgs to sync for each target pattern + let orgsToSync = []; + for (const orgPattern of rule.targets || []) { if (orgPattern === '*') { - // Get all org installations - const installs = await getInstallations(robot) - orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization').map(i => i.account.login) + orgsToSync.push(...orgLogins); } else if (orgPattern.endsWith('*')) { - // Prefix match - const prefix = orgPattern.slice(0, -1) - const installs = await getInstallations(robot) - orgsToSync = installs.filter(i => i.account && i.account.type === 'Organization' && i.account.login.startsWith(prefix)).map(i => i.account.login) + const prefix = orgPattern.slice(0, -1); + orgsToSync.push(...orgLogins.filter(login => login.startsWith(prefix))); } else { - orgsToSync = [orgPattern] + orgsToSync.push(orgPattern); } - for (const orgName of orgsToSync) { - robot.log.info(`Preparing to sync file '${fileName}' to org '${orgName}' with mergeStrategy='${mergeStrategy}'`) - // Check if org has a safe-settings config repo (repo exists) - const destRepo = env.ADMIN_REPO - // use the octokit client authenticated for the hub installation - const githubDest = await getOrgInstallation(robot, orgName) - if (!githubDest) { - robot.log.info(`Skipping org ${orgName}: no installation found.`) - continue + } + // Remove duplicates + orgsToSync = Array.from(new Set(orgsToSync)); + robot.log.debug(`Rule '${rule.name}' matches file '${fileName}'. Targets: ${orgsToSync.join(', ')}`); + for (const orgName of orgsToSync) { + robot.log.debug(`Preparing to sync file '${fileName}' to org '${orgName}' with mergeStrategy='${mergeStrategy}'`); + const destRepo = env.ADMIN_REPO; + const githubDest = await getOrgInstallation(robot, orgName); + if (!githubDest) { + robot.log.info(`Skipping org ${orgName}: no installation found.`); + continue; + } + let repoExists = false; + try { + await githubDest.repos.get({ owner: orgName, repo: destRepo }); + repoExists = true; + } catch (err) { + if (err.status === 404) { + robot.log.info(`Skipping org ${orgName}: config repo '${destRepo}' does not exist.`); + continue; + } else { + throw err; } - let repoExists = false - try { - await githubDest.repos.get({ owner: orgName, repo: destRepo }) - repoExists = true - } catch (err) { - if (err.status === 404) { - robot.log.info(`Skipping org ${orgName}: config repo '${destRepo}' does not exist.`) - continue - } else { - throw err - } + } + if (!repoExists) continue; + const destPath = `${env.CONFIG_PATH}/${fileName}`; + let exists = false; + let existingSha = undefined; + try { + robot.log.debug(`Checking existence of ${destPath} in ${orgName}/${destRepo}`); + const resp = await githubDest.repos.getContent({ + owner: orgName, + repo: destRepo, + path: destPath, + ref: 'main' + }); + if (!Array.isArray(resp.data)) { + robot.log.debug(`Found ${destPath} in ${orgName}/${destRepo}`); + exists = true; + existingSha = resp.data.sha; } - if (!repoExists) continue - // Check if file exists in org's repo - const destPath = `${env.CONFIG_PATH}/${fileName}` - let exists = false - let existingContent = null - try { - robot.log.info(`Checking existence of ${destPath} in ${orgName}/${destRepo}`) - const resp = await githubDest.repos.getContent({ - owner: orgName, - repo: destRepo, - path: destPath, - ref: 'main' - }) - if (!Array.isArray(resp.data)) { - robot.log.info(`Found ${destPath} in ${orgName}/${destRepo}`) - exists = true - existingContent = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8') - } - } catch (err) { - if (err.status === 404) { - robot.log.info(`File ${destPath} not found in ${orgName}/${destRepo} (this is fine for both merge strategies)`) - exists = false - existingContent = null - } else { - robot.log.info(`Error checking ${destPath} in ${orgName}/${destRepo}: ${err.message}`) - throw err - } + } catch (err) { + if (err.status === 404) { + robot.log.info(`File ${destPath} not found in ${orgName}/${destRepo} (this is fine for both merge strategies)`); + exists = false; + existingSha = undefined; + } else { + robot.log.error(`Error checking ${destPath} in ${orgName}/${destRepo}: ${err.message}`); + throw err; } - // Merge strategy logic - if (mergeStrategy === 'merge' && exists) { - robot.log.info(`Skipping sync of ${fileName} to ${orgName} (already exists & mergeStrategy=${mergeStrategy})`) - continue + } + if (mergeStrategy === 'merge' && exists) { + robot.log.info(`Skipping sync of ${fileName} to ${orgName} (already exists & mergeStrategy=${mergeStrategy})`); + continue; + } + robot.log.info(`Syncing ${fileName} to ${orgName} (mergeStrategy=${mergeStrategy})`); + try { + let srcContentResp; + const pr = context.payload && context.payload.pull_request; + const srcRef = pr && pr.head && pr.head.sha ? pr.head.sha : 'main'; + srcContentResp = await context.octokit.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fileObj.filename, + ref: srcRef + }); + const data = srcContentResp.data; + if (Array.isArray(data)) { + robot.log.debug(`Skipping directory ${fileObj.filename}`); + continue; } - // For overwrite or merge with no existing file, sync - robot.log.info(`Syncing ${fileName} to ${orgName} (mergeStrategy=${mergeStrategy})`) - // Actual sync logic: create or update file in org repo - try { - // Get source file content from hub repo (use PR head SHA if available, else main) - let srcContentResp - const pr = context.payload && context.payload.pull_request - const srcRef = pr && pr.head && pr.head.sha ? pr.head.sha : 'main' - srcContentResp = await context.octokit.repos.getContent({ - owner: env.SAFE_SETTINGS_HUB_ORG, - repo: env.SAFE_SETTINGS_HUB_REPO, - path: fileObj.filename, - ref: srcRef - }) - const data = srcContentResp.data - if (Array.isArray(data)) { - robot.log.info(`Skipping directory ${fileObj.filename}`) - continue - } - const fileContent = Buffer.from(data.content, data.encoding).toString('utf8') - const encoded = Buffer.from(fileContent, 'utf8').toString('base64') - - // Prepare commit message and branch - const destBaseBranch = 'main' - const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1') - const timestamp = Date.now() - const branchName = directPush ? destBaseBranch : `safe-settings-globals-sync/${orgName}-${fileName}-${timestamp}` - - // Create branch if not direct push - if (!directPush) { - try { - const baseRef = await githubDest.rest.git.getRef({ owner: orgName, repo: destRepo, ref: `heads/${destBaseBranch}` }) - const baseSha = baseRef.data.object.sha - await githubDest.rest.git.createRef({ owner: orgName, repo: destRepo, ref: `refs/heads/${branchName}`, sha: baseSha }) - robot.log.info(`Created branch ${branchName} in ${orgName}/${destRepo}`) - } catch (err) { - if (err.status === 422) { - robot.log.warn(`Branch ${branchName} already exists, continuing`) - } else { - throw err - } - } + const fileContent = Buffer.from(data.content, data.encoding).toString('utf8'); + const encoded = Buffer.from(fileContent, 'utf8').toString('base64'); + const destBaseBranch = 'main'; + const directPush = (env.SAFE_SETTINGS_HUB_DIRECT_PUSH === 'true' || env.SAFE_SETTINGS_HUB_DIRECT_PUSH === '1'); + const timestamp = Date.now(); + const branchName = directPush ? destBaseBranch : `safe-settings-globals-sync/${orgName}-${fileName}-${timestamp}`; + await createBranchIfNeeded(githubDest, orgName, destRepo, destBaseBranch, branchName, directPush, robot.log); + await createOrUpdateFile(githubDest, { + owner: orgName, + repo: destRepo, + path: destPath, + message: directPush ? `Direct sync globals file '${fileName}' from hub` : `Sync globals file '${fileName}' from hub`, + content: encoded, + branch: branchName, + sha: exists ? existingSha : undefined, + committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, + author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } + }, robot.log); + if (!directPush) { + try { + const prTitle = `Sync globals file '${fileName}' from hub`; + const prBody = `Automated sync of globals file '${fileName}' from hub to ${orgName}.`; + const created = await githubDest.rest.pulls.create({ owner: orgName, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }); + robot.log.info(`Created PR ${created.data.html_url} in ${orgName}/${destRepo}`) + } catch (prErr) { + robot.log.error(`Failed to create PR in ${orgName}/${destRepo}: ${prErr.message}`) + throw prErr } - - // Create or update file - await githubDest.rest.repos.createOrUpdateFileContents({ - owner: orgName, - repo: destRepo, - path: destPath, - message: directPush ? `Direct sync globals file '${fileName}' from hub` : `Sync globals file '${fileName}' from hub`, - content: encoded, - branch: branchName, - sha: exists ? (await githubDest.repos.getContent({ owner: orgName, repo: destRepo, path: destPath, ref: branchName })).data.sha : undefined, - committer: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' }, - author: { name: 'Safe Settings Bot', email: 'safe-settings-bot@example.com' } - }) - robot.log.info(`Committed ${destPath} to ${orgName}/${destRepo}@${branchName}`) - - // Create PR if not direct push - if (!directPush) { - try { - const prTitle = `Sync globals file '${fileName}' from hub` - const prBody = `Automated sync of globals file '${fileName}' from hub to ${orgName}.` - const created = await githubDest.rest.pulls.create({ owner: orgName, repo: destRepo, title: prTitle, head: branchName, base: destBaseBranch, body: prBody }) - robot.log.info(`Created PR ${created.data.html_url} in ${orgName}/${destRepo}`) - } catch (prErr) { - robot.log.error(`Failed to create PR in ${orgName}/${destRepo}: ${prErr.message}`) - throw prErr - } - } else { - robot.log.info(`Changes pushed directly to ${orgName}/${destRepo}@${destBaseBranch}`) - } - } catch (syncErr) { - robot.log.error(`Failed to sync globals file ${fileName} to ${orgName}: ${syncErr.message}`) + } else { + robot.log.info(`Changes pushed directly to ${orgName}/${destRepo}@${destBaseBranch}`) } + } catch (syncErr) { + robot.log.error(`Failed to sync globals file ${fileName} to ${orgName}: ${syncErr.message}`) } } } @@ -497,7 +492,7 @@ async function syncHubGlobalsUpdate (robot, context, files) { * @returns {Promise>} Results of the operation for each organization */ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { - const path = require('path') + attachFileLogger(robot) const results = [] try { if (!Array.isArray(orgNames) || orgNames.length === 0) return results @@ -585,6 +580,7 @@ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { results.push({ org: orgName, error: `failed to check destination: ${probeErr.message}` }) continue } + // 404 -> not present, proceed } } catch (e) { diff --git a/lib/routes.js b/lib/routes.js index 842350601..7a65ac847 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -605,6 +605,54 @@ function setupRoutes (robot, getRouter) { } }) + + // GET /api/safe-settings/hub/log + // Returns parsed log entries (JSON): [{ timestamp, level, message }, ...] + router.get('/api/safe-settings/hub/log', async (req, res) => { + const lines = parseInt(req.query.lines || process.env.SAFE_SETTINGS_LOG_FILE_MAX_LINES || '1000', 10) + const levelsQuery = req.query.levels // comma-separated e.g. 'ERROR,WARN' + const allowedLevels = levelsQuery ? new Set(String(levelsQuery).split(',').map(s => s.trim().toUpperCase()).filter(Boolean)) : null + + const candidates = [] + if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE) + candidates.push(path.join(rootDir, 'safe-settings.log')) + candidates.push(path.join(rootDir, '..', 'safe-settings.log')) + candidates.push(path.join(rootDir, 'ui', 'safe-settings.log')) + + let found = null + for (const p of candidates) { + if (!p) continue + try { + const st = await fs.promises.stat(p) + if (st && st.isFile()) { found = p; break } + } catch (e) { + // ignore + } + } + if (!found) return res.status(404).json({ error: 'Log file not found' }) + + try { + const data = await fs.promises.readFile(found, 'utf8') + const arr = data.split(/\r?\n/).filter(Boolean) + const tail = arr.slice(-lines) + const parsed = tail.map(line => { + // Expecting format: 2025-09-10T12:34:56.789Z [INFO] message + const m = line.match(/^(\d{4}-\d{2}-\d{2}T[^\s]+)\s+\[([A-Z]+)\]\s+(.*)$/) + if (m) { + return { timestamp: m[1], level: m[2], message: m[3], raw: line } + } + // fallback: try to extract level in brackets + const m2 = line.match(/\[([A-Z]+)\]\s*(.*)$/) + if (m2) return { timestamp: null, level: m2[1], message: m2[2], raw: line } + return { timestamp: null, level: 'UNKNOWN', message: line, raw: line } + }) + const filtered = allowedLevels ? parsed.filter(p => allowedLevels.has(String(p.level).toUpperCase())) : parsed + return res.json({ count: filtered.length, entries: filtered }) + } catch (err) { + return res.status(500).json({ error: err && err.message ? err.message : String(err) }) + } + }) + return router } diff --git a/package-lock.json b/package-lock.json index 2d0164b43..6fb37db30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "eta": "^3.5.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", - "minimatch": "^10.0.1", + "minimatch": "^10.0.3", "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", @@ -1324,6 +1324,27 @@ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "license": "MIT" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -5046,6 +5067,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/before-after-hook": { @@ -5112,15 +5134,6 @@ "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" }, - "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/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -10864,12 +10877,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" diff --git a/package.json b/package.json index 63a2337da..8c6aee56d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "eta": "^3.5.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", - "minimatch": "^10.0.1", + "minimatch": "^10.0.3", "next": "^15.5.2", "node-cron": "^3.0.2", "octokit": "^5.0.2", diff --git a/safe-settings.log b/safe-settings.log new file mode 100644 index 000000000..962c13d3d --- /dev/null +++ b/safe-settings.log @@ -0,0 +1,54 @@ +2025-09-11T01:43:41.125Z [INFO] Received 'pull_request.closed' event: 45 +2025-09-11T01:43:41.125Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2025-09-11T01:43:42.072Z [INFO] Files changed in PR #45: .github/safe-settings/globals/suborg.yml +2025-09-11T01:43:42.072Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). +2025-09-11T01:43:42.073Z [INFO] Syncing safe settings for 'globals/'. +2025-09-11T01:43:42.359Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + "rules": [ + { + "name": "global-defaults", + "targets": [ + "*" + ], + "files": [ + "*.yml" + ], + "mergeStrategy": "merge" + }, + { + "name": "security-policies", + "targets": [ + "acme-*", + "foo-bar" + ], + "files": [ + "settings.yml" + ], + "mergeStrategy": "overwrite" + } + ] +} +2025-09-11T01:43:42.361Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml +2025-09-11T01:43:42.361Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' +2025-09-11T01:43:42.361Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu +2025-09-11T01:43:42.988Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T01:43:43.292Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) +2025-09-11T01:43:43.292Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config +2025-09-11T01:43:43.292Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' +2025-09-11T01:43:43.773Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T01:43:44.077Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config +2025-09-11T01:43:44.078Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) +2025-09-11T01:43:44.078Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' +2025-09-11T01:43:44.793Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T01:43:45.082Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config +2025-09-11T01:43:45.082Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) +2025-09-11T01:43:45.082Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' +2025-09-11T01:43:45.593Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. +2025-09-11T01:43:45.593Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' +2025-09-11T01:43:46.208Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T01:43:46.461Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) +2025-09-11T01:43:46.461Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config +2025-09-11T01:43:46.461Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' +2025-09-11T01:43:46.897Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. +2025-09-11T01:43:46.897Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' +2025-09-11T01:43:47.342Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/api/logs/route.js b/ui/src/app/api/logs/route.js new file mode 100644 index 000000000..ec873364a --- /dev/null +++ b/ui/src/app/api/logs/route.js @@ -0,0 +1,28 @@ +import fs from 'fs/promises' +import path from 'path' + +export const dynamic = 'force-static' + +async function findLogFile () { + const candidates = [] + if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE) + candidates.push(path.join(process.cwd(), 'safe-settings.log')) + candidates.push(path.join(process.cwd(), '..', 'safe-settings.log')) + candidates.push(path.join(process.cwd(), '..', '..', 'safe-settings.log')) + + for (const p of candidates) { + if (!p) continue + try { + const st = await fs.stat(p) + if (st && st.isFile()) return p + } catch (e) { + // ignore + } + } + return null +} + +export async function GET () { + const msg = 'Disabled in static export: use the backend endpoint /api/safe-settings/logs or set SAFE_SETTINGS_LOG_FILE to point at the log file.' + return new Response(msg, { status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' } }) +} diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index f24b10862..893537339 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -108,21 +108,39 @@ export default function TitleBar() { )} +
  • + + + + + About + {pathname === "/dashboard/help" && ( + + )} + +
  • +
  • + + + + + Sync-Logs + {pathname === "/dashboard/logs" && ( + + )} + +
  • - - - - - About - {pathname === "/dashboard/help" && ( - - )} - diff --git a/ui/src/app/dashboard/logs/page.jsx b/ui/src/app/dashboard/logs/page.jsx new file mode 100644 index 000000000..184e6da63 --- /dev/null +++ b/ui/src/app/dashboard/logs/page.jsx @@ -0,0 +1,110 @@ +"use client" +import TitleBar from '../../components/TitleBar' +import { useState } from 'react' + +export default function LogsPage () { + // Static mock data for demonstration + const mockEntries = [ + { timestamp: '2025-09-11T10:00:00.000Z', level: 'INFO', message: 'Safe Settings service started.' }, + { timestamp: '2025-09-11T10:01:05.123Z', level: 'WARN', message: 'Config file missing, using defaults.' }, + { timestamp: '2025-09-11T10:02:10.456Z', level: 'ERROR', message: 'Failed to sync settings: network error.' }, + { timestamp: '2025-09-11T10:03:00.789Z', level: 'DEBUG', message: 'Polling GitHub API for updates.' }, + { timestamp: '2025-09-11T10:04:15.000Z', level: 'INFO', message: 'Sync completed successfully.' }, + { timestamp: '2025-09-11T10:05:00.000Z', level: 'INFO', message: 'SYNC: Organization settings updated.' }, + { timestamp: '2025-09-11T10:06:00.000Z', level: 'ERROR', message: 'SYNC: Failed to update organization settings.' } + ] + + const logLevels = ['INFO', 'WARN', 'DEBUG', 'ERROR'] + const [selectedLevels, setSelectedLevels] = useState(new Set(logLevels)) + const [search, setSearch] = useState('') + + const toggleLevel = (lvl) => { + const next = new Set(selectedLevels) + if (next.has(lvl)) next.delete(lvl) + else next.add(lvl) + setSelectedLevels(next) + } + + const filtered = mockEntries.filter(e => + selectedLevels.has(e.level.toUpperCase()) && + (search.trim() === '' || e.message.toLowerCase().includes(search.trim().toLowerCase())) + ) + + return ( + <> + +
    +
    +
    +
    +

    Safe Settings Log

    +

    View recent log entries for Safe Settings operations and syncs.

    +
    +
    +
    +
    +
    +
    +
    Filter Options
    +
    + Log Levels: +
    + {logLevels.map(lvl => ( + + ))} +
    +
    +
    + Search Message: + setSearch(e.target.value)} + style={{ maxWidth: 300 }} + /> +
    +
    +
    +
    +
    +
    +
    +
    Log Entries
    +
    + + + + + + + + + + {filtered.map((row, i) => { + let levelClass = '' + if (row.level === 'ERROR') levelClass = 'log-error' + else if (row.level === 'WARN') levelClass = 'log-warn' + return ( + + + + + + ) + })} + +
    TimestampLevelMessage
    {row.timestamp || '-'}{row.level || 'UNKNOWN'}{row.message}
    + {filtered.length === 0 &&
    No log entries match your filters.
    } +
    +
    +
    +
    +
    + + ) +} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css index 09a592e7c..0dbf38c60 100644 --- a/ui/src/app/globals.css +++ b/ui/src/app/globals.css @@ -266,4 +266,12 @@ tr td { background-color: var(--bg-primary) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; +} + +.log-error { + color: #c00 !important; +} + +.log-warn { + color: #b8860b !important; } \ No newline at end of file From 5188801a72e0c979942438a9c3f3914d8b828888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Thu, 2 Oct 2025 11:01:32 -0400 Subject: [PATCH 05/22] updated README --- docs/hubSyncHandler/README.md | 198 ++++++++++++++++++++++++++++++++-- 1 file changed, 190 insertions(+), 8 deletions(-) diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index df08f0f6f..480acd2b3 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -4,25 +4,208 @@ ## Overview -This feature adds a hub‑and‑spoke synchronization capability to Safe Settings. +This adds the **hub‑and‑spoke synchronization capability** to Safe Settings. -One central **master admin repository** (the hub) serves as the authoritative source of configuration which is automatically propagated to each organization’s **admin repository** (the spokes). +One central **master admin repository** (the hub) serves as the authoritative source of configuration ('Master' Admin Config Repo) which is automatically propagated to each organization’s **admin repository** (the spokes). -**Note:** When something changes in the central repo, only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync with little manual work. +**Note:** When something changes in the 'Master' repo (the hub), only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync. ## Sync Lifecycle (High Level) ```mermaid graph TD -A0(PR Closed) --> A1(HUB Admin Repo) +A0(PR Closed Event) --> A1(HUB Admin Repo) A1(ORG Admin Repo) --> B(ORG Admin Repo) A1(HUB Admin Repo) --> C(ORG Admin Repo) A1(HUB Admin Repo) --> D(ORG Admin Repo) ``` -## Environment Variables & Inputs +## Gettings Started -Environment variables specific to the 'Sync-Feature' +>**Note:** for the standard setup lets assume that Safe-Settings configuration on the Admin Config Repos (Spokes) are stored in `.github/` + +These are the basic steps to setup the Enterprise-Level Safe-Settings, using **Hub-sync** support. + +### ✅ Step 1: Register the App +**Register the Safe-Settings App** in your Enterprise (Enterprise App) or in your Organization. + +For App "installation tragets" (Where can this GitHub App be installed?) +Choose ***Any account*** + +### ✅ Step 2: Install the App +**Install the Safe-Settings App** in any Organzation that you would like Safe-Settings to manage. + +### ✅ Step 3: Create the 'Org-Level' Safe-Settings Admin Config Repo (Spokes) +Create the Org-Level Repo that is your dedicated Safe-Settings Config Repo and will hold all Safe-Settings configurations for the Org. + +### ✅ Step 4: Create the 'Master' Safe-Settings Admin Config Repo (Hub) +Choose any Organization where the Safe-Settings App is installed and create a 'Master' Safe-Settings Admin Config Repo. + +The Repository requires a standard directory structure for storing the config data: +```bash +.github/ +└─ safe-settings/ + ├── globals/ + │ └── manifest.yml + └── organizations/ + ├── org1/ + │ └── ...yml + └── org2/ + └── ...yml +``` + +Notes: +- The `manifest.yml` is a required file, that defines rules for syncing **Global** Safe-Settings configurations. We will address the content format later. +- `org1` and `org2` are just examples and should be replaced with the real names of the Orgs that you want to manage with the **Hub-Sync**. + +### ✅ Step 5: Configure the 'Master' Safe-Settings Admin Config Repo (Hub) + +The **Hub-Sync** feature supports two options +1. **Organization Sync:** +Any settings file in the `organizations/` directory will be synced to the specific `` (Spoke) Admin config Repo subfolder (eg.: /.github/). Only updated files are sync'd to the ORG admin config Repo (spokes). +1. **Global Sync:** Any settings file in the `globals/` directory will be synced to the specific `` (Spoke) Admin config Repo subfolder (eg.: /.github/). + + :warning: The actual sync operation is based on the rules defined in the `globals/manifest.yml`. The rules provide fine grained control over the sync targets and sync strategy. + +These two options only require that you provide the files you would like to sync, in the correct sub-directory. + +#### ✅ Step 5.1: Configure the `manifest.yml` in the 'Master' Safe-Settings Admin Config Repo (Hub) + +The `manifest.yml` defines the sync rules for global settings distribution. +- Sample `manifest.yml` + + ``` + rules: + - name: global-defaults + # specify the target ORG(s) + targets: + - "*" + files: + - "*.yml" + + # mergeStrategy: merge | overwrite | preserve + # -------------------------------------------- + # merge = use a PR to sync files + # overwrite = sync all files to the target ORG(s) (no PR) + mergeStrategy: merge + + - name: security-policies + # specify the target ORG(s) + targets: + - "acme-*" + - "foo-bar" + files: + - settings.yml + mergeStrategy: overwrite + + # optional toggle, default true + # enabled: false + ``` + +### Example Rule Breakdown + +```yaml +- name: global-defaults + targets: + - "*" + files: + - "*.yml" + mergeStrategy: merge +``` +- **Purpose:** Sync all YAML files to all organizations, merging changes via PR. + +```yaml +- name: security-policies + targets: + - "acme-*" + - "foo-bar" + files: + - settings.yml + mergeStrategy: overwrite + enabled: false +``` + +- **Purpose:** Overwrite `settings.yml` in specific organizations, but currently disabled. + + +### `manifest.yml` Reference + +The `manifest.yml` file defines synchronization rules for Safe-Settings hub-and-spoke configuration management. Each rule specifies which organizations and files to target, and how to handle synchronization. + +### Top-Level Structure + +```yaml +rules: + - name: + targets: [, ...] + files: [, ...] + mergeStrategy: + enabled: # optional + # ...additional fields as needed +``` + +--- + +### Elements + +#### `rules` +- **Type:** Array of objects +- **Description:** List of synchronization rules. Each rule controls how specific files are synced to target organizations. + +#### Rule Object + +##### `name` +- **Type:** String +- **Description:** Unique identifier for the rule. Used for reference and logging. +- **Example:** `global-defaults`, `security-policies` + +##### `targets` +- **Type:** Array of strings +- **Description:** List of organization names or patterns to apply the rule to. + - `"*"`: All organizations + - `"acme-*"`: Organizations with names starting with `acme-` + - `"foo-bar"`: Specific organization +- **Example:** + ```yaml + targets: + - "*" + - "acme-*" + - "foo-bar" + ``` + +##### `files` +- **Type:** Array of strings +- **Description:** File patterns to sync. Supports wildcards. + - `"*.yml"`: All YAML files + - `"settings.yml"`: Specific file +- **Example:** + ```yaml + files: + - "*.yml" + - "settings.yml" + ``` + +##### `mergeStrategy` +- **Type:** String (`merge`, `overwrite`, `preserve`) +- **Description:** Determines how files are synced: + - `merge`: use a PR to sync files + - `overwrite`: Sync all files, replacing existing ones (direct commit, no PR) +- **Example:** + ```yaml + mergeStrategy: merge + ``` + +##### `enabled` +- **Type:** Boolean (optional) +- **Description:** Toggle to enable or disable the rule. Default is `true`. +- **Example:** + ```yaml + enabled: false + ``` + +--- + +### Environment Variables & Inputs Specific to the **Hub-Sync** feature | Name | Purpose | Default | |------|---------|---------| @@ -79,5 +262,4 @@ The following table summarizes the Safe Settings API endpoints: GET /api/safe-settings/env ``` ---- - +--- \ No newline at end of file From de8bab4af871226997265ab3cba87b61aad0a504 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:03:28 -0400 Subject: [PATCH 06/22] handle multiple changes as a batch --- index.js | 56 +++++++++++++++++++------------------------------ lib/settings.js | 38 ++++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/index.js b/index.js index e6fd1c8d7..ebb1ff929 100644 --- a/index.js +++ b/index.js @@ -40,7 +40,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { + async function syncSettings (nop, context, repo = context.repo(), ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -48,7 +48,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const runtimeConfig = await configManager.loadGlobalSettingsYaml() const config = Object.assign({}, deploymentConfig, runtimeConfig) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref) + return Settings.sync(nop, context, repo, config, ref) } catch (e) { if (nop) { let filename = env.SETTINGS_FILE_PATH @@ -65,7 +65,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncSettings (nop, context, repo = context.repo(), ref) { + async function syncSelectedSettings (nop, context, repos, subOrgs, ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -73,7 +73,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const runtimeConfig = await configManager.loadGlobalSettingsYaml() const config = Object.assign({}, deploymentConfig, runtimeConfig) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.sync(nop, context, repo, config, ref) + if (ref) { + return Settings.syncSelectedRepos(nop, context, repos, subOrgs, config, ref) + } else { + return Settings.syncSelectedRepos(nop, context, repos, subOrgs, config) + } } catch (e) { if (nop) { let filename = env.SETTINGS_FILE_PATH @@ -81,9 +85,9 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => filename = env.DEPLOYMENT_CONFIG_FILE_PATH deploymentConfig = {} } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + const nopcommand = new NopCommand(filename, context.repo(), null, e, 'ERROR') robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + Settings.handleError(nop, context, context.repo(), deploymentConfig, ref, nopcommand) } else { throw e } @@ -264,17 +268,11 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } const repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) - if (repoChanges.length > 0) { - return Promise.all(repoChanges.map(repo => { - return syncSettings(false, context, repo) - })) - } - const changes = getAllChangedSubOrgConfigs(payload) - if (changes.length) { - return Promise.all(changes.map(suborg => { - return syncSubOrgSettings(false, context, suborg) - })) + const subOrgChanges = getAllChangedSubOrgConfigs(payload) + + if (repoChanges.length > 0 || subOrgChanges.length > 0) { + return syncSelectedSettings(false, context, repoChanges, subOrgChanges) } robot.log.debug(`No changes in '${Settings.FILE_PATH}' detected, returning...`) @@ -572,15 +570,10 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => robot.log.debug(`Updating check run ${JSON.stringify(params)}`) await context.octokit.checks.update(params) - // guarding against null value from upstream libary that is - // causing a 404 and the check to stall - // from issue: https://github.com/github/safe-settings/issues/185#issuecomment-1075240374 - if (check_suite.before === '0000000000000000000000000000000000000000') { - check_suite.before = check_suite.pull_requests[0].base.sha - } - params = Object.assign(context.repo(), { basehead: `${check_suite.before}...${check_suite.after}` }) - const changes = await context.octokit.repos.compareCommitsWithBasehead(params) - const files = changes.data.files.map(f => { return f.filename }) + params = Object.assign(context.repo(), { pull_number: pull_request.number }) + + const changes = await context.octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}/files', params) + const files = changes.data.map(f => { return f.filename }) const settingsModified = files.includes(Settings.FILE_PATH) @@ -590,17 +583,10 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } const repoChanges = getChangedRepoConfigName(files, context.repo().owner) - if (repoChanges.length > 0) { - return Promise.all(repoChanges.map(repo => { - return syncSettings(true, context, repo, pull_request.head.ref) - })) - } - const subOrgChanges = getChangedSubOrgConfigName(files) - if (subOrgChanges.length) { - return Promise.all(subOrgChanges.map(suborg => { - return syncSubOrgSettings(true, context, suborg, context.repo(), pull_request.head.ref) - })) + + if (repoChanges.length > 0 || subOrgChanges.length > 0) { + return syncSelectedSettings(true, context, repoChanges, subOrgChanges, pull_request.head.ref) } // if no safe-settings changes detected, send a success to the check run diff --git a/lib/settings.js b/lib/settings.js index 6c42e439b..8d9e07b2b 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -42,6 +42,31 @@ class Settings { } } + static async syncSelectedRepos (nop, context, repos, subOrgs, config, ref) { + const settings = new Settings(nop, context, context.repo(), config, ref) + + try { + for (const repo of repos) { + settings.repo = repo + await settings.loadConfigs(repo) + if (settings.isRestricted(repo.repo)) { + continue + } + await settings.updateRepos(repo) + } + for (const suborg of subOrgs) { + settings.subOrgConfigMap = [suborg] + settings.suborgChange = !!suborg + await settings.loadConfigs() + await settings.updateAll() + } + await settings.handleResults() + } catch (error) { + settings.logError(error.message) + await settings.handleResults() + } + } + static async sync (nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) try { @@ -506,17 +531,20 @@ ${this.results.reduce((x, y) => { log.debug('Fetching repositories') return github.paginate('GET /installation/repositories').then(repositories => { return Promise.all(repositories.map(repository => { - if (this.isRestricted(repository.name)) { - return null - } - const { owner, name } = repository - return this.updateRepos({ owner: owner.login, repo: name }) + return this.checkAndProcessRepo(owner.login, name) }) ) }) } + async checkAndProcessRepo (owner, name) { + if (this.isRestricted(name)) { + return null + } + return this.updateRepos({ owner, repo: name }) + } + /** * Loads a file from GitHub * From fa00d78c38df52ba66203e254fac623b0b2a2b46 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:07:36 -0400 Subject: [PATCH 07/22] Update index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ebb1ff929..608a2b8a8 100644 --- a/index.js +++ b/index.js @@ -572,7 +572,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => params = Object.assign(context.repo(), { pull_number: pull_request.number }) - const changes = await context.octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}/files', params) + const changes = await context.octokit.pulls.listFiles(params) const files = changes.data.map(f => { return f.filename }) const settingsModified = files.includes(Settings.FILE_PATH) From b87397cf4579da0ff4da07cb3bf24b2be535b10a Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:08:05 -0400 Subject: [PATCH 08/22] Update index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/index.js b/index.js index 608a2b8a8..57312b710 100644 --- a/index.js +++ b/index.js @@ -73,11 +73,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const runtimeConfig = await configManager.loadGlobalSettingsYaml() const config = Object.assign({}, deploymentConfig, runtimeConfig) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - if (ref) { - return Settings.syncSelectedRepos(nop, context, repos, subOrgs, config, ref) - } else { - return Settings.syncSelectedRepos(nop, context, repos, subOrgs, config) - } + return Settings.syncSelectedRepos(nop, context, repos, subOrgs, config, ref) } catch (e) { if (nop) { let filename = env.SETTINGS_FILE_PATH From 6b358e5b81ecd35f103490ee44cba660a73f2f4f Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Sat, 4 Oct 2025 20:45:54 -0400 Subject: [PATCH 09/22] depup files in a push --- index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 57312b710..3448df382 100644 --- a/index.js +++ b/index.js @@ -263,9 +263,19 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return syncAllSettings(false, context) } - const repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) + let repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) - const subOrgChanges = getAllChangedSubOrgConfigs(payload) + let subOrgChanges = getAllChangedSubOrgConfigs(payload) + const dedupedRepos = [...new Set(repoChanges.map(r => r.repo))].map(name => { + return repoChanges.find(r => r.repo === name) + }) + repoChanges = dedupedRepos + const dedupedSubOrgs = [...new Set(subOrgChanges.map(s => s.name))].map(name => { + return subOrgChanges.find(s => s.name === name) + }) + subOrgChanges = dedupedSubOrgs + robot.log.debug(`deduped repos ${JSON.stringify(repoChanges)}`) + robot.log.debug(`deduped subOrgs ${JSON.stringify(subOrgChanges)}`) if (repoChanges.length > 0 || subOrgChanges.length > 0) { return syncSelectedSettings(false, context, repoChanges, subOrgChanges) From c971041333ac4ea6e226869bea1f10017c565220 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Sat, 4 Oct 2025 20:49:28 -0400 Subject: [PATCH 10/22] Update index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3448df382..3c54e8154 100644 --- a/index.js +++ b/index.js @@ -270,8 +270,8 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return repoChanges.find(r => r.repo === name) }) repoChanges = dedupedRepos - const dedupedSubOrgs = [...new Set(subOrgChanges.map(s => s.name))].map(name => { - return subOrgChanges.find(s => s.name === name) + const dedupedSubOrgs = [...new Set(subOrgChanges.map(s => s.repo))].map(repo => { + return subOrgChanges.find(s => s.repo === repo) }) subOrgChanges = dedupedSubOrgs robot.log.debug(`deduped repos ${JSON.stringify(repoChanges)}`) From ac8e195dd999e40bde5933ca1de6480001fb07ce Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Sun, 5 Oct 2025 09:12:26 -0400 Subject: [PATCH 11/22] moved the dedup logic --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3c54e8154..c757da2b1 100644 --- a/index.js +++ b/index.js @@ -266,8 +266,8 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => let repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) let subOrgChanges = getAllChangedSubOrgConfigs(payload) - const dedupedRepos = [...new Set(repoChanges.map(r => r.repo))].map(name => { - return repoChanges.find(r => r.repo === name) + const dedupedRepos = [...new Set(repoChanges.map(r => r.repo))].map(repo => { + return repoChanges.find(r => r.repo === repo) }) repoChanges = dedupedRepos const dedupedSubOrgs = [...new Set(subOrgChanges.map(s => s.repo))].map(repo => { From 8bc76fcf92e9cd1885105ce32773b2779bd8f75a Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Sun, 5 Oct 2025 09:14:47 -0400 Subject: [PATCH 12/22] Update index.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- index.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index c757da2b1..ca2f6dc61 100644 --- a/index.js +++ b/index.js @@ -266,14 +266,9 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => let repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) let subOrgChanges = getAllChangedSubOrgConfigs(payload) - const dedupedRepos = [...new Set(repoChanges.map(r => r.repo))].map(repo => { - return repoChanges.find(r => r.repo === repo) - }) - repoChanges = dedupedRepos - const dedupedSubOrgs = [...new Set(subOrgChanges.map(s => s.repo))].map(repo => { - return subOrgChanges.find(s => s.repo === repo) - }) - subOrgChanges = dedupedSubOrgs + repoChanges = repoChanges.filter((r, i, arr) => arr.findIndex(item => item.repo === r.repo) === i) + + subOrgChanges = subOrgChanges.filter((s, i, arr) => arr.findIndex(item => item.repo === s.repo) === i) robot.log.debug(`deduped repos ${JSON.stringify(repoChanges)}`) robot.log.debug(`deduped subOrgs ${JSON.stringify(subOrgChanges)}`) From a5ef531e35a96d48a5981bc68af2c85401fa5959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Fri, 8 May 2026 14:21:43 -0400 Subject: [PATCH 13/22] improved ui --- docs/hubSyncHandler/README.md | 2 +- docs/sample-settings/settings.yml | 24 +- hubSyncHandler.log | 1668 ++++++++++-------- lib/configManager.js | 5 + lib/hubSyncHandler.js | 43 +- lib/routes.js | 41 +- ui/package-lock.json | 2632 ++++++++++++++++++++++++++++- ui/package.json | 3 + 8 files changed, 3663 insertions(+), 755 deletions(-) diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index 480acd2b3..1f35935c9 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -15,7 +15,7 @@ One central **master admin repository** (the hub) serves as the authoritative so ```mermaid graph TD A0(PR Closed Event) --> A1(HUB Admin Repo) -A1(ORG Admin Repo) --> B(ORG Admin Repo) +A1(HUB Admin Repo) --> B(ORG Admin Repo) A1(HUB Admin Repo) --> C(ORG Admin Repo) A1(HUB Admin Repo) --> D(ORG Admin Repo) ``` diff --git a/docs/sample-settings/settings.yml b/docs/sample-settings/settings.yml index 7e19d3354..f763e9997 100644 --- a/docs/sample-settings/settings.yml +++ b/docs/sample-settings/settings.yml @@ -237,18 +237,18 @@ rulesets: # The actors that can bypass the rules in this ruleset bypass_actors: - - actor_id: number - # type: The type of actor that can bypass a ruleset - # - RepositoryRole - # - Team - # - Integration - # - OrganizationAdmin - actor_type: Team - # When the specified actor can bypass the ruleset. `pull_request` - # means that an actor can only bypass rules on pull requests. - # - always - # - pull_request - bypass_mode: pull_request + # - actor_id: number + # # type: The type of actor that can bypass a ruleset + # - RepositoryRole + # - Team + # - Integration + # - OrganizationAdmin + # actor_type: Team + # # When the specified actor can bypass the ruleset. `pull_request` + # # means that an actor can only bypass rules on pull requests. + # - always + # - pull_request + # bypass_mode: pull_request - actor_id: 1 actor_type: OrganizationAdmin diff --git a/hubSyncHandler.log b/hubSyncHandler.log index 84e9592ce..141449883 100644 --- a/hubSyncHandler.log +++ b/hubSyncHandler.log @@ -1,754 +1,1000 @@ -2025-09-11T15:47:52.340Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) -2025-09-11T15:47:52.339Z [INFO] Received 'pull_request.closed' event: 47 -2025-09-11T15:47:52.636Z [INFO] Files changed in PR #47: .github/safe-settings/globals/suborg.yml -2025-09-11T15:47:52.637Z [INFO] Syncing safe settings for 'globals/'. -2025-09-11T15:47:52.636Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). -2025-09-11T15:47:52.864Z [DEBUG] Loaded manifest.yml rules from hub repo:{ - "rules": [ +2026-05-06T21:33:39.184Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:39.184Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-159 +2026-05-06T21:33:39.184Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-159 +2026-05-06T21:33:39.520Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:39.520Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:40.568Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:40.568Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:41.153Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:41.153Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-160 +2026-05-06T21:33:41.153Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-160 +2026-05-06T21:33:41.311Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:41.310Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:42.592Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:42.592Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:43.244Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:43.245Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-161 +2026-05-06T21:33:43.245Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-161 +2026-05-06T21:33:43.401Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:43.401Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:44.679Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:44.679Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:45.190Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-162 +2026-05-06T21:33:45.190Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-162 +2026-05-06T21:33:45.190Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:45.310Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:45.310Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:46.540Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:46.540Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:47.174Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-163 +2026-05-06T21:33:47.175Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-163 +2026-05-06T21:33:47.174Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:47.288Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:47.288Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:48.525Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:48.525Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:49.183Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-164 +2026-05-06T21:33:49.183Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-164 +2026-05-06T21:33:49.183Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:49.463Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:49.463Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:50.442Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:50.442Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:51.134Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:51.134Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-166 +2026-05-06T21:33:51.134Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-166 +2026-05-06T21:33:51.400Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:51.400Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:52.528Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:52.528Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:53.073Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-168 +2026-05-06T21:33:53.073Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-168 +2026-05-06T21:33:53.073Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:53.418Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:53.418Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:54.559Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:54.559Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:55.325Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:55.325Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-169 +2026-05-06T21:33:55.325Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-169 +2026-05-06T21:33:55.430Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:55.430Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:56.585Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:56.585Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:57.375Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-167 +2026-05-06T21:33:57.375Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:57.375Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-167 +2026-05-06T21:33:57.716Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:57.716Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:58.535Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:58.535Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:33:59.175Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-165 +2026-05-06T21:33:59.175Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:33:59.175Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-165 +2026-05-06T21:33:59.404Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:33:59.404Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:00.506Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:00.506Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:00.690Z [TRACE] Fetching installations +2026-05-06T21:34:00.690Z [DEBUG] running a task every minute +2026-05-06T21:34:01.031Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2026-05-06T21:34:01.125Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:34:01.125Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-170 +2026-05-06T21:34:01.125Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-170 +2026-05-06T21:34:01.308Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2026-05-06T21:34:01.308Z [DEBUG] In getSubOrgConfigMap {"repo":"safe-settings-config","owner":"jefeish-test1"} +2026-05-06T21:34:01.308Z [DEBUG] In loadConfigMap {"owner":"jefeish-test1","repo":"safe-settings-config","path":".github/suborgs"} +2026-05-06T21:34:01.325Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:01.325Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:01.567Z [DEBUG] Error getting settings {"owner":"jefeish-test1","repo":"safe-settings-config","path":".github/suborgs"} HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:34:01.567Z [DEBUG] In getRepoConfigMap {"repo":"safe-settings-config","owner":"jefeish-test1"} +2026-05-06T21:34:01.801Z [DEBUG] repo configs = {} +2026-05-06T21:34:01.802Z [DEBUG] { + method: 'GET', + baseUrl: 'https://api.github.com', + headers: { + accept: 'application/vnd.github.v3+json', + 'user-agent': 'probot/13.4.4 octokit-core.js/5.2.0 Node.js/23', + 'x-github-api-version': '2022-11-28' + }, + mediaType: { format: '', previews: [] }, + request: { hook: [Function: bound bound register], retryCount: 1 }, + url: '/orgs/{org}/rulesets', + org: 'jefeish-test1' +} +2026-05-06T21:34:01.801Z [DEBUG] No repos directory in the safe-settings-config/.github +2026-05-06T21:34:01.802Z [DEBUG] Getting all rulesets for the org undefined +2026-05-06T21:34:02.123Z [DEBUG] [] + + [ + { + "name": "Template", + "target": "branch", + "enforcement": "active", + "bypass_actors": [ + { + "actor_id": 1, + "actor_type": "OrganizationAdmin", + "bypass_mode": "always" + }, + { + "actor_id": 7898, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + }, + { + "actor_id": 210920, + "actor_type": "Integration", + "bypass_mode": "always" + } + ], + "conditions": { + "ref_name": { + "include": [ + "~DEFAULT_BRANCH" + ], + "exclude": [ + "refs/heads/oldmaster" + ] + }, + "repository_name": { + "include": [ + "test*" + ], + "exclude": [ + "test", + "test1" + ], + "protected": true + } + }, + "rules": [ + { + "type": "creation" + }, + { + "type": "update", + "parameters": { + "update_allows_fetch_and_merge": true + } + }, + { + "type": "deletion" + }, + { + "type": "required_linear_history" + }, + { + "type": "required_signatures" + }, + { + "type": "required_deployments", + "parameters": { + "required_deployment_environments": [ + "staging" + ] + } + }, + { + "type": "pull_request", + "parameters": { + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": true, + "require_last_push_approval": true, + "required_approving_review_count": 1, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "required_status_checks": [ + { + "context": "CodeQL", + "integration_id": 1234 + }, + { + "context": "GHAS Compliance", + "integration_id": 1234 + } + ] + } + }, + { + "type": "workflows", + "parameters": { + "workflows": [ + { + "path": ".github/workflows/example.yml", + "repository_id": 123456, + "ref": "refs/heads/main", + "sha": "1234567890abcdef" + } + ] + } + }, + { + "type": "commit_message_pattern", + "parameters": { + "name": "test commit_message_pattern", + "negate": true, + "operator": "starts_with", + "pattern": "skip*" + } + }, + { + "type": "commit_author_email_pattern", + "parameters": { + "name": "test commit_author_email_pattern", + "negate": false, + "operator": "regex", + "pattern": "^.*@example.com$" + } + }, + { + "type": "committer_email_pattern", + "parameters": { + "name": "test committer_email_pattern", + "negate": false, + "operator": "regex", + "pattern": "^.*@example.com$" + } + }, + { + "type": "branch_name_pattern", + "parameters": { + "name": "test branch_name_pattern", + "negate": false, + "operator": "regex", + "pattern": ".*/.*" + } + }, + { + "type": "tag_name_pattern", + "parameters": { + "name": "test tag_name_pattern", + "negate": false, + "operator": "regex", + "pattern": ".*/.*" + } + } + ] + } +] +2026-05-06T21:34:02.124Z [DEBUG] Creating Rulesets with the following values { + "name": "Template", + "target": "branch", + "enforcement": "active", + "bypass_actors": [ { - "name": "global-defaults", - "targets": [ - "*" - ], - "files": [ - "*.yml" - ], - "mergeStrategy": "merge" + "actor_id": 1, + "actor_type": "OrganizationAdmin", + "bypass_mode": "always" }, { - "name": "security-policies", - "targets": [ - "acme-*", - "foo-bar" - ], - "files": [ - "settings.yml" - ], - "mergeStrategy": "overwrite" - } - ] -} -2025-09-11T15:47:53.106Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' -2025-09-11T15:47:53.106Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu -2025-09-11T15:47:53.106Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml -2025-09-11T15:47:53.434Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config -2025-09-11T15:47:53.680Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config -2025-09-11T15:47:53.681Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' -2025-09-11T15:47:53.681Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) -2025-09-11T15:47:54.039Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config -2025-09-11T15:47:54.273Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config -2025-09-11T15:47:54.273Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) -2025-09-11T15:47:54.273Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' -2025-09-11T15:47:54.585Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config -2025-09-11T15:47:54.886Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config -2025-09-11T15:47:54.886Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) -2025-09-11T15:47:54.886Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' -2025-09-11T15:47:55.093Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. -2025-09-11T15:47:55.093Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' -2025-09-11T15:47:55.511Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config -2025-09-11T15:47:55.758Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config -2025-09-11T15:47:55.759Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' -2025-09-11T15:47:55.759Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) -2025-09-11T15:47:55.933Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' -2025-09-11T15:47:55.933Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. -2025-09-11T15:47:56.108Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. -2025-09-11T15:47:59.386Z [DEBUG] Pull_request REopened ! -2025-09-11T15:47:59.386Z [DEBUG] Is Admin repo event false -2025-09-11T15:47:59.386Z [DEBUG] Not working on the Admin repo, returning... -2025-09-11T15:49:09.315Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} -2025-09-11T15:49:09.315Z [DEBUG] Branch Protection edited by a Human -2025-09-11T15:49:09.315Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-11T15:49:09.553Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} -2025-09-11T15:49:24.081Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) -2025-09-11T15:49:24.081Z [INFO] Received 'pull_request.closed' event: 47 -2025-09-11T15:49:24.356Z [INFO] Files changed in PR #47: .github/safe-settings/globals/suborg.yml -2025-09-11T15:49:24.356Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). -2025-09-11T15:49:24.357Z [INFO] Syncing safe settings for 'globals/'. -2025-09-11T15:49:24.617Z [DEBUG] Loaded manifest.yml rules from hub repo:{ - "rules": [ + "actor_id": 7898, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + }, { - "name": "global-defaults", - "targets": [ - "*" - ], - "files": [ - "*.yml" + "actor_id": 210920, + "actor_type": "Integration", + "bypass_mode": "always" + } + ], + "conditions": { + "ref_name": { + "include": [ + "~DEFAULT_BRANCH" ], - "mergeStrategy": "merge" + "exclude": [ + "refs/heads/oldmaster" + ] }, - { - "name": "security-policies", - "targets": [ - "acme-*", - "foo-bar" + "repository_name": { + "include": [ + "test*" ], - "files": [ - "settings.yml" + "exclude": [ + "test", + "test1" ], - "mergeStrategy": "overwrite" + "protected": true } - ] -} -2025-09-11T15:49:24.814Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jetest99' with mergeStrategy='merge' -2025-09-11T15:49:24.814Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/suborg.yml -2025-09-11T15:49:24.814Z [DEBUG] Rule 'global-defaults' matches file 'suborg.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu -2025-09-11T15:49:25.155Z [DEBUG] Is Admin repo event false -2025-09-11T15:49:25.155Z [DEBUG] Not working on the Admin repo, returning... -2025-09-11T15:49:25.341Z [DEBUG] Checking existence of .github/suborg.yml in jetest99/safe-settings-config -2025-09-11T15:49:25.565Z [DEBUG] Found .github/suborg.yml in jetest99/safe-settings-config -2025-09-11T15:49:25.566Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-training' with mergeStrategy='merge' -2025-09-11T15:49:25.566Z [INFO] Skipping sync of suborg.yml to jetest99 (already exists & mergeStrategy=merge) -2025-09-11T15:49:25.935Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-training/safe-settings-config -2025-09-11T15:49:26.172Z [DEBUG] Found .github/suborg.yml in jefeish-training/safe-settings-config -2025-09-11T15:49:26.173Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-test1' with mergeStrategy='merge' -2025-09-11T15:49:26.173Z [INFO] Skipping sync of suborg.yml to jefeish-training (already exists & mergeStrategy=merge) -2025-09-11T15:49:26.524Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-test1/safe-settings-config -2025-09-11T15:49:26.777Z [DEBUG] Found .github/suborg.yml in jefeish-test1/safe-settings-config -2025-09-11T15:49:26.777Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'copilot-for-emus' with mergeStrategy='merge' -2025-09-11T15:49:26.777Z [INFO] Skipping sync of suborg.yml to jefeish-test1 (already exists & mergeStrategy=merge) -2025-09-11T15:49:26.964Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. -2025-09-11T15:49:26.964Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' -2025-09-11T15:49:27.285Z [DEBUG] Checking existence of .github/suborg.yml in jefeish-migration-test/safe-settings-config -2025-09-11T15:49:27.487Z [DEBUG] Found .github/suborg.yml in jefeish-migration-test/safe-settings-config -2025-09-11T15:49:27.487Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-training' with mergeStrategy='merge' -2025-09-11T15:49:27.487Z [INFO] Skipping sync of suborg.yml to jefeish-migration-test (already exists & mergeStrategy=merge) -2025-09-11T15:49:27.661Z [DEBUG] Preparing to sync file 'suborg.yml' to org 'decyjphr-emu' with mergeStrategy='merge' -2025-09-11T15:49:27.661Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. -2025-09-11T15:49:27.830Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. -2025-09-11T15:50:54.611Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-11T15:50:54.611Z [DEBUG] Repository member edited by Bot -2025-09-11T15:50:55.683Z [DEBUG] Repository member edited by Bot -2025-09-11T15:50:55.683Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-11T15:50:56.556Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-11T15:50:56.556Z [DEBUG] Repository member edited by Bot -2025-09-11T15:50:57.768Z [DEBUG] Repository member edited by Bot -2025-09-11T15:50:57.768Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-11T15:51:20.652Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-11T15:51:20.652Z [DEBUG] Branch Protection edited by Bot -2025-09-11T15:51:24.560Z [DEBUG] Not triggered by Safe-settings... -2025-09-11T15:51:24.559Z [DEBUG] Check run was created! -2025-09-11T15:51:35.514Z [DEBUG] Branch Protection edited by Bot -2025-09-11T15:51:35.514Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-11T15:51:36.562Z [DEBUG] Not triggered by Safe-settings... -2025-09-11T15:51:36.562Z [DEBUG] Check run was created! -2025-09-11T15:53:20.953Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-11T15:53:22.397Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-11T15:53:22.397Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-11T15:53:22.397Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-11T15:53:22.628Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-11T15:53:22.628Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:53:22.628Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, - ref: main -2025-09-11T15:53:22.859Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-11T15:53:22.859Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:53:23.452Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-11T15:53:23.452Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-11T15:53:23.452Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-11T15:53:23.691Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-11T15:53:23.691Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-test1, - ref: main -2025-09-11T15:53:23.691Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:53:23.944Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-11T15:53:23.944Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:53:24.491Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-11T15:53:24.491Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-11T15:53:24.491Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-11T15:53:24.733Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:53:24.733Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:53:24.733Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, - ref: main -2025-09-11T15:53:25.050Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-11T15:53:25.050Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:53:25.516Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-11T15:53:25.516Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-11T15:53:25.516Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-11T15:53:25.516Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-11T15:53:25.985Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-11T15:53:25.985Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-11T15:53:25.985Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-11T15:53:26.589Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-11T15:53:26.589Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-11T15:53:26.589Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-11T15:53:26.825Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, - ref: main -2025-09-11T15:53:26.825Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:53:26.825Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:53:27.058Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-11T15:53:27.058Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:55:44.833Z [INFO] Retrieving settings from org: jetest99 -2025-09-11T15:55:45.087Z [INFO] Skipping jetest99: already present in hub -2025-09-11T15:55:45.087Z [INFO] Retrieving settings from org: jefeish-training -2025-09-11T15:55:45.298Z [INFO] Retrieving settings from org: jefeish-test1 -2025-09-11T15:55:45.298Z [INFO] Skipping jefeish-training: already present in hub -2025-09-11T15:55:45.551Z [INFO] Retrieving settings from org: copilot-for-emus -2025-09-11T15:55:45.551Z [INFO] Skipping jefeish-test1: already present in hub -2025-09-11T15:55:46.001Z [INFO] Retrieving settings from org: jefeish-migration-test -2025-09-11T15:55:46.292Z [INFO] Retrieving settings from org: decyjphr-training -2025-09-11T15:55:46.292Z [INFO] Skipping jefeish-migration-test: already present in hub -2025-09-11T15:55:46.556Z [INFO] Skipping decyjphr-training: already present in hub -2025-09-11T15:55:46.556Z [INFO] Retrieving settings from org: decyjphr-emu -2025-09-11T15:56:33.309Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-11T15:56:34.976Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-11T15:56:34.976Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-11T15:56:34.976Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-11T15:56:35.221Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, - ref: main -2025-09-11T15:56:35.221Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:35.221Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-11T15:56:35.434Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-11T15:56:35.434Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:56:36.047Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-11T15:56:36.047Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-11T15:56:36.047Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-11T15:56:36.273Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-11T15:56:36.273Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:36.273Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-test1, - ref: main -2025-09-11T15:56:36.514Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:56:36.514Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-11T15:56:37.018Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-11T15:56:37.018Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-11T15:56:37.018Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-11T15:56:37.239Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:56:37.239Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, - ref: main -2025-09-11T15:56:37.239Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:37.445Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-11T15:56:37.445Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:56:37.914Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-11T15:56:37.914Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-11T15:56:37.914Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-11T15:56:37.914Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-11T15:56:38.412Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-11T15:56:38.412Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-11T15:56:38.412Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-11T15:56:38.977Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-11T15:56:38.977Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-11T15:56:38.977Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-11T15:56:39.247Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:39.247Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:56:39.247Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, - ref: main -2025-09-11T15:56:39.484Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:56:39.485Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-11T15:56:51.776Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-11T15:56:53.217Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-11T15:56:53.217Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-11T15:56:53.217Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-11T15:56:53.436Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:53.436Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-11T15:56:53.436Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, - ref: main -2025-09-11T15:56:53.666Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-11T15:56:53.666Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:56:54.354Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-11T15:56:54.354Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-11T15:56:54.354Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-11T15:56:54.566Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:54.566Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-test1, - ref: main -2025-09-11T15:56:54.566Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-11T15:56:54.792Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:56:54.792Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-11T15:56:55.340Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-11T15:56:55.340Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-11T15:56:55.340Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-11T15:56:55.563Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:56:55.563Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:55.563Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, - ref: main -2025-09-11T15:56:55.807Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:56:55.808Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-11T15:56:56.233Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-11T15:56:56.233Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-11T15:56:56.233Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-11T15:56:56.233Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-11T15:56:56.688Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-11T15:56:56.688Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-11T15:56:56.688Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-11T15:56:57.315Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-11T15:56:57.315Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-11T15:56:57.314Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-11T15:56:57.525Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-11T15:56:57.525Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-11T15:56:57.525Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, - ref: main -2025-09-11T15:56:57.745Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-11T15:56:57.745Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-12T01:30:19.210Z [DEBUG] Check run was created! -2025-09-12T01:30:19.210Z [DEBUG] Not triggered by Safe-settings... -2025-09-13T22:42:46.364Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-13T22:42:48.651Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-13T22:42:48.651Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-13T22:42:48.651Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-13T22:42:48.902Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:42:48.902Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-13T22:42:48.902Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, - ref: main -2025-09-13T22:42:49.124Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-13T22:42:49.124Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-13T22:42:49.899Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-13T22:42:49.899Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-13T22:42:49.899Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-13T22:42:50.157Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:42:50.157Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-13T22:42:50.157Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-test1, - ref: main -2025-09-13T22:42:50.373Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-13T22:42:50.373Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-13T22:42:51.186Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-13T22:42:51.186Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-13T22:42:51.186Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-13T22:42:51.533Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-13T22:42:51.533Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, - ref: main -2025-09-13T22:42:51.533Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:42:51.901Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-13T22:42:51.901Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-13T22:42:52.487Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-13T22:42:52.487Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-13T22:42:52.487Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-13T22:42:52.487Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-13T22:42:52.978Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-13T22:42:52.979Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-13T22:42:52.978Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-13T22:42:53.877Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-13T22:42:53.877Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-13T22:42:53.877Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-13T22:42:54.131Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:42:54.131Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-13T22:42:54.131Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, - ref: main -2025-09-13T22:42:54.397Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-13T22:42:54.397Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-13T22:43:30.372Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-13T22:43:34.138Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-13T22:43:34.138Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-13T22:43:34.138Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-13T22:43:34.350Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:43:34.350Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, - ref: main -2025-09-13T22:43:34.350Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-13T22:43:34.574Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-13T22:43:34.574Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-13T22:43:35.156Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-13T22:43:35.156Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-13T22:43:35.156Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-13T22:43:35.390Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-13T22:43:35.390Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:43:35.390Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-test1, - ref: main -2025-09-13T22:43:35.778Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-13T22:43:35.778Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-13T22:43:36.334Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-13T22:43:36.334Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-13T22:43:36.334Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-13T22:43:36.548Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:43:36.548Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-13T22:43:36.548Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, - ref: main -2025-09-13T22:43:36.780Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-13T22:43:36.780Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-13T22:43:37.236Z [DEBUG] 9. [SYNC DEBUG] Org hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-13T22:43:37.236Z [DEBUG] 10. [SYNC DEBUG] Hub hash: efd3489f6ad8fd9d572bbcfeded6ee3c49104dc5478b370c6adde5184e57865e -2025-09-13T22:43:37.236Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-13T22:43:37.236Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-13T22:43:37.666Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-13T22:43:37.666Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-13T22:43:37.666Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-13T22:43:38.247Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-13T22:43:38.247Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-13T22:43:38.247Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-13T22:43:38.457Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-13T22:43:38.457Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: - owner: jefeish-training, - repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, - ref: main -2025-09-13T22:43:38.457Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-13T22:43:38.763Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-13T22:43:38.763Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-16T15:47:20.575Z [INFO] Received 'pull_request.closed' event: 10 -2025-09-16T15:47:20.577Z [INFO] Pull request.closed is not from master admin repo/org (jefeish-training/safe-settings-config-master), ignoring -2025-09-16T15:47:20.664Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... -2025-09-16T15:47:20.664Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-16T15:47:20.966Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} -2025-09-16T15:47:21.579Z [DEBUG] Is Admin repo event true -2025-09-16T15:47:21.579Z [DEBUG] Working on the default branch, returning... -2025-09-16T15:47:22.242Z [DEBUG] Branch Protection edited by Bot -2025-09-16T15:47:22.242Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2025-09-16T15:47:22.349Z [DEBUG] Not working on the default branch, returning... -2025-09-16T15:47:23.391Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:47:23.391Z [DEBUG] Check run was created! -2025-09-16T15:47:37.290Z [DEBUG] Branch Protection edited by a Human -2025-09-16T15:47:37.290Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} -2025-09-16T15:47:37.290Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-16T15:47:37.812Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} -2025-09-16T15:48:45.963Z [DEBUG] Branch Protection edited by a Human -2025-09-16T15:48:45.963Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-16T15:48:45.963Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} -2025-09-16T15:48:46.215Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} -2025-09-16T15:50:41.987Z [DEBUG] Check run was created! -2025-09-16T15:50:41.988Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:50:48.685Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-16T15:50:48.685Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} -2025-09-16T15:50:48.685Z [DEBUG] Branch Protection edited by a Human -2025-09-16T15:50:48.941Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} -2025-09-16T15:51:28.479Z [DEBUG] Check run was created! -2025-09-16T15:51:28.479Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:51:31.307Z [DEBUG] Check run was created! -2025-09-16T15:51:31.307Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:51:50.461Z [DEBUG] Check run was created! -2025-09-16T15:51:50.461Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:51:51.381Z [DEBUG] Check run was created! -2025-09-16T15:51:51.381Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:58:35.618Z [DEBUG] Branch Protection edited by a Human -2025-09-16T15:58:35.618Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-16T15:58:35.618Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} -2025-09-16T15:58:36.135Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} -2025-09-16T15:58:45.832Z [DEBUG] Check run was created! -2025-09-16T15:58:45.832Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:58:45.867Z [DEBUG] Check run was created! -2025-09-16T15:58:45.867Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:58:46.449Z [DEBUG] Is Admin repo event false -2025-09-16T15:58:46.449Z [DEBUG] Not working on the Admin repo, returning... -2025-09-16T15:58:46.724Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:58:46.724Z [DEBUG] Check run was created! -2025-09-16T15:58:49.218Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T15:58:49.218Z [DEBUG] Check run was created! -2025-09-16T16:01:29.319Z [DEBUG] Check run was created! -2025-09-16T16:01:29.319Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T17:13:27.125Z [DEBUG] Not working on the Admin repo, returning... -2025-09-16T17:13:27.125Z [DEBUG] Is Admin repo event false -2025-09-16T17:13:28.760Z [DEBUG] Check run was created! -2025-09-16T17:13:28.760Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T17:13:28.848Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T17:13:28.848Z [DEBUG] Check run was created! -2025-09-16T17:13:28.918Z [DEBUG] Check run was created! -2025-09-16T17:13:28.918Z [DEBUG] Not triggered by Safe-settings... -2025-09-16T17:14:33.551Z [DEBUG] Check run was created! -2025-09-16T17:14:33.551Z [DEBUG] Not triggered by Safe-settings... -2025-09-18T14:09:14.208Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) -2025-09-18T14:09:14.208Z [INFO] Received 'pull_request.closed' event: 48 -2025-09-18T14:09:14.585Z [INFO] Files changed in PR #48: .github/safe-settings/globals/manifest.yml -2025-09-18T14:09:14.585Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). -2025-09-18T14:09:14.586Z [INFO] Syncing safe settings for 'globals/'. -2025-09-18T14:09:14.924Z [DEBUG] Loaded manifest.yml rules from hub repo:{ + }, "rules": [ { - "name": "global-defaults", - "targets": [ - "*" - ], - "files": [ - "*.yml" - ], - "mergeStrategy": "merge" + "type": "creation" }, { - "name": "security-policies", - "targets": [ - "acme-*", - "foo-bar" - ], - "files": [ - "settings.yml" - ], - "mergeStrategy": "overwrite" + "type": "update", + "parameters": { + "update_allows_fetch_and_merge": true + } + }, + { + "type": "deletion" + }, + { + "type": "required_linear_history" + }, + { + "type": "required_signatures" + }, + { + "type": "required_deployments", + "parameters": { + "required_deployment_environments": [ + "staging" + ] + } + }, + { + "type": "pull_request", + "parameters": { + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": true, + "require_last_push_approval": true, + "required_approving_review_count": 1, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "required_status_checks": [ + { + "context": "CodeQL", + "integration_id": 1234 + }, + { + "context": "GHAS Compliance", + "integration_id": 1234 + } + ] + } + }, + { + "type": "workflows", + "parameters": { + "workflows": [ + { + "path": ".github/workflows/example.yml", + "repository_id": 123456, + "ref": "refs/heads/main", + "sha": "1234567890abcdef" + } + ] + } + }, + { + "type": "commit_message_pattern", + "parameters": { + "name": "test commit_message_pattern", + "negate": true, + "operator": "starts_with", + "pattern": "skip*" + } + }, + { + "type": "commit_author_email_pattern", + "parameters": { + "name": "test commit_author_email_pattern", + "negate": false, + "operator": "regex", + "pattern": "^.*@example.com$" + } + }, + { + "type": "committer_email_pattern", + "parameters": { + "name": "test committer_email_pattern", + "negate": false, + "operator": "regex", + "pattern": "^.*@example.com$" + } + }, + { + "type": "branch_name_pattern", + "parameters": { + "name": "test branch_name_pattern", + "negate": false, + "operator": "regex", + "pattern": ".*/.*" + } + }, + { + "type": "tag_name_pattern", + "parameters": { + "name": "test tag_name_pattern", + "negate": false, + "operator": "regex", + "pattern": ".*/.*" + } } ] } -2025-09-18T14:09:15.216Z [DEBUG] Skipping sync for manifest.yml (should only exist in hub) -2025-09-18T14:09:15.672Z [DEBUG] Check run was created! -2025-09-18T14:09:15.672Z [DEBUG] Not triggered by Safe-settings... -2025-09-18T14:09:23.527Z [DEBUG] Not working on the Admin repo, returning... -2025-09-18T14:09:23.526Z [DEBUG] Is Admin repo event false -2025-09-18T14:13:27.105Z [DEBUG] Is Admin repo event false -2025-09-18T14:13:27.105Z [DEBUG] Not working on the Admin repo, returning... -2025-09-18T14:13:56.822Z [DEBUG] Is Admin repo event false -2025-09-18T14:13:56.822Z [DEBUG] Not working on the Admin repo, returning... -2025-09-18T14:13:56.822Z [DEBUG] Pull_request opened ! -2025-09-18T14:14:06.314Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) -2025-09-18T14:14:06.314Z [INFO] Received 'pull_request.closed' event: 49 -2025-09-18T14:14:07.042Z [INFO] Files changed in PR #49: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-18T14:14:07.043Z [INFO] Syncing safe settings for organization: jefeish-training -2025-09-18T14:14:07.042Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). -2025-09-18T14:14:07.043Z [INFO] Organization: jefeish-training, Destination Repo: safe-settings-config, Destination Folder: .github -2025-09-18T14:14:07.042Z [INFO] Orgs updated in PR #49: jefeish-training -2025-09-18T14:14:07.043Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' -2025-09-18T14:14:07.043Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' -2025-09-18T14:14:07.333Z [INFO] DEBUG: PR #49 contains 1 changed file(s) -2025-09-18T14:14:07.333Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch -2025-09-18T14:14:07.333Z [INFO] DEBUG: first file object = { - "sha": "8f345e9e4d6701accc0d39f587d00950c9a17ed5", - "filename": ".github/safe-settings/organizations/jefeish-training/settings.yml", - "status": "modified", - "additions": 175, - "deletions": 175, - "changes": 350, - "blob_url": "https://github.com/jefeish-training/safe-settings-config-master/blob/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml", - "raw_url": "https://github.com/jefeish-training/safe-settings-config-master/raw/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml", - "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml?ref=ee5e72b1fcb62dda5d16fd244fe36bb072589455", - "patch": "@@ -222,178 +222,178 @@ validator:\n \n # Rulesets\n # See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options\n-rulesets:\n- - name: Template\n- # The target of the ruleset. Can be one of:\n- # - branch\n- # - tag\n- target: branch\n- # The enforcement level of the ruleset. `evaluate` allows admins to test\n- # rules before enforcing them.\n- # - disabled\n- # - active\n- # - evaluate\n- enforcement: active\n-\n- # The actors that can bypass the rules in this ruleset\n- bypass_actors:\n- - actor_id: number\n- # type: The type of actor that can bypass a ruleset\n- # - RepositoryRole\n- # - Team\n- # - Integration\n- # - OrganizationAdmin\n- actor_type: Team\n- # When the specified actor can bypass the ruleset. `pull_request`\n- # means that an actor can only bypass rules on pull requests.\n- # - always\n- # - pull_request\n- bypass_mode: pull_request\n-\n- - actor_id: 1\n- actor_type: OrganizationAdmin\n- bypass_mode: always\n-\n- - actor_id: 7898\n- actor_type: RepositoryRole\n- bypass_mode: always\n-\n- - actor_id: 210920\n- actor_type: Integration\n- bypass_mode: always\n-\n- conditions:\n- # Parameters for a repository ruleset ref name condition\n- ref_name:\n- # Array of ref names or patterns to include. One of these\n- # patterns must match for the condition to pass. Also accepts\n- # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n- # include all branches.\n- include: [\"~DEFAULT_BRANCH\"]\n-\n- # Array of ref names or patterns to exclude. The condition\n- # will not pass if any of these patterns match.\n- exclude: [\"refs/heads/oldmaster\"]\n-\n- # This condition only exists at the org level (remove for suborg and repo level rulesets)\n- repository_name:\n- # Array of repository names or patterns to include.\n- # One of these patterns must match for the condition\n- # to pass. Also accepts `~ALL` to include all\n- # repositories.\n- include: [\"test*\"]\n- # Array of repository names or patterns to exclude. The\n- # condition will not pass if any of these patterns\n- # match.\n- exclude: [\"test\", \"test1\"]\n- # Whether renaming of target repositories is\n- # prevented.\n- protected: true\n-\n- # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n- rules:\n- - type: creation\n- - type: update\n- parameters:\n- # Branch can pull changes from its upstream repository\n- update_allows_fetch_and_merge: true\n- - type: deletion\n- - type: required_linear_history\n- - type: required_signatures\n-\n- - type: required_deployments\n- parameters:\n- required_deployment_environments: [\"staging\"]\n-\n- - type: pull_request\n- parameters:\n- # Reviewable commits pushed will dismiss previous pull\n- # request review approvals.\n- dismiss_stale_reviews_on_push: true\n- # Require an approving review in pull requests that modify\n- # files that have a designated code owner\n- require_code_owner_review: true\n- # Whether the most recent reviewable push must be approved\n- # by someone other than the person who pushed it.\n- require_last_push_approval: true\n- # The number of approving reviews that are required before a\n- # pull request can be merged.\n- required_approving_review_count: 1\n- # All conversations on code must be resolved before a pull\n- # request can be merged.\n- required_review_thread_resolution: true\n-\n- # Choose which status checks must pass before branches can be merged\n- # into a branch that matches this rule. When enabled, commits must\n- # first be pushed to another branch, then merged or pushed directly\n- # to a branch that matches this rule after status checks have\n- # passed.\n- - type: required_status_checks\n- parameters:\n- # Whether pull requests targeting a matching branch must be\n- # tested with the latest code. This setting will not take\n- # effect unless at least one status check is enabled.\n- strict_required_status_checks_policy: true\n- required_status_checks:\n- - context: CodeQL\n- integration_id: 1234\n- - context: GHAS Compliance\n- integration_id: 1234\n-\n- # Choose which workflows must pass before branches can be merged.\n- - type: workflows\n- parameters:\n- workflows:\n- - path: .github/workflows/example.yml\n- # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n- # in the browser console of the repository to get the repository_id\n- repository_id: 123456\n- # One of the following:\n- # Branch or tag\n- ref: refs/heads/main\n- # Commit SHA\n- sha: 1234567890abcdef\n-\n- - type: commit_message_pattern\n- parameters:\n- name: test commit_message_pattern\n- # required:\n- # - operator\n- # - pattern\n- negate: true\n- operator: starts_with\n- # The operator to use for matching.\n- # - starts_with\n- # - ends_with\n- # - contains\n- # - regex\n- pattern: skip*\n- # The pattern to match with.\n-\n- - type: commit_author_email_pattern\n- parameters:\n- name: test commit_author_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: committer_email_pattern\n- parameters:\n- name: test committer_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: branch_name_pattern\n- parameters:\n- name: test branch_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n-\n- - type: \"tag_name_pattern\"\n- parameters:\n- name: test tag_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n+# rulesets:\n+# - name: Template\n+# # The target of the ruleset. Can be one of:\n+# # - branch\n+# # - tag\n+# target: branch\n+# # The enforcement level of the ruleset. `evaluate` allows admins to test\n+# # rules before enforcing them.\n+# # - disabled\n+# # - active\n+# # - evaluate\n+# enforcement: active\n+\n+# # The actors that can bypass the rules in this ruleset\n+# bypass_actors:\n+# - actor_id: number\n+# # type: The type of actor that can bypass a ruleset\n+# # - RepositoryRole\n+# # - Team\n+# # - Integration\n+# # - OrganizationAdmin\n+# actor_type: Team\n+# # When the specified actor can bypass the ruleset. `pull_request`\n+# # means that an actor can only bypass rules on pull requests.\n+# # - always\n+# # - pull_request\n+# bypass_mode: pull_request\n+\n+# - actor_id: 1\n+# actor_type: OrganizationAdmin\n+# bypass_mode: always\n+\n+# - actor_id: 7898\n+# actor_type: RepositoryRole\n+# bypass_mode: always\n+\n+# - actor_id: 210920\n+# actor_type: Integration\n+# bypass_mode: always\n+\n+# conditions:\n+# # Parameters for a repository ruleset ref name condition\n+# ref_name:\n+# # Array of ref names or patterns to include. One of these\n+# # patterns must match for the condition to pass. Also accepts\n+# # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n+# # include all branches.\n+# include: [\"~DEFAULT_BRANCH\"]\n+\n+# # Array of ref names or patterns to exclude. The condition\n+# # will not pass if any of these patterns match.\n+# exclude: [\"refs/heads/oldmaster\"]\n+\n+# # This condition only exists at the org level (remove for suborg and repo level rulesets)\n+# repository_name:\n+# # Array of repository names or patterns to include.\n+# # One of these patterns must match for the condition\n+# # to pass. Also accepts `~ALL` to include all\n+# # repositories.\n+# include: [\"test*\"]\n+# # Array of repository names or patterns to exclude. The\n+# # condition will not pass if any of these patterns\n+# # match.\n+# exclude: [\"test\", \"test1\"]\n+# # Whether renaming of target repositories is\n+# # prevented.\n+# protected: true\n+\n+# # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n+# rules:\n+# - type: creation\n+# - type: update\n+# parameters:\n+# # Branch can pull changes from its upstream repository\n+# update_allows_fetch_and_merge: true\n+# - type: deletion\n+# - type: required_linear_history\n+# - type: required_signatures\n+\n+# - type: required_deployments\n+# parameters:\n+# required_deployment_environments: [\"staging\"]\n+\n+# - type: pull_request\n+# parameters:\n+# # Reviewable commits pushed will dismiss previous pull\n+# # request review approvals.\n+# dismiss_stale_reviews_on_push: true\n+# # Require an approving review in pull requests that modify\n+# # files that have a designated code owner\n+# require_code_owner_review: true\n+# # Whether the most recent reviewable push must be approved\n+# # by someone other than the person who pushed it.\n+# require_last_push_approval: true\n+# # The number of approving reviews that are required before a\n+# # pull request can be merged.\n+# required_approving_review_count: 1\n+# # All conversations on code must be resolved before a pull\n+# # request can be merged.\n+# required_review_thread_resolution: true\n+\n+# # Choose which status checks must pass before branches can be merged\n+# # into a branch that matches this rule. When enabled, commits must\n+# # first be pushed to another branch, then merged or pushed directly\n+# # to a branch that matches this rule after status checks have\n+# # passed.\n+# - type: required_status_checks\n+# parameters:\n+# # Whether pull requests targeting a matching branch must be\n+# # tested with the latest code. This setting will not take\n+# # effect unless at least one status check is enabled.\n+# strict_required_status_checks_policy: true\n+# required_status_checks:\n+# - context: CodeQL\n+# integration_id: 1234\n+# - context: GHAS Compliance\n+# integration_id: 1234\n+\n+# # Choose which workflows must pass before branches can be merged.\n+# - type: workflows\n+# parameters:\n+# workflows:\n+# - path: .github/workflows/example.yml\n+# # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n+# # in the browser console of the repository to get the repository_id\n+# repository_id: 123456\n+# # One of the following:\n+# # Branch or tag\n+# ref: refs/heads/main\n+# # Commit SHA\n+# sha: 1234567890abcdef\n+\n+# - type: commit_message_pattern\n+# parameters:\n+# name: test commit_message_pattern\n+# # required:\n+# # - operator\n+# # - pattern\n+# negate: true\n+# operator: starts_with\n+# # The operator to use for matching.\n+# # - starts_with\n+# # - ends_with\n+# # - contains\n+# # - regex\n+# pattern: skip*\n+# # The pattern to match with.\n+\n+# - type: commit_author_email_pattern\n+# parameters:\n+# name: test commit_author_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: committer_email_pattern\n+# parameters:\n+# name: test committer_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: branch_name_pattern\n+# parameters:\n+# name: test branch_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"\n+\n+# - type: \"tag_name_pattern\"\n+# parameters:\n+# name: test tag_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"" +2026-05-06T21:34:02.123Z [DEBUG] Results of comparing Rulesets diffable target [] with source [{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}] is {"msg":"Changes found","additions":{"0":{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}},"modifications":{}} +2026-05-06T21:34:02.438Z [ERROR] RequestError [HttpError]: Validation Failed: "Actor role must be part of the ruleset source or owner organization" - https://docs.github.com/rest/orgs/rules#create-an-organization-repository-ruleset + at /Users/jefeish/projects/safe-settings/node_modules/@octokit/request/dist-node/index.js:125:21 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + at async sendRequestWithRetries (/Users/jefeish/projects/safe-settings/node_modules/octokit-auth-probot/node_modules/@octokit/auth-app/dist-node/index.js:411:12) + at async Job.doExecute (/Users/jefeish/projects/safe-settings/node_modules/bottleneck/light.js:405:18) { + status: 422, + response: { + url: 'https://api.github.com/orgs/jefeish-test1/rulesets', + status: 422, + headers: { + 'access-control-allow-origin': '*', + 'access-control-expose-headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset', + 'content-length': '231', + 'content-security-policy': "default-src 'none'", + 'content-type': 'application/json; charset=utf-8', + date: 'Wed, 06 May 2026 21:34:02 GMT', + 'referrer-policy': 'origin-when-cross-origin, strict-origin-when-cross-origin', + server: 'github.com', + 'strict-transport-security': 'max-age=31536000; includeSubdomains; preload', + vary: 'Accept-Encoding, Accept, X-Requested-With', + 'x-accepted-github-permissions': 'organization_administration=write', + 'x-content-type-options': 'nosniff', + 'x-frame-options': 'deny', + 'x-github-api-version-selected': '2022-11-28', + 'x-github-media-type': 'github.v3; format=json', + 'x-github-request-id': 'F252:289F06:4575063:40C8A51:69FBB3CA', + 'x-ratelimit-limit': '15000', + 'x-ratelimit-remaining': '14869', + 'x-ratelimit-reset': '1778106601', + 'x-ratelimit-resource': 'core', + 'x-ratelimit-used': '131', + 'x-xss-protection': '0' + }, + data: { + message: 'Validation Failed', + errors: [Array], + documentation_url: 'https://docs.github.com/rest/orgs/rules#create-an-organization-repository-ruleset', + status: '422' + } + }, + request: { + method: 'POST', + url: 'https://api.github.com/orgs/jefeish-test1/rulesets', + headers: { + accept: 'application/vnd.github.v3+json', + 'user-agent': 'probot/13.4.4 octokit-core.js/5.2.0 Node.js/23', + 'x-github-api-version': '2022-11-28', + authorization: 'token [REDACTED]', + 'content-type': 'application/json; charset=utf-8' + }, + body: '{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}', + request: { hook: [Function: bound bound register], retryCount: 1 } + } +} +2026-05-06T21:34:02.438Z [DEBUG] Fetching repositories +2026-05-06T21:34:02.522Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:02.522Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:02.789Z [DEBUG] Process normally... Not a SubOrg config change or SubOrg config was changed and this repo is part of it. {"owner":"jefeish-test1","repo":"safe-settings-config"} suborg config undefined +2026-05-06T21:34:02.789Z [DEBUG] consolidated config is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false,"name":"safe-settings-config","org":"jefeish-test1"},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} +2026-05-06T21:34:02.789Z [DEBUG] safe-settings-config not in restricted repos admin,.github,safe-settings +2026-05-06T21:34:02.789Z [DEBUG] found a matching repoconfig for this repo {"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false,"name":"safe-settings-config","org":"jefeish-test1"} +2026-05-06T21:34:02.789Z [DEBUG] Found section labels in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] Found section branches in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] Found section teams in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] suborg config for safe-settings-config is undefined +2026-05-06T21:34:02.789Z [DEBUG] Found section milestones in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] Found section autolinks in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] Found section collaborators in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] Found section validator in the config. Creating plugin... +2026-05-06T21:34:02.789Z [DEBUG] Syncing Repo safe-settings-config +2026-05-06T21:34:02.789Z [DEBUG] Found section custom_properties in the config. Creating plugin... +2026-05-06T21:34:03.108Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:34:03.108Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-171 +2026-05-06T21:34:03.108Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-171 +2026-05-06T21:34:03.183Z [DEBUG] Adding name for gitignore_template node +2026-05-06T21:34:03.183Z [DEBUG] Updating repo with settings ["new-topic","another-topic"] {"mediaType":{"previews":["nebula-preview"]},"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false,"name":"safe-settings-config","org":"jefeish-test1","owner":"jefeish-test1","repo":"safe-settings-config"} +2026-05-06T21:34:03.183Z [DEBUG] There are repo changes +2026-05-06T21:34:03.183Z [DEBUG] Result of comparing repo for changes = [object Object] +2026-05-06T21:34:03.183Z [DEBUG] Result of comparing topics for changes source ["another-topic","new-topic"] target ["new-topic","another-topic"] = [object Object] +2026-05-06T21:34:03.183Z [DEBUG] Adding name for license_template mit +2026-05-06T21:34:03.347Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:03.347Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:03.637Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:34:03.637Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test1 and repo safe-settings-config +2026-05-06T21:34:03.638Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test1 and repo safe-settings-config +2026-05-06T21:34:04.619Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:04.619Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:05.159Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:34:05.159Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-172 +2026-05-06T21:34:05.159Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-172 +2026-05-06T21:34:05.422Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:05.422Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:05.540Z [DEBUG] Finding labels for {"per_page":100,"owner":"jefeish-test1","repo":"safe-settings-config","headers":{"accept":"application/vnd.github.symmetra-preview+json"}} +2026-05-06T21:34:05.541Z [DEBUG] Finding teams for jefeish-test1/safe-settings-config +2026-05-06T21:34:05.541Z [DEBUG] Getting all custom properties for the repo jefeish-test1/safe-settings-config +2026-05-06T21:34:05.888Z [DEBUG] [ + { + "id": 14747418, + "key_prefix": "JIRA-", + "url_template": "https://jira.github.com/browse/JIRA-", + "is_alphanumeric": false + }, + { + "id": 14747419, + "key_prefix": "MYLINK-", + "url_template": "https://mywebsite.com/", + "is_alphanumeric": true + } +] + + [ + { + "key_prefix": "JIRA-", + "url_template": "https://jira.github.com/browse/JIRA-", + "is_alphanumeric": false + }, + { + "key_prefix": "MYLINK-", + "url_template": "https://mywebsite.com/" + } +] +2026-05-06T21:34:05.889Z [DEBUG] There are no changes for Autolinks for repo safe-settings-config. Skipping changes +2026-05-06T21:34:05.888Z [DEBUG] Results of comparing Autolinks diffable target [{"id":14747418,"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"id":14747419,"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/","is_alphanumeric":true}] with source [{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}] is {"msg":"Changes found","additions":{},"modifications":{},"deletions":{}} +2026-05-06T21:34:05.948Z [DEBUG] [ + { + "url": "https://api.github.com/repos/jefeish-test1/safe-settings-config/milestones/1", + "html_url": "https://github.com/jefeish-test1/safe-settings-config/milestone/1", + "labels_url": "https://api.github.com/repos/jefeish-test1/safe-settings-config/milestones/1/labels", + "id": 15823110, + "node_id": "MI_kwDOPqtde84A8XEG", + "number": 1, + "title": "milestone-title", + "description": "milestone-description", + "creator": { + "login": "fabrikam-safe-settings[bot]", + "id": 223158109, + "node_id": "BOT_kgDODU0fXQ", + "avatar_url": "https://avatars.githubusercontent.com/b/5789?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D", + "html_url": "https://github.com/apps/fabrikam-safe-settings", + "followers_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "open_issues": 0, + "closed_issues": 0, + "state": "open", + "created_at": "2026-05-06T21:30:07Z", + "updated_at": "2026-05-06T21:30:07Z", + "due_on": null, + "closed_at": null + } +] + + [ + { + "title": "milestone-title", + "description": "milestone-description", + "state": "open" + } +] +2026-05-06T21:34:05.948Z [DEBUG] There are no changes for Milestones for repo safe-settings-config. Skipping changes +2026-05-06T21:34:05.948Z [DEBUG] Results of comparing Milestones diffable target [{"url":"https://api.github.com/repos/jefeish-test1/safe-settings-config/milestones/1","html_url":"https://github.com/jefeish-test1/safe-settings-config/milestone/1","labels_url":"https://api.github.com/repos/jefeish-test1/safe-settings-config/milestones/1/labels","id":15823110,"node_id":"MI_kwDOPqtde84A8XEG","number":1,"title":"milestone-title","description":"milestone-description","creator":{"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":0,"state":"open","created_at":"2026-05-06T21:30:07Z","updated_at":"2026-05-06T21:30:07Z","due_on":null,"closed_at":null}] with source [{"title":"milestone-title","description":"milestone-description","state":"open"}] is {"msg":"Changes found","additions":{},"modifications":{},"deletions":{}} +2026-05-06T21:34:05.968Z [DEBUG] [] + + [ + { + "username": "regpaco", + "permission": "push" + }, + { + "username": "beetlejuice", + "permission": "pull" + } +] +2026-05-06T21:34:05.968Z [DEBUG] Results of comparing Collaborators diffable target [] with source [{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull"}] is {"msg":"Changes found","additions":{"0":{"username":"regpaco","permission":"push"},"1":{"username":"beetlejuice","permission":"pull"}},"modifications":{}} +2026-05-06T21:34:05.978Z [DEBUG] Found teams [{"name":"docss","id":17408360,"node_id":"T_kwDODb9_884BCaFo","slug":"docss","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408360","html_url":"https://github.com/orgs/jefeish-test1/teams/docss","members_url":"https://api.github.com/organizations/230653939/team/17408360/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408360/repos","type":"organization","organization_id":230653939,"permission":"push","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true},"parent":null},{"name":"globalteam","id":17408361,"node_id":"T_kwDODb9_884BCaFp","slug":"globalteam","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408361","html_url":"https://github.com/orgs/jefeish-test1/teams/globalteam","members_url":"https://api.github.com/organizations/230653939/team/17408361/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408361/repos","type":"organization","organization_id":230653939,"permission":"push","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true},"parent":null},{"name":"core","id":17408362,"node_id":"T_kwDODb9_884BCaFq","slug":"core","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408362","html_url":"https://github.com/orgs/jefeish-test1/teams/core","members_url":"https://api.github.com/organizations/230653939/team/17408362/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408362/repos","type":"organization","organization_id":230653939,"permission":"admin","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true},"parent":null},{"name":"docs","id":17408363,"node_id":"T_kwDODb9_884BCaFr","slug":"docs","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408363","html_url":"https://github.com/orgs/jefeish-test1/teams/docs","members_url":"https://api.github.com/organizations/230653939/team/17408363/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408363/repos","type":"organization","organization_id":230653939,"permission":"pull","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true},"parent":null}] +2026-05-06T21:34:05.978Z [DEBUG] Calling API to get security managers {"method":"GET","url":"https://api.github.com/orgs/jefeish-test1/security-managers","headers":{"accept":"application/vnd.github.v3+json","user-agent":"probot/13.4.4 octokit-core.js/5.2.0 Node.js/23"},"request":{"retryCount":1}} +2026-05-06T21:34:05.978Z [DEBUG] Removing all security manager teams since they should not be handled here +2026-05-06T21:34:06.021Z [DEBUG] [ + { + "name": "repository-is-cool", + "value": "true" + } +] + + [ + { + "name": "test", + "value": "test" + } +] +2026-05-06T21:34:06.021Z [DEBUG] Create Custom Property "test" for the repo jefeish-test1/safe-settings-config +2026-05-06T21:34:06.021Z [DEBUG] Results of comparing CustomProperties diffable target [{"name":"repository-is-cool","value":"true"}] with source [{"name":"test","value":"test"}] is {"msg":"Changes found","additions":[{"name":"test","value":"test"}],"modifications":{},"deletions":[{"name":"repository-is-cool","value":"true"}]} +2026-05-06T21:34:06.021Z [DEBUG] Delete Custom Property "repository-is-cool" for the repo jefeish-test1/safe-settings-config +2026-05-06T21:34:06.020Z [DEBUG] Found 1 custom properties +2026-05-06T21:34:06.042Z [DEBUG] Repo safe-settings-config Passed Validation for pattern [a-zA-Z0-9_-]+ +2026-05-06T21:34:06.214Z [DEBUG] Response from the call is [] +2026-05-06T21:34:06.214Z [DEBUG] Results of comparing Teams diffable target [{"name":"docss","id":17408360,"node_id":"T_kwDODb9_884BCaFo","slug":"docss","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408360","html_url":"https://github.com/orgs/jefeish-test1/teams/docss","members_url":"https://api.github.com/organizations/230653939/team/17408360/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408360/repos","type":"organization","organization_id":230653939,"permission":"push","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true},"parent":null},{"name":"globalteam","id":17408361,"node_id":"T_kwDODb9_884BCaFp","slug":"globalteam","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408361","html_url":"https://github.com/orgs/jefeish-test1/teams/globalteam","members_url":"https://api.github.com/organizations/230653939/team/17408361/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408361/repos","type":"organization","organization_id":230653939,"permission":"push","permissions":{"admin":false,"maintain":false,"push":true,"triage":true,"pull":true},"parent":null},{"name":"core","id":17408362,"node_id":"T_kwDODb9_884BCaFq","slug":"core","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408362","html_url":"https://github.com/orgs/jefeish-test1/teams/core","members_url":"https://api.github.com/organizations/230653939/team/17408362/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408362/repos","type":"organization","organization_id":230653939,"permission":"admin","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true},"parent":null},{"name":"docs","id":17408363,"node_id":"T_kwDODb9_884BCaFr","slug":"docs","description":null,"privacy":"secret","notification_setting":"notifications_enabled","url":"https://api.github.com/organizations/230653939/team/17408363","html_url":"https://github.com/orgs/jefeish-test1/teams/docs","members_url":"https://api.github.com/organizations/230653939/team/17408363/members{/member}","repositories_url":"https://api.github.com/organizations/230653939/team/17408363/repos","type":"organization","organization_id":230653939,"permission":"pull","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true},"parent":null}] with source [{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}] is {"msg":"Changes found","additions":{},"modifications":[{"visibility":{"0":"c","1":"l","2":"o","3":"s","4":"e","5":"d"},"name":"globalteam"}],"deletions":{}} +2026-05-06T21:34:06.214Z [DEBUG] Adding name for visibility closed +2026-05-06T21:34:06.214Z [DEBUG] [ + { + "name": "docss", + "id": 17408360, + "node_id": "T_kwDODb9_884BCaFo", + "slug": "docss", + "description": null, + "privacy": "secret", + "notification_setting": "notifications_enabled", + "url": "https://api.github.com/organizations/230653939/team/17408360", + "html_url": "https://github.com/orgs/jefeish-test1/teams/docss", + "members_url": "https://api.github.com/organizations/230653939/team/17408360/members{/member}", + "repositories_url": "https://api.github.com/organizations/230653939/team/17408360/repos", + "type": "organization", + "organization_id": 230653939, + "permission": "push", + "permissions": { + "admin": false, + "maintain": false, + "push": true, + "triage": true, + "pull": true + }, + "parent": null + }, + { + "name": "globalteam", + "id": 17408361, + "node_id": "T_kwDODb9_884BCaFp", + "slug": "globalteam", + "description": null, + "privacy": "secret", + "notification_setting": "notifications_enabled", + "url": "https://api.github.com/organizations/230653939/team/17408361", + "html_url": "https://github.com/orgs/jefeish-test1/teams/globalteam", + "members_url": "https://api.github.com/organizations/230653939/team/17408361/members{/member}", + "repositories_url": "https://api.github.com/organizations/230653939/team/17408361/repos", + "type": "organization", + "organization_id": 230653939, + "permission": "push", + "permissions": { + "admin": false, + "maintain": false, + "push": true, + "triage": true, + "pull": true + }, + "parent": null + }, + { + "name": "core", + "id": 17408362, + "node_id": "T_kwDODb9_884BCaFq", + "slug": "core", + "description": null, + "privacy": "secret", + "notification_setting": "notifications_enabled", + "url": "https://api.github.com/organizations/230653939/team/17408362", + "html_url": "https://github.com/orgs/jefeish-test1/teams/core", + "members_url": "https://api.github.com/organizations/230653939/team/17408362/members{/member}", + "repositories_url": "https://api.github.com/organizations/230653939/team/17408362/repos", + "type": "organization", + "organization_id": 230653939, + "permission": "admin", + "permissions": { + "admin": true, + "maintain": true, + "push": true, + "triage": true, + "pull": true + }, + "parent": null + }, + { + "name": "docs", + "id": 17408363, + "node_id": "T_kwDODb9_884BCaFr", + "slug": "docs", + "description": null, + "privacy": "secret", + "notification_setting": "notifications_enabled", + "url": "https://api.github.com/organizations/230653939/team/17408363", + "html_url": "https://github.com/orgs/jefeish-test1/teams/docs", + "members_url": "https://api.github.com/organizations/230653939/team/17408363/members{/member}", + "repositories_url": "https://api.github.com/organizations/230653939/team/17408363/repos", + "type": "organization", + "organization_id": 230653939, + "permission": "pull", + "permissions": { + "admin": false, + "maintain": false, + "push": false, + "triage": false, + "pull": true + }, + "parent": null + } +] + + [ + { + "name": "core", + "permission": "admin" + }, + { + "name": "docss", + "permission": "push" + }, + { + "name": "docs", + "permission": "pull" + }, + { + "name": "globalteam", + "permission": "push", + "visibility": "closed" + } +] +2026-05-06T21:34:06.389Z [DEBUG] There are no changes for branch {"owner":"jefeish-test1","repo":"safe-settings-config","branch":"main"}. Skipping branch protection changes +2026-05-06T21:34:06.388Z [DEBUG] Result of compareDeep = [object Object] +2026-05-06T21:34:06.421Z [DEBUG] [ + { + "id": 9233743419, + "node_id": "LA_kwDOPqtde88AAAACJl--Ow", + "url": "https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/bug", + "name": "bug", + "color": "CC0000", + "default": true, + "description": "An issue with the system" + }, + { + "id": 10882892904, + "node_id": "LA_kwDOPqtde88AAAACiKvEaA", + "url": "https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/feature", + "name": "feature", + "color": "336699", + "default": false, + "description": "New functionality." + }, + { + "id": 10882893036, + "node_id": "LA_kwDOPqtde88AAAACiKvE7A", + "url": "https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/first-timers-only", + "name": "first-timers-only", + "color": "326699", + "default": false, + "description": null + }, + { + "id": 10882893129, + "node_id": "LA_kwDOPqtde88AAAACiKvFSQ", + "url": "https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/new-label", + "name": "new-label", + "color": "326699", + "default": false, + "description": null + } +] + + [ + { + "name": "bug", + "color": "CC0000", + "description": "An issue with the system" + }, + { + "name": "feature", + "color": "336699", + "description": "New functionality." + }, + { + "name": "first-timers-only", + "oldname": "Help Wanted", + "color": "326699" + }, + { + "name": "new-label", + "oldname": "Help Wanted", + "color": "326699" + } +] +2026-05-06T21:34:06.421Z [DEBUG] Adding name for oldname Help Wanted +2026-05-06T21:34:06.421Z [DEBUG] Results of comparing Labels diffable target [{"id":9233743419,"node_id":"LA_kwDOPqtde88AAAACJl--Ow","url":"https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/bug","name":"bug","color":"CC0000","default":true,"description":"An issue with the system"},{"id":10882892904,"node_id":"LA_kwDOPqtde88AAAACiKvEaA","url":"https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/feature","name":"feature","color":"336699","default":false,"description":"New functionality."},{"id":10882893036,"node_id":"LA_kwDOPqtde88AAAACiKvE7A","url":"https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/first-timers-only","name":"first-timers-only","color":"326699","default":false,"description":null},{"id":10882893129,"node_id":"LA_kwDOPqtde88AAAACiKvFSQ","url":"https://api.github.com/repos/jefeish-test1/safe-settings-config/labels/new-label","name":"new-label","color":"326699","default":false,"description":null}] with source [{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"326699"},{"name":"new-label","oldname":"Help Wanted","color":"326699"}] is {"msg":"Changes found","additions":{},"modifications":[{"oldname":{"0":"H","1":"e","2":"l","3":"p","4":" ","5":"W","6":"a","7":"n","8":"t","9":"e","10":"d"},"name":"first-timers-only"},{"oldname":{"0":"H","1":"e","2":"l","3":"p","4":" ","5":"W","6":"a","7":"n","8":"t","9":"e","10":"d"},"name":"new-label"}],"deletions":{}} +2026-05-06T21:34:06.421Z [DEBUG] Adding name for oldname Help Wanted +2026-05-06T21:34:06.422Z [DEBUG] Updating labels for { + "name": "first-timers-only", + "oldname": "Help Wanted", + "color": "326699" +} { + "owner": "jefeish-test1", + "repo": "safe-settings-config", + "branch": "main" +} +2026-05-06T21:34:06.422Z [DEBUG] Updating labels for { + "name": "new-label", + "oldname": "Help Wanted", + "color": "326699" +} { + "owner": "jefeish-test1", + "repo": "safe-settings-config", + "branch": "main" } -2025-09-18T14:14:07.333Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-18T14:14:07.334Z [INFO] DEBUG: FILE[0] raw={"sha":"8f345e9e4d6701accc0d39f587d00950c9a17ed5","filename":".github/safe-settings/organizations/jefeish-training/settings.yml","status":"modified","additions":175,"deletions":175,"changes":350,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/ee5e72b1fcb62dda5d16fd244fe36bb072589455/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-training%2Fsettings.yml?ref=ee5e72b1fcb62dda5d16fd244fe36bb072589455","patch":"@@ -222,178 +222,178 @@ validator:\n \n # Rulesets\n # See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options\n-rulesets:\n- - name: Template\n- # The target of the ruleset. Can be one of:\n- # - branch\n- # - tag\n- target: branch\n- # The enforcement level of the ruleset. `evaluate` allows admins to test\n- # rules before enforcing them.\n- # - disabled\n- # - active\n- # - evaluate\n- enforcement: active\n-\n- # The actors that can bypass the rules in this ruleset\n- bypass_actors:\n- - actor_id: number\n- # type: The type of actor that can bypass a ruleset\n- # - RepositoryRole\n- # - Team\n- # - Integration\n- # - OrganizationAdmin\n- actor_type: Team\n- # When the specified actor can bypass the ruleset. `pull_request`\n- # means that an actor can only bypass rules on pull requests.\n- # - always\n- # - pull_request\n- bypass_mode: pull_request\n-\n- - actor_id: 1\n- actor_type: OrganizationAdmin\n- bypass_mode: always\n-\n- - actor_id: 7898\n- actor_type: RepositoryRole\n- bypass_mode: always\n-\n- - actor_id: 210920\n- actor_type: Integration\n- bypass_mode: always\n-\n- conditions:\n- # Parameters for a repository ruleset ref name condition\n- ref_name:\n- # Array of ref names or patterns to include. One of these\n- # patterns must match for the condition to pass. Also accepts\n- # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n- # include all branches.\n- include: [\"~DEFAULT_BRANCH\"]\n-\n- # Array of ref names or patterns to exclude. The condition\n- # will not pass if any of these patterns match.\n- exclude: [\"refs/heads/oldmaster\"]\n-\n- # This condition only exists at the org level (remove for suborg and repo level rulesets)\n- repository_name:\n- # Array of repository names or patterns to include.\n- # One of these patterns must match for the condition\n- # to pass. Also accepts `~ALL` to include all\n- # repositories.\n- include: [\"test*\"]\n- # Array of repository names or patterns to exclude. The\n- # condition will not pass if any of these patterns\n- # match.\n- exclude: [\"test\", \"test1\"]\n- # Whether renaming of target repositories is\n- # prevented.\n- protected: true\n-\n- # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n- rules:\n- - type: creation\n- - type: update\n- parameters:\n- # Branch can pull changes from its upstream repository\n- update_allows_fetch_and_merge: true\n- - type: deletion\n- - type: required_linear_history\n- - type: required_signatures\n-\n- - type: required_deployments\n- parameters:\n- required_deployment_environments: [\"staging\"]\n-\n- - type: pull_request\n- parameters:\n- # Reviewable commits pushed will dismiss previous pull\n- # request review approvals.\n- dismiss_stale_reviews_on_push: true\n- # Require an approving review in pull requests that modify\n- # files that have a designated code owner\n- require_code_owner_review: true\n- # Whether the most recent reviewable push must be approved\n- # by someone other than the person who pushed it.\n- require_last_push_approval: true\n- # The number of approving reviews that are required before a\n- # pull request can be merged.\n- required_approving_review_count: 1\n- # All conversations on code must be resolved before a pull\n- # request can be merged.\n- required_review_thread_resolution: true\n-\n- # Choose which status checks must pass before branches can be merged\n- # into a branch that matches this rule. When enabled, commits must\n- # first be pushed to another branch, then merged or pushed directly\n- # to a branch that matches this rule after status checks have\n- # passed.\n- - type: required_status_checks\n- parameters:\n- # Whether pull requests targeting a matching branch must be\n- # tested with the latest code. This setting will not take\n- # effect unless at least one status check is enabled.\n- strict_required_status_checks_policy: true\n- required_status_checks:\n- - context: CodeQL\n- integration_id: 1234\n- - context: GHAS Compliance\n- integration_id: 1234\n-\n- # Choose which workflows must pass before branches can be merged.\n- - type: workflows\n- parameters:\n- workflows:\n- - path: .github/workflows/example.yml\n- # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n- # in the browser console of the repository to get the repository_id\n- repository_id: 123456\n- # One of the following:\n- # Branch or tag\n- ref: refs/heads/main\n- # Commit SHA\n- sha: 1234567890abcdef\n-\n- - type: commit_message_pattern\n- parameters:\n- name: test commit_message_pattern\n- # required:\n- # - operator\n- # - pattern\n- negate: true\n- operator: starts_with\n- # The operator to use for matching.\n- # - starts_with\n- # - ends_with\n- # - contains\n- # - regex\n- pattern: skip*\n- # The pattern to match with.\n-\n- - type: commit_author_email_pattern\n- parameters:\n- name: test commit_author_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: committer_email_pattern\n- parameters:\n- name: test committer_email_pattern\n- negate: false\n- operator: regex\n- pattern: \"^.*@example.com$\"\n-\n- - type: branch_name_pattern\n- parameters:\n- name: test branch_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n-\n- - type: \"tag_name_pattern\"\n- parameters:\n- name: test tag_name_pattern\n- negate: false\n- operator: regex\n- pattern: \".*\\/.*\"\n+# rulesets:\n+# - name: Template\n+# # The target of the ruleset. Can be one of:\n+# # - branch\n+# # - tag\n+# target: branch\n+# # The enforcement level of the ruleset. `evaluate` allows admins to test\n+# # rules before enforcing them.\n+# # - disabled\n+# # - active\n+# # - evaluate\n+# enforcement: active\n+\n+# # The actors that can bypass the rules in this ruleset\n+# bypass_actors:\n+# - actor_id: number\n+# # type: The type of actor that can bypass a ruleset\n+# # - RepositoryRole\n+# # - Team\n+# # - Integration\n+# # - OrganizationAdmin\n+# actor_type: Team\n+# # When the specified actor can bypass the ruleset. `pull_request`\n+# # means that an actor can only bypass rules on pull requests.\n+# # - always\n+# # - pull_request\n+# bypass_mode: pull_request\n+\n+# - actor_id: 1\n+# actor_type: OrganizationAdmin\n+# bypass_mode: always\n+\n+# - actor_id: 7898\n+# actor_type: RepositoryRole\n+# bypass_mode: always\n+\n+# - actor_id: 210920\n+# actor_type: Integration\n+# bypass_mode: always\n+\n+# conditions:\n+# # Parameters for a repository ruleset ref name condition\n+# ref_name:\n+# # Array of ref names or patterns to include. One of these\n+# # patterns must match for the condition to pass. Also accepts\n+# # `~DEFAULT_BRANCH` to include the default branch or `~ALL` to\n+# # include all branches.\n+# include: [\"~DEFAULT_BRANCH\"]\n+\n+# # Array of ref names or patterns to exclude. The condition\n+# # will not pass if any of these patterns match.\n+# exclude: [\"refs/heads/oldmaster\"]\n+\n+# # This condition only exists at the org level (remove for suborg and repo level rulesets)\n+# repository_name:\n+# # Array of repository names or patterns to include.\n+# # One of these patterns must match for the condition\n+# # to pass. Also accepts `~ALL` to include all\n+# # repositories.\n+# include: [\"test*\"]\n+# # Array of repository names or patterns to exclude. The\n+# # condition will not pass if any of these patterns\n+# # match.\n+# exclude: [\"test\", \"test1\"]\n+# # Whether renaming of target repositories is\n+# # prevented.\n+# protected: true\n+\n+# # Refer to https://docs.github.com/en/rest/orgs/rules#create-an-organization-repository-ruleset\n+# rules:\n+# - type: creation\n+# - type: update\n+# parameters:\n+# # Branch can pull changes from its upstream repository\n+# update_allows_fetch_and_merge: true\n+# - type: deletion\n+# - type: required_linear_history\n+# - type: required_signatures\n+\n+# - type: required_deployments\n+# parameters:\n+# required_deployment_environments: [\"staging\"]\n+\n+# - type: pull_request\n+# parameters:\n+# # Reviewable commits pushed will dismiss previous pull\n+# # request review approvals.\n+# dismiss_stale_reviews_on_push: true\n+# # Require an approving review in pull requests that modify\n+# # files that have a designated code owner\n+# require_code_owner_review: true\n+# # Whether the most recent reviewable push must be approved\n+# # by someone other than the person who pushed it.\n+# require_last_push_approval: true\n+# # The number of approving reviews that are required before a\n+# # pull request can be merged.\n+# required_approving_review_count: 1\n+# # All conversations on code must be resolved before a pull\n+# # request can be merged.\n+# required_review_thread_resolution: true\n+\n+# # Choose which status checks must pass before branches can be merged\n+# # into a branch that matches this rule. When enabled, commits must\n+# # first be pushed to another branch, then merged or pushed directly\n+# # to a branch that matches this rule after status checks have\n+# # passed.\n+# - type: required_status_checks\n+# parameters:\n+# # Whether pull requests targeting a matching branch must be\n+# # tested with the latest code. This setting will not take\n+# # effect unless at least one status check is enabled.\n+# strict_required_status_checks_policy: true\n+# required_status_checks:\n+# - context: CodeQL\n+# integration_id: 1234\n+# - context: GHAS Compliance\n+# integration_id: 1234\n+\n+# # Choose which workflows must pass before branches can be merged.\n+# - type: workflows\n+# parameters:\n+# workflows:\n+# - path: .github/workflows/example.yml\n+# # Run $(\"meta[name=octolytics-dimension-repository_id]\").getAttribute('content')\n+# # in the browser console of the repository to get the repository_id\n+# repository_id: 123456\n+# # One of the following:\n+# # Branch or tag\n+# ref: refs/heads/main\n+# # Commit SHA\n+# sha: 1234567890abcdef\n+\n+# - type: commit_message_pattern\n+# parameters:\n+# name: test commit_message_pattern\n+# # required:\n+# # - operator\n+# # - pattern\n+# negate: true\n+# operator: starts_with\n+# # The operator to use for matching.\n+# # - starts_with\n+# # - ends_with\n+# # - contains\n+# # - regex\n+# pattern: skip*\n+# # The pattern to match with.\n+\n+# - type: commit_author_email_pattern\n+# parameters:\n+# name: test commit_author_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: committer_email_pattern\n+# parameters:\n+# name: test committer_email_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \"^.*@example.com$\"\n+\n+# - type: branch_name_pattern\n+# parameters:\n+# name: test branch_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\"\n+\n+# - type: \"tag_name_pattern\"\n+# parameters:\n+# name: test tag_name_pattern\n+# negate: false\n+# operator: regex\n+# pattern: \".*\\/.*\""} -2025-09-18T14:14:07.334Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-training/settings.yml" length=65 -2025-09-18T14:14:07.334Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-18T14:14:07.334Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-training -2025-09-18T14:14:07.335Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-training -2025-09-18T14:14:07.536Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #49 to jefeish-training/safe-settings-config@main under .github (directPush=true) -2025-09-18T14:14:09.113Z [INFO] Committed .github/settings.yml to jefeish-training/safe-settings-config@main -2025-09-18T14:14:09.113Z [INFO] Changes pushed directly to jefeish-training/safe-settings-config@main -2025-09-18T14:14:10.074Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... -2025-09-18T14:14:10.074Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2025-09-18T14:14:10.392Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"}} -2025-09-18T14:14:12.340Z [DEBUG] Is Admin repo event true -2025-09-18T14:14:12.340Z [DEBUG] Working on the default branch, returning... -2025-09-18T14:14:49.484Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-18T14:14:51.219Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-18T14:14:51.219Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-18T14:14:51.219Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-18T14:14:51.485Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:14:51.485Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-18T14:14:51.485Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:34:06.442Z [ERROR] Error HttpError: Validation Failed: "User is not a member of the enterprise." - https://docs.github.com/rest/reference/repos#add-a-repository-collaborator in Collaborators for repo: {"owner":"jefeish-test1","repo":"safe-settings-config","branch":"main"} entries [{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}] +2026-05-06T21:34:06.523Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:06.523Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:07.064Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} +2026-05-06T21:34:07.064Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-173 +2026-05-06T21:34:07.064Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-173 +2026-05-06T21:34:07.375Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-05-06T21:34:07.375Z [DEBUG] Repository Edited by a Bot +2026-05-06T21:34:24.193Z [ERROR] retrieveSettingsFromOrgs error: Not Found - https://docs.github.com/rest/git/refs#get-a-reference +2026-05-06T21:34:24.196Z [ERROR] RequestError [HttpError]: Not Found - https://docs.github.com/rest/git/refs#get-a-reference + at /Users/jefeish/projects/safe-settings/node_modules/@octokit/request/dist-node/index.js:125:21 + at process.processTicksAndRejections (node:internal/process/task_queues:105:5) + at async sendRequestWithRetries (/Users/jefeish/projects/safe-settings/node_modules/octokit-auth-probot/node_modules/@octokit/auth-app/dist-node/index.js:411:12) + at async Job.doExecute (/Users/jefeish/projects/safe-settings/node_modules/bottleneck/light.js:405:18) { + status: 404, + response: { + url: 'https://api.github.com/repos/jefeish-training/safe-settings-config-master/git/ref/heads%2Fmain', + status: 404, + headers: { + 'access-control-allow-origin': '*', + 'access-control-expose-headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset', + 'content-encoding': 'gzip', + 'content-security-policy': "default-src 'none'", + 'content-type': 'application/json; charset=utf-8', + date: 'Wed, 06 May 2026 21:34:24 GMT', + 'referrer-policy': 'origin-when-cross-origin, strict-origin-when-cross-origin', + server: 'github.com', + 'strict-transport-security': 'max-age=31536000; includeSubdomains; preload', + 'transfer-encoding': 'chunked', + vary: 'Accept-Encoding, Accept, X-Requested-With', + 'x-accepted-github-permissions': 'contents=read', + 'x-content-type-options': 'nosniff', + 'x-frame-options': 'deny', + 'x-github-api-version-selected': '2022-11-28', + 'x-github-media-type': 'github.v3; format=json', + 'x-github-request-id': 'FBA4:202FF2:47C6D22:432FA07:69FBB3E0', + 'x-ratelimit-limit': '15000', + 'x-ratelimit-remaining': '14995', + 'x-ratelimit-reset': '1778106545', + 'x-ratelimit-resource': 'core', + 'x-ratelimit-used': '5', + 'x-xss-protection': '0' + }, + data: { + message: 'Not Found', + documentation_url: 'https://docs.github.com/rest/git/refs#get-a-reference', + status: '404' + } + }, + request: { + method: 'GET', + url: 'https://api.github.com/repos/jefeish-training/safe-settings-config-master/git/ref/heads%2Fmain', + headers: { + accept: 'application/vnd.github.v3+json', + 'user-agent': 'probot/13.4.4 octokit-core.js/5.2.0 Node.js/23', + authorization: 'token [REDACTED]' + }, + request: { hook: [Function: bound bound register] } + } +} +2026-05-06T21:40:04.044Z [ERROR] retrieveSettingsFromOrgs error: Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found. Please ensure the repository exists and has a 'main' branch. +2026-05-06T21:40:04.046Z [ERROR] Error: Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found. Please ensure the repository exists and has a 'main' branch. + at retrieveSettingsFromOrgs (/Users/jefeish/projects/safe-settings/lib/hubSyncHandler.js:521:15) + at async /Users/jefeish/projects/safe-settings/lib/routes.js:600:23 +2026-05-06T21:42:19.178Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-test,jefeish-test1,jefeish-training organizations... +2026-05-06T21:42:21.157Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test: main +2026-05-06T21:42:21.157Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test: .github/safe-settings/organizations/jefeish-test +2026-05-06T21:42:21.157Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test +2026-05-06T21:42:21.372Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:42:21.372Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml +2026-05-06T21:42:21.372Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, + path: .github/safe-settings/organizations/jefeish-test, ref: main -2025-09-18T14:14:51.745Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-18T14:14:51.745Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-18T14:14:52.466Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-18T14:14:52.465Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-18T14:14:52.466Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-18T14:14:52.707Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-18T14:14:52.707Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:42:21.595Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:42:21.596Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test +2026-05-06T21:42:22.328Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2026-05-06T21:42:22.328Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2026-05-06T21:42:22.328Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2026-05-06T21:42:22.543Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, repo.yml, settings.yml, suborg.yml +2026-05-06T21:42:22.543Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:42:22.543Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, path: .github/safe-settings/organizations/jefeish-test1, ref: main -2025-09-18T14:14:52.707Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:14:52.991Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-18T14:14:52.992Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-18T14:14:53.724Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-18T14:14:53.724Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-18T14:14:53.724Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-18T14:14:53.987Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:14:53.987Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:42:22.748Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:42:22.748Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2026-05-06T21:43:07.996Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found +2026-05-06T21:43:10.801Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found +2026-05-06T21:43:49.984Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-test,jefeish-test1,jefeish-training organizations... +2026-05-06T21:43:51.920Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test +2026-05-06T21:43:51.920Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test: main +2026-05-06T21:43:51.920Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test: .github/safe-settings/organizations/jefeish-test +2026-05-06T21:43:52.137Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml +2026-05-06T21:43:52.137Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, + path: .github/safe-settings/organizations/jefeish-test, ref: main -2025-09-18T14:14:53.987Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-18T14:14:54.285Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-18T14:14:54.285Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-18T14:14:54.869Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-18T14:14:54.869Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 -2025-09-18T14:14:54.869Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-18T14:14:54.869Z [DEBUG] 9. [SYNC DEBUG] Org hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 -2025-09-18T14:14:55.440Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-18T14:14:55.440Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-18T14:14:55.440Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-18T14:14:56.225Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-18T14:14:56.225Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-18T14:14:56.225Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-18T14:14:56.469Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-18T14:14:56.469Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:14:56.469Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:43:52.137Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:43:52.368Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test +2026-05-06T21:43:52.368Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:43:52.756Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-test,jefeish-test1,jefeish-training organizations... +2026-05-06T21:43:53.091Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2026-05-06T21:43:53.091Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2026-05-06T21:43:53.091Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2026-05-06T21:43:53.324Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:43:53.324Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, + path: .github/safe-settings/organizations/jefeish-test1, ref: main -2025-09-18T14:14:56.735Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-18T14:14:56.735Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-18T14:15:07.249Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-migration-test,jefeish-test1,jefeish-training,jetest99 organizations... -2025-09-18T14:15:08.957Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-migration-test: .github/safe-settings/organizations/jefeish-migration-test -2025-09-18T14:15:08.957Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-migration-test -2025-09-18T14:15:08.957Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-migration-test: main -2025-09-18T14:15:09.220Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:43:53.324Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, repo.yml, settings.yml, suborg.yml +2026-05-06T21:43:53.529Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:43:53.530Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2026-05-06T21:43:54.608Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test: main +2026-05-06T21:43:54.608Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test: .github/safe-settings/organizations/jefeish-test +2026-05-06T21:43:54.608Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test +2026-05-06T21:43:54.822Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:43:54.821Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml +2026-05-06T21:43:54.822Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-migration-test, + path: .github/safe-settings/organizations/jefeish-test, ref: main -2025-09-18T14:15:09.220Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-18T14:15:09.220Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:15:09.484Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-migration-test -2025-09-18T14:15:09.484Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-18T14:15:10.150Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 -2025-09-18T14:15:10.150Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 -2025-09-18T14:15:10.150Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main -2025-09-18T14:15:10.394Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, settings.yml, suborg.yml -2025-09-18T14:15:10.394Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:43:55.019Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:43:55.019Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test +2026-05-06T21:43:55.675Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2026-05-06T21:43:55.675Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2026-05-06T21:43:55.675Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2026-05-06T21:43:55.887Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, path: .github/safe-settings/organizations/jefeish-test1, ref: main -2025-09-18T14:15:10.394Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:15:10.648Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-18T14:15:10.648Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 -2025-09-18T14:15:11.505Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-training: .github/safe-settings/organizations/jefeish-training -2025-09-18T14:15:11.505Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-training: main -2025-09-18T14:15:11.505Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-training -2025-09-18T14:15:11.761Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:15:11.761Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-18T14:15:11.761Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:43:55.887Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:43:55.887Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, repo.yml, settings.yml, suborg.yml +2026-05-06T21:43:56.086Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:43:56.086Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2026-05-06T21:44:33.943Z [INFO] Starting commit and sync status fetch for copilot-for-emus,decyjphr-emu,decyjphr-training,jefeish-test,jefeish-test1,jefeish-training organizations... +2026-05-06T21:44:35.772Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test +2026-05-06T21:44:35.772Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test: main +2026-05-06T21:44:35.772Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test: .github/safe-settings/organizations/jefeish-test +2026-05-06T21:44:35.979Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml +2026-05-06T21:44:35.979Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:44:35.979Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jefeish-training, + path: .github/safe-settings/organizations/jefeish-test, ref: main -2025-09-18T14:15:12.100Z [DEBUG] 6. [SYNC DEBUG] Hub hubFilesResp file names: settings.yml, suborg.yml -2025-09-18T14:15:12.100Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/settings.yml, hubFile: .github/safe-settings/organizations/jefeish-training/settings.yml -2025-09-18T14:15:13.786Z [DEBUG] 9. [SYNC DEBUG] Org hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 -2025-09-18T14:15:13.786Z [DEBUG] 7. [SYNC DEBUG] Fetching file contents for org: jefeish-training, orgFile: .github/suborg.yml, hubFile: .github/safe-settings/organizations/jefeish-training/suborg.yml -2025-09-18T14:15:13.786Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 4ebf30e70a0517fc9a5f36e9a4e087c866d9a2a791755d18bcc7bedd7e104278 -2025-09-18T14:15:13.786Z [DEBUG] 8. [SYNC DEBUG] Comparing file: settings.yml -2025-09-18T14:15:14.195Z [DEBUG] Not working on the Admin repo, returning... -2025-09-18T14:15:14.195Z [DEBUG] Is Admin repo event false -2025-09-18T14:15:14.354Z [DEBUG] 10. [SYNC DEBUG] Hub hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-18T14:15:14.354Z [DEBUG] 8. [SYNC DEBUG] Comparing file: suborg.yml -2025-09-18T14:15:14.354Z [DEBUG] 9. [SYNC DEBUG] Org hash: 6c1fecd3dabe4bc0677d0f21427ebc03c8af34531000a13b425c1387902b86a6 -2025-09-18T14:15:15.049Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jetest99: .github/safe-settings/organizations/jetest99 -2025-09-18T14:15:15.049Z [DEBUG] 3. [SYNC DEBUG] Org: jetest99 -2025-09-18T14:15:15.049Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jetest99: main -2025-09-18T14:15:15.316Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: settings.yml, suborg.yml -2025-09-18T14:15:15.316Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training -2025-09-18T14:15:15.316Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: +2026-05-06T21:44:36.178Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:44:36.178Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test +2026-05-06T21:44:36.845Z [DEBUG] 1. [SYNC DEBUG] Hub file path for org jefeish-test1: .github/safe-settings/organizations/jefeish-test1 +2026-05-06T21:44:36.845Z [DEBUG] 2. [SYNC DEBUG] Hub file branch/ref for org jefeish-test1: main +2026-05-06T21:44:36.845Z [DEBUG] 3. [SYNC DEBUG] Org: jefeish-test1 +2026-05-06T21:44:37.068Z [DEBUG] 5a. [SYNC DEBUG] Fetching hub files for: owner: jefeish-training, repo: safe-settings-config-master, - path: .github/safe-settings/organizations/jetest99, + path: .github/safe-settings/organizations/jefeish-test1, ref: main -2025-09-18T14:15:15.620Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2025-09-18T14:15:15.620Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jetest99 -2025-09-18T14:21:33.248Z [INFO] Received 'pull_request.closed' event: 50 -2025-09-18T14:21:33.248Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) -2025-09-18T14:21:34.144Z [DEBUG] Not working on the Admin repo, returning... -2025-09-18T14:21:34.144Z [DEBUG] Is Admin repo event false -2025-09-18T14:21:34.212Z [DEBUG] Detected changes in the globals folder. Routing to syncHubGlobalsUpdate(...). -2025-09-18T14:21:34.212Z [INFO] Files changed in PR #50: .github/safe-settings/globals/repo.yml -2025-09-18T14:21:34.212Z [INFO] Syncing safe settings for 'globals/'. -2025-09-18T14:21:34.524Z [DEBUG] Loaded manifest.yml rules from hub repo:{ - "rules": [ - { - "name": "global-defaults", - "targets": [ - "*" - ], - "files": [ - "*.yml" - ], - "mergeStrategy": "merge" - }, - { - "name": "security-policies", - "targets": [ - "acme-*", - "foo-bar" - ], - "files": [ - "settings.yml" - ], - "mergeStrategy": "overwrite" - } - ] -} -2025-09-18T14:21:34.737Z [DEBUG] Evaluating globals file: .github/safe-settings/globals/repo.yml -2025-09-18T14:21:34.737Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jetest99' with mergeStrategy='merge' -2025-09-18T14:21:34.737Z [DEBUG] Rule 'global-defaults' matches file 'repo.yml'. Targets: jetest99, jefeish-training, jefeish-test1, copilot-for-emus, jefeish-migration-test, decyjphr-training, decyjphr-emu -2025-09-18T14:21:35.425Z [DEBUG] Checking existence of .github/repo.yml in jetest99/safe-settings-config -2025-09-18T14:21:35.732Z [INFO] File .github/repo.yml not found in jetest99/safe-settings-config (this is fine for both merge strategies) -2025-09-18T14:21:35.733Z [INFO] Syncing repo.yml to jetest99 (mergeStrategy=merge) -2025-09-18T14:21:36.704Z [INFO] Committed .github/repo.yml to jetest99/safe-settings-config@main -2025-09-18T14:21:36.704Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-training' with mergeStrategy='merge' -2025-09-18T14:21:36.704Z [INFO] Changes pushed directly to jetest99/safe-settings-config@main -2025-09-18T14:21:37.083Z [DEBUG] Checking existence of .github/repo.yml in jefeish-training/safe-settings-config -2025-09-18T14:21:37.357Z [INFO] File .github/repo.yml not found in jefeish-training/safe-settings-config (this is fine for both merge strategies) -2025-09-18T14:21:37.357Z [INFO] Syncing repo.yml to jefeish-training (mergeStrategy=merge) -2025-09-18T14:21:37.740Z [DEBUG] No changes in '.github/settings.yml' detected, returning... -2025-09-18T14:21:38.344Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-test1' with mergeStrategy='merge' -2025-09-18T14:21:38.344Z [INFO] Changes pushed directly to jefeish-training/safe-settings-config@main -2025-09-18T14:21:38.343Z [INFO] Committed .github/repo.yml to jefeish-training/safe-settings-config@main -2025-09-18T14:21:38.554Z [DEBUG] Working on the default branch, returning... -2025-09-18T14:21:38.554Z [DEBUG] Is Admin repo event true -2025-09-18T14:21:39.149Z [DEBUG] Checking existence of .github/repo.yml in jefeish-test1/safe-settings-config -2025-09-18T14:21:39.418Z [INFO] File .github/repo.yml not found in jefeish-test1/safe-settings-config (this is fine for both merge strategies) -2025-09-18T14:21:39.418Z [INFO] Syncing repo.yml to jefeish-test1 (mergeStrategy=merge) -2025-09-18T14:21:39.482Z [DEBUG] No changes in '.github/settings.yml' detected, returning... -2025-09-18T14:21:40.470Z [INFO] Committed .github/repo.yml to jefeish-test1/safe-settings-config@main -2025-09-18T14:21:40.471Z [INFO] Changes pushed directly to jefeish-test1/safe-settings-config@main -2025-09-18T14:21:40.471Z [DEBUG] Preparing to sync file 'repo.yml' to org 'copilot-for-emus' with mergeStrategy='merge' -2025-09-18T14:21:40.690Z [DEBUG] Is Admin repo event true -2025-09-18T14:21:40.690Z [DEBUG] Working on the default branch, returning... -2025-09-18T14:21:40.962Z [DEBUG] Preparing to sync file 'repo.yml' to org 'jefeish-migration-test' with mergeStrategy='merge' -2025-09-18T14:21:40.961Z [INFO] Skipping org copilot-for-emus: config repo 'safe-settings-config' does not exist. -2025-09-18T14:21:41.228Z [DEBUG] No changes in '.github/settings.yml' detected, returning... -2025-09-18T14:21:41.575Z [DEBUG] Checking existence of .github/repo.yml in jefeish-migration-test/safe-settings-config -2025-09-18T14:21:41.832Z [INFO] File .github/repo.yml not found in jefeish-migration-test/safe-settings-config (this is fine for both merge strategies) -2025-09-18T14:21:41.832Z [INFO] Syncing repo.yml to jefeish-migration-test (mergeStrategy=merge) -2025-09-18T14:21:42.191Z [DEBUG] Working on the default branch, returning... -2025-09-18T14:21:42.191Z [DEBUG] Is Admin repo event true -2025-09-18T14:21:42.736Z [INFO] Committed .github/repo.yml to jefeish-migration-test/safe-settings-config@main -2025-09-18T14:21:42.736Z [DEBUG] Preparing to sync file 'repo.yml' to org 'decyjphr-training' with mergeStrategy='merge' -2025-09-18T14:21:42.736Z [INFO] Changes pushed directly to jefeish-migration-test/safe-settings-config@main -2025-09-18T14:21:43.197Z [DEBUG] Preparing to sync file 'repo.yml' to org 'decyjphr-emu' with mergeStrategy='merge' -2025-09-18T14:21:43.197Z [INFO] Skipping org decyjphr-training: config repo 'safe-settings-config' does not exist. -2025-09-18T14:21:43.647Z [INFO] Skipping org decyjphr-emu: config repo 'safe-settings-config' does not exist. -2025-09-18T14:21:43.751Z [DEBUG] No changes in '.github/settings.yml' detected, returning... -2025-09-18T14:21:54.154Z [DEBUG] Working on the default branch, returning... -2025-09-18T14:21:54.154Z [DEBUG] Is Admin repo event true +2026-05-06T21:44:37.068Z [DEBUG] 5. [SYNC DEBUG] Hub: jefeish-training +2026-05-06T21:44:37.068Z [DEBUG] 4. [SYNC DEBUG] Org orgFilesResp file names: README.md, repo.yml, settings.yml, suborg.yml +2026-05-06T21:44:37.287Z [ERROR] 6a. [SYNC DEBUG] Error fetching hub files: HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content +2026-05-06T21:44:37.287Z [WARN] 6b. [SYNC DEBUG] File name mismatch for org jefeish-test1 +2026-05-06T21:44:43.619Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found +2026-05-06T21:44:45.456Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found +2026-05-06T21:44:49.024Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found diff --git a/lib/configManager.js b/lib/configManager.js index 58f5bb436..eb7c3710e 100644 --- a/lib/configManager.js +++ b/lib/configManager.js @@ -23,6 +23,11 @@ module.exports = class ConfigManager { this.log.error(`Error getting settings ${e}`) }) + // Return null if response is undefined (error occurred) + if (!response || !response.data) { + return null + } + // Ignore in case path is a folder // - https://developer.github.com/v3/repos/contents/#response-if-content-is-a-directory if (Array.isArray(response.data)) { diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js index 24d5b5496..621a36447 100644 --- a/lib/hubSyncHandler.js +++ b/lib/hubSyncHandler.js @@ -512,12 +512,35 @@ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { const baseBranch = options.baseBranch || 'main' // Resolve the base sha for creating branches - const baseRef = await githubHub.rest.git.getRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `heads/${baseBranch}` }) - const baseSha = baseRef.data && baseRef.data.object && baseRef.data.object.sha + let baseRef, baseSha + try { + baseRef = await githubHub.rest.git.getRef({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, ref: `heads/${baseBranch}` }) + baseSha = baseRef.data && baseRef.data.object && baseRef.data.object.sha + } catch (refErr) { + if (refErr && refErr.status === 404) { + // Hub repo doesn't exist - return N/A for all requested orgs + robot.log.warn(`Hub repository ${env.SAFE_SETTINGS_HUB_ORG}/${hubRepoName} or branch '${baseBranch}' not found`) + return orgNames.map(org => ({ org, status: 'N/A', reason: `hub_repo_not_found: ${env.SAFE_SETTINGS_HUB_ORG}/${hubRepoName}` })) + } + throw refErr + } // Helper: collect all files under a path in a repo (recursively) async function collectFilesFromRepo (githubClient, owner, repo, dirPath, ref = 'main') { const out = [] + + // First verify the repo exists by checking for the ref + try { + await githubClient.rest.git.getRef({ owner, repo, ref: `heads/${ref}` }) + } catch (repoCheckErr) { + if (repoCheckErr && repoCheckErr.status === 404) { + const err404 = new Error(`Repository ${owner}/${repo} or branch '${ref}' not found`) + err404.status = 404 + throw err404 + } + throw repoCheckErr + } + async function walk (p) { try { const resp = await githubClient.repos.getContent({ owner, repo, path: p, ref }) @@ -571,7 +594,7 @@ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { const destCheck = await githubHub.rest.repos.getContent({ owner: env.SAFE_SETTINGS_HUB_ORG, repo: hubRepoName, path: destOrgPath, ref: baseBranch }) if (Array.isArray(destCheck.data) && destCheck.data.length > 0) { robot.log.info(`Skipping ${orgName}: already present in hub`) - results.push({ org: orgName, skipped: true, reason: 'already_imported' }) + results.push({ org: orgName, status: 'imported', reason: 'already_imported' }) continue } } catch (probeErr) { @@ -604,10 +627,20 @@ async function retrieveSettingsFromOrgs (robot, orgNames = [], options = {}) { const sourceBase = (env.CONFIG_PATH || '.github').replace(/\/$/, '') // collect files from the source admin repo under CONFIG_PATH - const files = await collectFilesFromRepo(githubSrc, orgName, adminRepo, sourceBase, 'main') + let files + try { + files = await collectFilesFromRepo(githubSrc, orgName, adminRepo, sourceBase, 'main') + } catch (collectErr) { + if (collectErr && collectErr.status === 404) { + robot.log.info(`Skipping ${orgName}: admin repo '${adminRepo}' not found`) + results.push({ org: orgName, status: 'N/A', reason: `admin_repo_not_found: ${adminRepo}` }) + continue + } + throw collectErr + } if (!files || files.length === 0) { - results.push({ org: orgName, info: 'no files found at CONFIG_PATH' }) + results.push({ org: orgName, status: 'N/A', reason: 'no_files_at_config_path' }) continue } diff --git a/lib/routes.js b/lib/routes.js index 7a65ac847..e85f99ca8 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -315,6 +315,7 @@ function setupRoutes (robot, getRouter) { * Note: recursive now defaults to true. Pass recursive=false for single-level listing. */ async function hubContent (req, res) { + let fullPath, ref try { // Use cached installations (TTL-based freshness) const installs = await cacheGetInstallations(robot) @@ -325,8 +326,8 @@ function setupRoutes (robot, getRouter) { const github = await robot.auth(install.id) const wildcardPath = req.params[0] || '' // from the * in the route - const ref = req.query.ref || 'main' - const fullPath = wildcardPath ? path.posix.join(env.CONFIG_PATH, wildcardPath) : env.CONFIG_PATH + ref = req.query.ref || 'main' + fullPath = wildcardPath ? path.posix.join(env.CONFIG_PATH, wildcardPath) : env.CONFIG_PATH // recursive defaults to true unless explicitly disabled with recursive=false const recursive = req.query.recursive !== 'false' let maxDepth = parseInt(req.query.maxDepth, 5) @@ -470,12 +471,21 @@ function setupRoutes (robot, getRouter) { return { type: 'dir', name: path.posix.basename(dirPath), path: dirPath, depth, entries, ...commitMeta } } - const response = await github.repos.getContent({ - owner: env.SAFE_SETTINGS_HUB_ORG, - repo: env.SAFE_SETTINGS_HUB_REPO, - path: fullPath, - ref - }) + let response + try { + response = await github.repos.getContent({ + owner: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fullPath, + ref + }) + } catch (apiError) { + robot.log.error(`GitHub API error details: status=${apiError.status}, message=${apiError.message}`) + if (apiError.response && apiError.response.data) { + robot.log.error(`GitHub API response: ${JSON.stringify(apiError.response.data)}`) + } + throw apiError + } const data = response.data if (Array.isArray(data)) { @@ -553,7 +563,16 @@ function setupRoutes (robot, getRouter) { return res.status(415).json({ error: 'Unsupported content type returned by GitHub API' }) } catch (e) { if (e.status === 404) { - return res.status(404).json({ error: 'Not found' }) + robot.log.error(`Hub content 404: ${env.SAFE_SETTINGS_HUB_ORG}/${env.SAFE_SETTINGS_HUB_REPO} path=${fullPath} ref=${ref}`) + return res.status(404).json({ + error: 'Not found', + details: { + org: env.SAFE_SETTINGS_HUB_ORG, + repo: env.SAFE_SETTINGS_HUB_REPO, + path: fullPath, + ref + } + }) } robot.log && robot.log.error && robot.log.error(e) return res.status(500).json({ error: e.message || 'unexpected error' }) @@ -598,10 +617,12 @@ function setupRoutes (robot, getRouter) { // lazy-require to avoid circular require issues during module load const { retrieveSettingsFromOrgs } = require('./hubSyncHandler') const results = await retrieveSettingsFromOrgs(robot, orgs) + // Always return 200 with results, even if some/all orgs failed return res.json({ ok: true, results }) } catch (e) { robot.log && robot.log.error && robot.log.error(e) - return res.status(500).json({ error: e.message || 'unexpected error' }) + // Return 200 with error indicator for UI instead of 500 + return res.status(200).json({ ok: false, error: e.message || 'unexpected error', results: [] }) } }) diff --git a/ui/package-lock.json b/ui/package-lock.json index 9f7381e78..23c0972c1 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13,7 +13,10 @@ "d3-drag": "^3.0.0", "d3-force": "^3.0.0", "d3-selection": "^3.0.0", + "mermaid": "^11.14.0", "next": "^15.4.7", + "react-markdown": "^10.1.0", + "rehype-mermaid": "^3.0.0", "swr": "^2.3.6" }, "devDependencies": { @@ -37,6 +40,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "12.0.0", + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", + "license": "Apache-2.0" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -178,6 +237,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -244,6 +312,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.2.tgz", + "integrity": "sha512-jVf75icVVgSVGf9+QWBeCHdFL35yZ06HMHl9sCa059pITTP781lOacvRazfwAmXDKiBiUdQQMWVnuiw/RaQNhQ==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@img/sharp-darwin-x64": { "version": "0.34.3", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", @@ -345,6 +430,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, "node_modules/@next/env": { "version": "15.4.7", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.7.tgz", @@ -652,13 +746,298 @@ "tailwindcss": "4.1.12" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -673,6 +1052,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", @@ -961,6 +1378,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-darwin-x64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", @@ -975,6 +1398,16 @@ "darwin" ] }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1261,6 +1694,16 @@ "node": ">= 0.4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1391,6 +1834,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1408,6 +1861,74 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chevrotain": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "12.0.0", + "@chevrotain/gast": "12.0.0", + "@chevrotain/regexp-to-ast": "12.0.0", + "@chevrotain/types": "12.0.0", + "@chevrotain/utils": "12.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.3.tgz", + "integrity": "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.18.1" + }, + "peerDependencies": { + "chevrotain": "^12.0.0" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -1469,6 +1990,25 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1476,6 +2016,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1491,28 +2040,252 @@ "node": ">= 8" } }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/cytoscape": { + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz", + "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.10" } }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", "dependencies": { - "d3-dispatch": "1 - 3", + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", "d3-selection": "3" }, "engines": { "node": ">=12" } }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", @@ -1527,6 +2300,66 @@ "node": ">=12" } }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", @@ -1536,6 +2369,84 @@ "node": ">=12" } }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", @@ -1545,6 +2456,42 @@ "node": ">=12" } }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -1554,6 +2501,51 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1615,11 +2607,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1633,6 +2630,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1676,6 +2686,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1695,7 +2714,20 @@ "node": ">=8" } }, - "node_modules/doctrine": { + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", @@ -1708,6 +2740,15 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1744,6 +2785,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -2350,6 +3403,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2360,6 +3423,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2682,6 +3751,12 @@ "dev": true, "license": "MIT" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -2776,6 +3851,196 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2803,6 +4068,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2813,6 +4088,12 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -2828,6 +4109,39 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -2980,6 +4294,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3038,6 +4362,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -3091,6 +4425,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -3348,6 +4694,31 @@ "node": ">=4.0" } }, + "node_modules/katex": { + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3358,6 +4729,29 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/langium": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.3.tgz", + "integrity": "sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng==", + "license": "MIT", + "dependencies": { + "@chevrotain/regexp-to-ast": "~12.0.0", + "chevrotain": "~12.0.0", + "chevrotain-allstar": "~0.4.3", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -3378,6 +4772,12 @@ "node": ">=0.10" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3458,6 +4858,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3465,6 +4871,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3488,6 +4904,18 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3498,6 +4926,159 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3508,6 +5089,499 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid-isomorphic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mermaid-isomorphic/-/mermaid-isomorphic-3.1.0.tgz", + "integrity": "sha512-mzrvfEVjnJIkJlEqxp3eMuR1wS0TeLCH1VK5E/T5yzWaBwI3JqjJuw70yUIThSCDJ5bRs6O3rgfp00oBAbvSeQ==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.0.0", + "katex": "^0.16.0", + "mermaid": "^11.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "playwright": "1" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3522,6 +5596,15 @@ "node": ">=8.6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3588,7 +5671,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3903,6 +5985,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3916,6 +6004,49 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3962,6 +6093,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -4023,6 +6170,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4084,6 +6241,33 @@ "dev": true, "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -4128,6 +6312,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-mermaid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-mermaid/-/rehype-mermaid-3.0.0.tgz", + "integrity": "sha512-fxrD5E4Fa1WXUjmjNDvLOMT4XB1WaxcfycFIWiYU0yEMQhcTDElc9aDFnbDFRLxG1Cfo1I3mfD5kg4sjlWaB+Q==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "mermaid-isomorphic": "^3.0.0", + "mini-svg-data-uri": "^1.0.0", + "space-separated-tokens": "^2.0.0", + "unified": "^11.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "playwright": "1" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4180,6 +6425,24 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4204,6 +6467,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -4259,6 +6528,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -4489,6 +6764,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -4623,6 +6908,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -4646,6 +6945,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -4669,6 +6986,12 @@ } } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4743,6 +7066,15 @@ "node": ">=18" } }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -4804,6 +7136,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4817,6 +7169,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -4961,6 +7322,121 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -5015,6 +7491,120 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5152,6 +7742,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/ui/package.json b/ui/package.json index cfa48ab9d..83f3571e5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,10 @@ "d3-drag": "^3.0.0", "d3-force": "^3.0.0", "d3-selection": "^3.0.0", + "mermaid": "^11.14.0", "next": "^15.4.7", + "react-markdown": "^10.1.0", + "rehype-mermaid": "^3.0.0", "swr": "^2.3.6" }, "devDependencies": { From b6887a2a1704512969a6b34e626d963698ab3c17 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Fri, 15 May 2026 10:53:37 -0400 Subject: [PATCH 14/22] Start at 2.1.18-rc1 and add roles plugin and enhance settings integration --- lib/plugins/custom_properties.js | 50 ++++++++--- lib/plugins/custom_repository_roles.js | 119 +++++++++++++++++++++++++ lib/settings.js | 12 ++- 3 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 lib/plugins/custom_repository_roles.js diff --git a/lib/plugins/custom_properties.js b/lib/plugins/custom_properties.js index 6b1f3ab36..35f0144da 100644 --- a/lib/plugins/custom_properties.js +++ b/lib/plugins/custom_properties.js @@ -12,10 +12,24 @@ module.exports = class CustomProperties extends Diffable { // Force all names to lowercase to avoid comparison issues. normalizeEntries () { - this.entries = this.entries.map(({ name, value }) => ({ - name: name.toLowerCase(), - value - })) + this.entries = this.entries.reduce((normalizedEntries, entry) => { + if (!entry || typeof entry !== 'object') { + return normalizedEntries + } + + const entryName = entry.name || entry.property_name + + if (typeof entryName !== 'string') { + return normalizedEntries + } + + normalizedEntries.push({ + name: entryName.toLowerCase(), + value: entry.value + }) + + return normalizedEntries + }, []) } async find () { @@ -25,7 +39,7 @@ module.exports = class CustomProperties extends Diffable { this.log.debug(`Getting all custom properties for the repo ${repoFullName}`) const customProperties = await this.github.paginate( - this.github.repos.getCustomPropertiesValues, + this.github.rest.repos.getCustomPropertiesValues, { owner, repo, @@ -38,10 +52,24 @@ module.exports = class CustomProperties extends Diffable { // Force all names to lowercase to avoid comparison issues. normalize (properties) { - return properties.map(({ property_name: propertyName, value }) => ({ - name: propertyName.toLowerCase(), - value - })) + return properties.reduce((normalizedProperties, property) => { + if (!property || typeof property !== 'object') { + return normalizedProperties + } + + const propertyName = property.property_name || property.name + + if (typeof propertyName !== 'string') { + return normalizedProperties + } + + normalizedProperties.push({ + name: propertyName.toLowerCase(), + value: property.value + }) + + return normalizedProperties + }, []) } comparator (existing, attrs) { @@ -82,14 +110,14 @@ module.exports = class CustomProperties extends Diffable { return new NopCommand( this.constructor.name, this.repo, - this.github.repos.createOrUpdateCustomPropertiesValues.endpoint(params), + this.github.rest.repos.createOrUpdateCustomPropertiesValues.endpoint(params), `${operation} Custom Property` ) } try { this.log.debug(`${operation} Custom Property "${name}" for the repo ${repoFullName}`) - await this.github.repos.createOrUpdateCustomPropertiesValues(params) + await this.github.rest.repos.createOrUpdateCustomPropertiesValues(params) this.log.debug(`Successfully ${operation.toLowerCase()}d Custom Property "${name}" for the repo ${repoFullName}`) } catch (e) { this.logError(`Error during ${operation} Custom Property "${name}" for the repo ${repoFullName}: ${e.message || e}`) diff --git a/lib/plugins/custom_repository_roles.js b/lib/plugins/custom_repository_roles.js new file mode 100644 index 000000000..1931b47cc --- /dev/null +++ b/lib/plugins/custom_repository_roles.js @@ -0,0 +1,119 @@ +const Diffable = require('./diffable') +const NopCommand = require('../nopcommand') +const MergeDeep = require('../mergeDeep') + +// Fields returned by the API that we should ignore when diffing +const ignorableFields = ['id', 'organization', 'created_at', 'updated_at'] + +const version = { + 'X-GitHub-Api-Version': '2026-03-10' +} + +module.exports = class CustomRepositoryRoles extends Diffable { + constructor (nop, github, repo, entries, log, errors) { + super(nop, github, repo, entries, log, errors) + this.github = github + this.repo = repo + this.entries = entries + this.log = log + this.nop = nop + } + + // Find all Custom Repository Roles for the org + find () { + this.log.debug(`Getting all custom repository roles for the org ${this.repo.owner}`) + + return this.github.request('GET /orgs/{org}/custom-repository-roles', { + org: this.repo.owner, + headers: version + }).then(res => { + const roles = (res && res.data && res.data.custom_roles) || [] + // Strip noise so deep-diff focuses on the configurable fields + return roles.map(r => ({ + id: r.id, + name: r.name, + description: r.description, + base_role: r.base_role, + permissions: r.permissions + })) + }).catch(e => { + return this.handleError(e, []) + }) + } + + comparator (existing, attrs) { + return existing.name === attrs.name + } + + changed (existing, attrs) { + const mergeDeep = new MergeDeep(this.log, this.github, ignorableFields) + const merged = mergeDeep.compareDeep(existing, attrs) + return merged.hasChanges + } + + update (existing, attrs) { + const parms = this.wrapAttrs(Object.assign({ role_id: existing.id }, attrs)) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint('PATCH /orgs/{org}/custom-repository-roles/{role_id}', parms), 'Update Custom Repository Role') + ]) + } + this.log.debug(`Updating Custom Repository Role with the following values ${JSON.stringify(parms, null, 2)}`) + return this.github.request('PATCH /orgs/{org}/custom-repository-roles/{role_id}', parms).then(res => { + this.log.debug(`Custom Repository Role updated successfully ${JSON.stringify(res.url)}`) + return res + }).catch(e => { + return this.handleError(e) + }) + } + + add (attrs) { + const parms = this.wrapAttrs(attrs) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint('POST /orgs/{org}/custom-repository-roles', parms), 'Create Custom Repository Role') + ]) + } + this.log.debug(`Creating Custom Repository Role with the following values ${JSON.stringify(parms, null, 2)}`) + return this.github.request('POST /orgs/{org}/custom-repository-roles', parms).then(res => { + this.log.debug(`Custom Repository Role created successfully ${JSON.stringify(res.url)}`) + return res + }).catch(e => { + return this.handleError(e) + }) + } + + remove (existing) { + const parms = this.wrapAttrs({ role_id: existing.id }) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint('DELETE /orgs/{org}/custom-repository-roles/{role_id}', parms), 'Delete Custom Repository Role') + ]) + } + this.log.debug(`Deleting Custom Repository Role with the following values ${JSON.stringify(parms, null, 2)}`) + return this.github.request('DELETE /orgs/{org}/custom-repository-roles/{role_id}', parms).then(res => { + this.log.debug(`Custom Repository Role deleted successfully ${JSON.stringify(res.url)}`) + return res + }).catch(e => { + if (e.status === 404) { + return + } + return this.handleError(e) + }) + } + + wrapAttrs (attrs) { + return Object.assign({}, attrs, { + org: this.repo.owner, + headers: version + }) + } + + handleError (e, returnValue) { + this.logError(e) + if (this.nop) { + return Promise.resolve([(new NopCommand(this.constructor.name, this.repo, null, `error: ${e}`, 'ERROR'))]) + } + return Promise.resolve(returnValue) + } +} diff --git a/lib/settings.js b/lib/settings.js index 8d9e07b2b..4815e5a6b 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -319,7 +319,15 @@ ${this.results.reduce((x, y) => { const rulesetsConfig = this.config.rulesets if (rulesetsConfig) { const RulesetsPlugin = Settings.PLUGINS.rulesets - return new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync().then(res => { + await new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync().then(res => { + this.appendToResults(res) + }) + } + + const customRepositoryRolesConfig = this.config.custom_repository_roles + if (customRepositoryRolesConfig) { + const CustomRepositoryRolesPlugin = Settings.PLUGINS.custom_repository_roles + await new CustomRepositoryRolesPlugin(this.nop, this.github, this.repo, customRepositoryRolesConfig, this.log, this.errors).sync().then(res => { this.appendToResults(res) }) } @@ -434,6 +442,7 @@ ${this.results.reduce((x, y) => { returnRepoSpecificConfigs (config) { const newConfig = Object.assign({}, config) // clone delete newConfig.rulesets + delete newConfig.custom_repository_roles return newConfig } @@ -1004,6 +1013,7 @@ Settings.PLUGINS = { rulesets: require('./plugins/rulesets'), environments: require('./plugins/environments'), custom_properties: require('./plugins/custom_properties.js'), + custom_repository_roles: require('./plugins/custom_repository_roles'), variables: require('./plugins/variables') } From bdcc6b57eae13dc3474b24ed47ffd0fcb329f988 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Fri, 15 May 2026 10:54:07 -0400 Subject: [PATCH 15/22] Add custom repository roles schema to settings.json --- schema/settings.json | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/schema/settings.json b/schema/settings.json index 4d390b38f..3649d88ea 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -191,6 +191,48 @@ "items": { "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1rulesets/post/requestBody/content/application~1json/schema" } + }, + "custom_repository_roles": { + "description": "Org-level custom repository roles. Only valid in the org-level settings.yml.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "base_role", + "permissions" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the custom role." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "A short description of the role." + }, + "base_role": { + "type": "string", + "enum": [ + "read", + "triage", + "write", + "maintain" + ], + "description": "The system role from which this role inherits permissions." + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional fine-grained permissions included in this role." + } + } + } } } -} +} \ No newline at end of file From 1d739f9f8cfa8bb4d72c8a53d779e52cf169cc58 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Tue, 19 May 2026 01:59:10 -0400 Subject: [PATCH 16/22] Add sub-org reevaluation logic and smoke tests --- README.md | 87 +++ lib/plugins/diffable.js | 6 + lib/plugins/repository.js | 12 + lib/settings.js | 176 +++++- package.json | 3 +- smoke-test.js | 945 +++++++++++++++++++++++++++++++++ test/unit/lib/settings.test.js | 181 +++++++ 7 files changed, 1405 insertions(+), 5 deletions(-) create mode 100644 smoke-test.js diff --git a/README.md b/README.md index 07a626748..b7382de34 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,27 @@ The App listens to the following webhook events: - __custom_property_values__: If new repository properties are set for a repository, `safe-settings` will run to so that if a sub-org config is defined by that property, it will be applied for the repo +### Suborg re-evaluation after repo-level changes + +A repo's suborg membership can depend on state that is itself written by `safe-settings`: + +- `suborgteams` — repos belong to a suborg because a given team is granted access +- `suborgproperties` — repos belong to a suborg because a custom property has a given value +- `suborgrepos` — repos belong to a suborg because their name matches a glob + +When a repo-level change (a push to `.github/repos/.yml`, or a `repository.created` event for a brand-new repo) adds a team, sets a custom property, or creates a repo whose name matches a suborg's `suborgrepos` glob, the repo may *newly* match a suborg config that was not applied in the first pass. + +To handle this, after applying a repo-yml change `safe-settings` re-evaluates the repo's suborg membership and, if a new suborg now matches, runs the repo through the apply pipeline a second time so the suborg's settings are picked up in the same sync. + +**Scope:** Re-evaluation runs only on the repo-yml change paths (`Settings.sync` and the per-repo loop of `Settings.syncSelectedRepos`). Global settings changes (`syncAll`) and suborg-yml changes (`syncSubOrgs`) already iterate all relevant repos and do not need it. + +**Loop prevention.** Two guards prevent infinite re-evaluation: + +1. **Stability check (primary):** Before applying changes, `safe-settings` snapshots the set of suborg source paths that match the repo. After applying, it refreshes the suborg cache and recomputes the set. If no new suborg source appeared, re-evaluation stops. +2. **Hard depth cap (safety net):** Each repo is re-evaluated at most `MAX_REEVALUATION_DEPTH = 1` time per sync. This resolves the dominant single-hop case (repo change → newly-matched suborg → apply suborg once) while preventing pathological chains (suborg A applies a team that activates suborg B that activates suborg C…). Chains beyond one hop are resolved on the next sync event, and a warning is logged when the cap is hit. + +**Trigger optimization.** Re-evaluation is skipped entirely when the resolved `repoConfig` has no `teams`, no `custom_properties`, and is not a rename — these are the only repo-level changes that can affect suborg matching. + ### Use `safe-settings` to rename repos If you rename a `` that corresponds to a repo, safe-settings will rename the repo to the new name. This behavior will take effect whether the env variable `BLOCK_REPO_RENAME_BY_HUMAN` is set or not. @@ -573,7 +594,73 @@ You can pass environment variables; the easiest way to do it is via a `.env` fil 3. __[Deploy and install the app](docs/deploy.md)__. Alternatively, the __[GitHub Actions Guide](docs/github-action.md)__ describes how to run `safe-settings` with GitHub Actions. +## Smoke Testing + +The repository includes an end-to-end smoke test script (`smoke-test.js`) that validates safe-settings against a live GitHub organization. It starts the app, creates repos/configs via the API, and verifies that safe-settings correctly applies and enforces settings. + +### Prerequisites + +- **Node.js** (same version used to run safe-settings) +- **`gh` CLI** — authenticated and available on PATH (used for drift-remediation tests only) +- A **GitHub App** installed on the target org with the required permissions +- A `.env` file in the project root (see below) + +### Authentication + +The smoke test uses **two authentication methods**: + +- **GitHub App token** (via `APP_ID` + `PRIVATE_KEY`) — used for the majority of tests: creating configs, merging PRs, validating repos, teams, rulesets, custom properties, etc. +- **Fine-grained PAT** (via `GH_TOKEN`) — used **only** in Phase 2 (team removal) and Phase 3 (rogue ruleset creation). These drift-remediation tests must appear as a human action because safe-settings ignores webhook events where `sender.type` is `Bot`. +### Configuration + +Add the following to your `.env` file: + +| Variable | Description | Required | +|---|---|---| +| `GH_ORG` | Target GitHub organization (e.g. `my-org`) | Yes | +| `APP_ID` | GitHub App ID | Yes | +| `PRIVATE_KEY` | GitHub App private key (use `\n` for newlines) | Yes | +| `WEBHOOK_PROXY_URL` | Smee.io proxy URL for webhooks | Yes | +| `ADMIN_REPO` | Admin repo name (default: `admin`) | No | +| `CONFIG_PATH` | Config path within admin repo (default: `.github`) | No | +| `GH_TOKEN` | Fine-grained PAT with org admin + repo permissions | Yes | +| `SMOKE_VERBOSE` | Set to `1` to show live safe-settings logs | No | + +### Running + +```bash +npm run smoke-test +# or +node smoke-test.js +``` + +### What it tests + +The smoke test runs 9 phases: + +| Phase | Description | +|---|---| +| **Setup** | Initializes the admin repo with an empty `settings.yml`, removes stale test repos, and starts safe-settings | +| **Phase 1** | Creates a repo config (`test`), validates NOP mode via check runs, merges, and verifies repo creation, teams, custom properties, and rulesets | +| **Phase 2** | Removes a team from the repo and verifies safe-settings re-adds it (drift remediation) | +| **Phase 3** | Creates a rogue ruleset and verifies safe-settings removes it (drift remediation) | +| **Phase 4** | Creates `demo-repo-service1` with teams, topics, and branch protection | +| **Phase 5** | Creates a suborg config and verifies org-scoped rulesets are applied to matching repos | +| **Phase 6** | Archives `demo-repo-service1` and verifies the repo is archived | +| **Phase 7** | Creates `demo-repo-service2` and verifies suborg rulesets are inherited | +| **Phase 8** | Creates org-level settings (custom repository roles + org rulesets) and verifies they are applied | +| **Teardown** | Shuts down safe-settings, deletes test repos, teams, custom roles, and rulesets | + +### Output + +The script uses colored terminal output with pass (✅) / fail (❌) indicators and prints a summary at the end: + +``` +══════════════════════════════════════ + Results: 45 passed, 0 failed +══════════════════════════════════════ +``` ## License diff --git a/lib/plugins/diffable.js b/lib/plugins/diffable.js index 069c68c78..44e5ae50c 100644 --- a/lib/plugins/diffable.js +++ b/lib/plugins/diffable.js @@ -62,6 +62,11 @@ module.exports = class Diffable extends ErrorStash { sync () { const resArray = [] + // Will be set to true when this plugin makes (or would make, in nop mode) + // any add/update/remove. Consumers (e.g. Settings suborg re-evaluation) + // can read `plugin.hasChanges` after `sync()` resolves to know whether + // anything actually changed for this repo. + this.hasChanges = false if (this.entries) { let filteredEntries = this.filterEntries() // this.log.debug(`filtered entries are ${JSON.stringify(filteredEntries)}`) @@ -72,6 +77,7 @@ module.exports = class Diffable extends ErrorStash { const compare = mergeDeep.compareDeep(existingRecords, filteredEntries) const results = { msg: 'Changes found', additions: compare.additions, modifications: compare.modifications, deletions: compare.deletions } this.log.debug(`Results of comparing ${this.constructor.name} diffable target ${JSON.stringify(existingRecords)} with source ${JSON.stringify(filteredEntries)} is ${JSON.stringify(results)}`) + this.hasChanges = !!compare.hasChanges if (!compare.hasChanges) { this.log.debug(`There are no changes for ${this.constructor.name} for repo ${this.repo.repo}. Skipping changes`) return Promise.resolve() diff --git a/lib/plugins/repository.js b/lib/plugins/repository.js index 14599f608..3333225d2 100644 --- a/lib/plugins/repository.js +++ b/lib/plugins/repository.js @@ -62,6 +62,10 @@ module.exports = class Repository extends ErrorStash { const resArray = [] this.log.debug(`Syncing Repo ${this.settings.name}`) this.settings.name = this.settings.name || this.settings.repo + // Change signals consumed by Settings suborg re-evaluation. + this.hasChanges = false + this.renamed = false + this.created = false // let hasChanges = false // let hasTopicChanges = false return this.github.repos.get(this.repo) @@ -74,6 +78,12 @@ module.exports = class Repository extends ErrorStash { const topicChanges = mergeDeep.compareDeep({ entries: resp.data.topics }, { entries: this.topics }) // hasTopicChanges = topicChanges.additions.length > 0 || topicChanges.modifications.length > 0 + this.hasChanges = !!(changes.hasChanges || topicChanges.hasChanges) + // A repo rename (changing the slug) shows up as a `name` modification. + if (changes.hasChanges && this.settings.name && resp.data.name && this.settings.name !== resp.data.name) { + this.renamed = true + } + // const results = JSON.stringify(changes, null, 2) const results = { msg: `${this.constructor.name} settings changes`, additions: changes.additions, modifications: changes.modifications, deletions: changes.deletions } @@ -120,6 +130,8 @@ module.exports = class Repository extends ErrorStash { }).catch(e => { if (e.status === 404) { if (this.force_create) { + this.hasChanges = true + this.created = true if (this.template) { this.log.debug(`Creating repo using template ${this.template}`) const options = { template_owner: this.repo.owner, template_repo: this.template, owner: this.repo.owner, name: this.repo.repo, private: (this.settings.private ? this.settings.private : true), description: this.settings.description ? this.settings.description : '' } diff --git a/lib/settings.js b/lib/settings.js index 4815e5a6b..d104efa5b 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -12,6 +12,12 @@ const eta = new Eta({ views: path.join(__dirname) }) const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting const yaml = require('js-yaml') +// When a repo-yml change applies teams/properties/etc to a repo, the repo may +// newly match a suborg config (via suborgteams/suborgproperties/suborgrepos). +// Re-run updateRepos for the same repo at most this many times. Depth=1 is the +// tightest cap: we resolve a single hop of newly-matched suborg per sync. +const MAX_REEVALUATION_DEPTH = 1 + class Settings { static fileCache = {} @@ -46,6 +52,10 @@ class Settings { const settings = new Settings(nop, context, context.repo(), config, ref) try { + // Re-eval is enabled only for the per-repo iteration (repo-yml change + // path). The trailing suborg iteration below already iterates all suborg + // repos, so it is left with the flag off. + settings.reevaluateOnChange = true for (const repo of repos) { settings.repo = repo await settings.loadConfigs(repo) @@ -54,6 +64,7 @@ class Settings { } await settings.updateRepos(repo) } + settings.reevaluateOnChange = false for (const suborg of subOrgs) { settings.subOrgConfigMap = [suborg] settings.suborgChange = !!suborg @@ -70,6 +81,10 @@ class Settings { static async sync (nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) try { + // Repo-yml change path: re-evaluate suborg membership for this repo if + // the applied changes (teams/custom_properties/new repo) cause it to + // newly match a suborg config. + settings.reevaluateOnChange = true await settings.loadConfigs(repo) if (settings.isRestricted(repo.repo)) { return @@ -124,6 +139,13 @@ class Settings { } } this.mergeDeep = new MergeDeep(this.log, this.github, [], this.configvalidators, this.overridevalidators) + // Suborg re-evaluation state (used only when reevaluateOnChange is true). + // - reevaluationDepth: repo name -> number of re-evaluation passes done. + // - reevaluatedRepos: repo name -> set of suborg source paths seen so far + // (used for stability comparison; if no new sources appear, we stop). + this.reevaluateOnChange = false + this.reevaluationDepth = new Map() + this.reevaluatedRepos = new Map() } // Create a check in the Admin repo for safe-settings. @@ -335,6 +357,12 @@ ${this.results.reduce((x, y) => { async updateRepos (repo) { this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs() + // Snapshot the set of suborg `source` paths that match this repo *before* + // we apply any changes. We compare against the post-apply set below to + // decide whether to re-evaluate (and to break stable loops). + const preMatchedSuborgSources = this.reevaluateOnChange + ? this.getAllMatchingSubOrgSources(repo.repo) + : null // Keeping this as is instead of doing an object assign as that would cause `Cannot read properties of undefined (reading 'startsWith')` error // Copilot code review would recoommend using object assign but that would cause the error let repoConfig = this.config.repository @@ -367,6 +395,10 @@ ${this.results.reduce((x, y) => { repoConfig = this.mergeDeep.mergeDeep({}, repoConfig, overrideRepoConfig) } if (repoConfig) { + // Track actual change signals from the plugins, used by the suborg + // re-evaluation logic below to avoid an unnecessary live API round-trip + // when nothing relevant actually changed. + const changeSignals = { teamsChanged: false, propertiesChanged: false, renamed: false, created: false } try { this.log.debug(`found a matching repoconfig for this repo ${JSON.stringify(repoConfig)}`) @@ -382,16 +414,27 @@ ${this.results.reduce((x, y) => { this.appendToResults(unArchiveResults) } - const repoResults = await new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync() + const repoPluginInstance = new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors) + const repoResults = await repoPluginInstance.sync() this.appendToResults(repoResults) + if (repoPluginInstance.renamed) changeSignals.renamed = true + if (repoPluginInstance.created) changeSignals.created = true + const childPluginInstances = childPlugins.map(([Plugin, config]) => { + return [Plugin, new Plugin(this.nop, this.github, repo, config, this.log, this.errors)] + }) const childResults = await Promise.all( - childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync() - }) + childPluginInstances.map(([, instance]) => instance.sync()) ) this.appendToResults(childResults) + // Collect change signals from relevant child plugins. + for (const [Plugin, instance] of childPluginInstances) { + if (!instance.hasChanges) continue + if (Plugin === Settings.PLUGINS.teams) changeSignals.teamsChanged = true + if (Plugin === Settings.PLUGINS.custom_properties) changeSignals.propertiesChanged = true + } + if (shouldArchive) { this.log.debug(`Archiving repo ${repo.repo}`) const archiveResults = await archivePlugin.sync() @@ -407,6 +450,14 @@ ${this.results.reduce((x, y) => { throw e } } + + // Suborg re-evaluation: if a repo-yml change actually applied teams or + // custom_properties (or this repo was just renamed/created), the repo + // may newly match a suborg config (suborgteams/suborgproperties/ + // suborgrepos). Refresh the suborg cache, compare matched-source sets; + // if it grew, re-run updateRepos once for this repo. Bounded by + // MAX_REEVALUATION_DEPTH and a stable-set check to prevent loops. + await this.maybeReevaluateSuborg(repo, repoConfig, preMatchedSuborgSources, changeSignals) } else { this.log.debug(`Didnt find any a matching repoconfig for this repo ${JSON.stringify(repo)} in ${JSON.stringify(this.repoConfigs)}`) const childPlugins = this.childPluginsList(repo) @@ -438,6 +489,123 @@ ${this.results.reduce((x, y) => { return undefined } + // Read-only helper used for suborg re-evaluation stability checks. + // Returns the set of suborg `source` paths (i.e. the suborg config file path) + // that match the given repo name. Apply-time behavior is unchanged: + // `getSubOrgConfig` still returns the first match and + // `storeSubOrgConfigIfNoConflicts` still forbids multi-suborg overlap at + // config-load time -- so this set normally contains 0 or 1 entries. We + // expose it as a Set so callers can detect the transition from {} -> {pathA} + // when a repo newly matches a suborg after teams/properties are applied. + getAllMatchingSubOrgSources (repoName) { + const sources = new Set() + if (!this.subOrgConfigs) { + return sources + } + for (const pattern of Object.keys(this.subOrgConfigs)) { + const glob = new Glob(pattern) + if (glob.test(repoName)) { + const source = this.subOrgConfigs[pattern]?.source + if (source) { + sources.add(source) + } + } + } + return sources + } + + // Force a refresh of the cached suborg configs. Used by the re-eval loop + // because suborgteams / suborgproperties resolution calls live GitHub APIs + // and may now match the repo after teams/properties were applied in the + // first pass. + async reloadSubOrgConfigs () { + this.subOrgConfigs = await this.getSubOrgConfigs() + } + + // Decide whether applying this repo's config actually changed state that + // could affect suborg matching. If no relevant change happened, skip the + // re-eval API roundtrip entirely. + // + // Preferred path: use plugin-emitted change signals from the just-completed + // sync (teams plugin actually added/removed/updated, custom_properties + // plugin changed values, repository plugin renamed/created). These come + // from the Diffable base class (`plugin.hasChanges`) and the Repository + // plugin (`renamed`, `created`). + // + // Fallback (changeSignals omitted, e.g. unit tests calling the helper in + // isolation): inspect the per-repo yml top-level shape for teams / + // custom_properties / rename indicators. + shouldConsiderReevaluation (repo, repoConfig, changeSignals) { + if (changeSignals) { + return !!( + changeSignals.teamsChanged || + changeSignals.propertiesChanged || + changeSignals.renamed || + changeSignals.created + ) + } + const repoYml = this.repoConfigs && ( + this.repoConfigs[`${repo.repo}.yml`] || this.repoConfigs[`${repo.repo}.yaml`] + ) + if (repoYml) { + if (Array.isArray(repoYml.teams) && repoYml.teams.length > 0) return true + if (Array.isArray(repoYml.custom_properties) && repoYml.custom_properties.length > 0) return true + } + if (repo && repo.oldname && repo.oldname !== repo.repo) return true + if (repoConfig && repoConfig.oldname && repoConfig.oldname !== repoConfig.name) return true + return false + } + + // After applying changes to a repo, decide whether to re-run updateRepos + // because the applied changes may have caused the repo to newly match a + // suborg config. Loop prevention has two layers: + // 1. Hard cap: MAX_REEVALUATION_DEPTH (=1) re-evaluation passes per repo. + // 2. Stability check: stop if the set of matched suborg sources did not + // grow (no new suborg source appeared since the last pass). + async maybeReevaluateSuborg (repo, repoConfig, preMatchedSuborgSources, changeSignals) { + if (!this.reevaluateOnChange) return + if (!preMatchedSuborgSources) return + if (!this.shouldConsiderReevaluation(repo, repoConfig, changeSignals)) { + this.log.debug(`Suborg re-eval: skipping for ${repo.repo} (no relevant changes from teams/custom_properties/repository plugins)`) + return + } + + const depth = this.reevaluationDepth.get(repo.repo) || 0 + if (depth >= MAX_REEVALUATION_DEPTH) { + this.log.warn(`Suborg re-eval: max depth (${MAX_REEVALUATION_DEPTH}) reached for ${repo.repo}; stopping. Any further suborg matches will be picked up on the next sync.`) + return + } + + // Refresh suborg config cache; suborgteams/suborgproperties resolution + // hits live GitHub APIs and may now match this repo. + await this.reloadSubOrgConfigs() + + const seen = this.reevaluatedRepos.get(repo.repo) || new Set(preMatchedSuborgSources) + const newMatched = this.getAllMatchingSubOrgSources(repo.repo) + + // Stability check: if no new suborg source appeared, we're done. + let hasNew = false + for (const source of newMatched) { + if (!seen.has(source)) { + hasNew = true + seen.add(source) + } + } + if (!hasNew) { + this.log.debug(`Suborg re-eval: stable for ${repo.repo} (matched sources: ${JSON.stringify(Array.from(newMatched))}); stopping.`) + return + } + + this.reevaluatedRepos.set(repo.repo, seen) + this.reevaluationDepth.set(repo.repo, depth + 1) + this.log.debug(`Suborg re-eval: new suborg source(s) matched ${repo.repo} after apply; re-running updateRepos (depth=${depth + 1}).`) + + // Reload repo-level configs for this repo so the next pass picks up any + // state changes; then recurse. Depth cap above prevents infinite loops. + this.repoConfigs = await this.getRepoConfigs(repo) + await this.updateRepos(repo) + } + // Remove Org specific configs from the repo config returnRepoSpecificConfigs (config) { const newConfig = Object.assign({}, config) // clone diff --git a/package.json b/package.json index fbfa284da..f0ae365bf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "test:me": "jest ", "test:unit:watch": "npm run test:unit -- --watch", "test:integration": "jest --roots=lib --roots=test/integration", - "test:integration:debug": "LOG_LEVEL=debug DEBUG=nock run-s test:integration" + "test:integration:debug": "LOG_LEVEL=debug DEBUG=nock run-s test:integration", + "smoke-test": "node smoke-test.js" }, "author": "Yadhav Jayaraman", "license": "ISC", diff --git a/smoke-test.js b/smoke-test.js new file mode 100644 index 000000000..dcb4df8af --- /dev/null +++ b/smoke-test.js @@ -0,0 +1,945 @@ +#!/usr/bin/env node + +/** + * Smoke Test for safe-settings + * + * Usage: + * 1. Ensure `.env` is configured with GH_ORG, APP_ID, PRIVATE_KEY, WEBHOOK_PROXY_URL, etc. + * 2. Set GH_TOKEN env var to a fine-grained PAT with org admin + repo permissions. + * This is required for drift-remediation tests (Phases 2 & 3) so that + * changes appear as a human (not Bot) and trigger safe-settings webhooks. + * 3. Run: `node smoke-test.js` + * Set SMOKE_VERBOSE=1 for live safe-settings logs. + * + * Auth: + * - Octokit (GitHub App): APP_ID + PRIVATE_KEY from .env — used for most operations. + * - gh CLI (user PAT): GH_TOKEN env var — used for drift tests only. + */ + +const { execSync, spawn } = require('child_process') +const fs = require('fs') +const path = require('path') + +// ─── Configuration ─────────────────────────────────────────────────────────── + +function loadEnv () { + const envPath = path.join(__dirname, '.env') + if (!fs.existsSync(envPath)) throw new Error('.env file not found') + const lines = fs.readFileSync(envPath, 'utf8').split('\n') + let currentKey = null + let currentValue = '' + let inMultiline = false + + for (const line of lines) { + if (inMultiline) { + currentValue += '\n' + line + if (line.includes('"') || line.includes("'")) { + const val = currentValue.replace(/^["']|["']$/g, '') + // Like dotenv: .env values don't override existing env vars + if (!(currentKey in process.env)) process.env[currentKey] = val + inMultiline = false + } + continue + } + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const eqIdx = trimmed.indexOf('=') + if (eqIdx === -1) continue + currentKey = trimmed.slice(0, eqIdx).trim() + currentValue = trimmed.slice(eqIdx + 1).trim() + if ((currentValue.startsWith('"') && !currentValue.endsWith('"')) || + (currentValue.startsWith("'") && !currentValue.endsWith("'"))) { + inMultiline = true + continue + } + const val = currentValue.replace(/^["']|["']$/g, '') + if (!(currentKey in process.env)) process.env[currentKey] = val + } +} + +loadEnv() + +const ORG = process.env.GH_ORG || 'decyjphr-emu' +const ADMIN_REPO = process.env.ADMIN_REPO || 'admin' +const CONFIG_PATH = process.env.CONFIG_PATH || '.github' +const APP_ID = process.env.APP_ID +const PRIVATE_KEY = (process.env.PRIVATE_KEY || '').replace(/\\n/g, '\n') + +const TEST_REPOS = ['test', 'demo-repo-service1', 'demo-repo-service2'] +const TEST_TEAMS = ['AD-GRP-PAYMENTS-PLATFORM-OWNERS', 'awesometeam-a-approvers'] + +const POLL_INTERVAL_MS = 5000 +const MAX_POLL_MS = 120000 +const WEBHOOK_SETTLE_MS = 15000 + +// Fine-grained PAT for drift tests (must appear as a human, not Bot) +const GH_TOKEN = process.env.GH_TOKEN || '' + +// ─── Octokit client (initialized in main) ──────────────────────────────────── + +let octokit = null + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +let passCount = 0 +let failCount = 0 +const failures = [] + +function log (msg) { console.log(`\x1b[36m[smoke]\x1b[0m ${msg}`) } +function logPass (msg) { passCount++; console.log(`\x1b[32m ✓ ${msg}\x1b[0m`) } +function logFail (msg) { failCount++; failures.push(msg); console.log(`\x1b[31m ✗ ${msg}\x1b[0m`) } +function logPhase (msg) { console.log(`\n\x1b[35m═══ ${msg} ═══\x1b[0m`) } + +function assert (condition, msg) { + if (condition) logPass(msg) + else logFail(msg) + return condition +} + +function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } + +async function poll (fn, { timeout = MAX_POLL_MS, interval = POLL_INTERVAL_MS, desc = 'condition' } = {}) { + const start = Date.now() + while (Date.now() - start < timeout) { + const result = await fn() + if (result) return result + await sleep(interval) + } + log(` ⚠ Timed out waiting for ${desc}`) + return null +} + +// ─── GitHub API helpers ────────────────────────────────────────────────────── + +async function getDefaultBranch () { + const { data } = await octokit.rest.repos.get({ owner: ORG, repo: ADMIN_REPO }) + return data.default_branch || 'main' +} + +async function createOrUpdateFile (owner, repo, filePath, content, branch, message) { + const b64 = Buffer.from(content).toString('base64') + let sha = null + try { + const { data } = await octokit.rest.repos.getContent({ owner, repo, path: filePath, ref: branch }) + sha = data.sha + } catch { /* file doesn't exist */ } + const params = { owner, repo, path: filePath, message, content: b64, branch } + if (sha) params.sha = sha + return (await octokit.rest.repos.createOrUpdateFileContents(params)).data +} + +async function deleteFile (owner, repo, filePath, branch, message) { + try { + const { data } = await octokit.rest.repos.getContent({ owner, repo, path: filePath, ref: branch }) + await octokit.rest.repos.deleteFile({ owner, repo, path: filePath, message, sha: data.sha, branch }) + } catch { /* file doesn't exist */ } +} + +async function cleanDirectory (owner, repo, dirPath) { + const branch = await getDefaultBranch() + try { + const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dirPath, ref: branch }) + if (Array.isArray(data)) { + for (const file of data) { + if (file.type === 'file') { + await deleteFile(owner, repo, file.path, branch, `Clean up ${file.path}`) + } + } + } + } catch { /* directory doesn't exist */ } +} + +async function createBranch (owner, repo, branchName) { + const defaultBranch = await getDefaultBranch() + const { data: ref } = await octokit.rest.git.getRef({ owner, repo, ref: `heads/${defaultBranch}` }) + await octokit.rest.git.createRef({ owner, repo, ref: `refs/heads/${branchName}`, sha: ref.object.sha }) +} + +async function deleteBranch (owner, repo, branch) { + try { await octokit.rest.git.deleteRef({ owner, repo, ref: `heads/${branch}` }) } catch { /* ok */ } +} + +async function createPR (owner, repo, title, head, base) { + const { data } = await octokit.rest.pulls.create({ owner, repo, title, head, base, body: `Smoke test: ${title}` }) + log(` Created PR #${data.number}`) + return data +} + +async function mergePR (owner, repo, prNumber) { + return (await octokit.rest.pulls.merge({ owner, repo, pull_number: prNumber, merge_method: 'merge' })).data +} + +async function deleteRepo (owner, repo) { + try { await octokit.rest.repos.delete({ owner, repo }) } catch { /* ok */ } +} + +async function deleteTeam (org, teamSlug) { + try { await octokit.rest.teams.deleteInOrg({ org, team_slug: teamSlug }) } catch { /* ok */ } +} + +async function waitForCheckRun (owner, repo, sha, { timeout = MAX_POLL_MS } = {}) { + return poll(async () => { + const { data } = await octokit.rest.checks.listForRef({ owner, repo, ref: sha }) + const cr = data.check_runs.find(c => c.name === 'Safe-setting validator') + return (cr && cr.status === 'completed') ? cr : null + }, { timeout, desc: 'check run to complete' }) +} + +// ─── Safe-settings process management ──────────────────────────────────────── + +let ssProcess = null + +function startSafeSettings () { + log('Starting safe-settings...') + ssProcess = spawn('npm', ['start'], { + cwd: __dirname, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'] + }) + ssProcess.stdout.on('data', (d) => { if (process.env.SMOKE_VERBOSE) process.stdout.write(d) }) + ssProcess.stderr.on('data', (d) => { if (process.env.SMOKE_VERBOSE) process.stderr.write(d) }) + ssProcess.on('exit', (code) => { log(`safe-settings exited with code ${code}`) }) +} + +function stopSafeSettings () { + if (ssProcess) { + log('Stopping safe-settings...') + ssProcess.kill('SIGTERM') + ssProcess = null + } +} + +// ─── YAML Configs ──────────────────────────────────────────────────────────── + +const REPO_TEST_YML = `repository: + name: test + description: Demo repository created via safe-settings + private: true + auto_init: true + force_create: true + has_issues: true + has_projects: false + has_wiki: false + delete_branch_on_merge: true + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: true + +teams: + - name: expert-services-developers + permission: push + +custom_properties: + - property_name: ent-ownership + value: expert-services + - property_name: ent-supervisory-org + value: expert-services + +rulesets: +- name: synk + target: branch + enforcement: disabled + bypass_actors: + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: pull_request + + conditions: + ref_name: + include: ["~DEFAULT_BRANCH"] + exclude: ["refs/heads/oldmaster"] + + rules: + - type: creation + - type: update + - type: deletion + - type: required_linear_history + - type: required_signatures + - type: pull_request + parameters: + dismiss_stale_reviews_on_push: true + require_code_owner_review: true + require_last_push_approval: true + required_approving_review_count: 2 + required_review_thread_resolution: true + + - type: commit_message_pattern + parameters: + name: test commit_message_pattern + negate: true + operator: starts_with + pattern: skip* + + - type: commit_author_email_pattern + parameters: + name: test commit_author_email_pattern + negate: false + operator: regex + pattern: "^.*@example.com$" + + - type: committer_email_pattern + parameters: + name: test committer_email_pattern + negate: false + operator: regex + pattern: "^.*@example.com$" + + - type: branch_name_pattern + parameters: + name: test branch_name_pattern + negate: false + operator: regex + pattern: ".*\\\\/.*" + +- name: Prevent merges when new SONAR alerts are introduced + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + exclude: [] + bypass_actors: + - actor_type: OrganizationAdmin + bypass_mode: always + rules: + - type: code_scanning + parameters: + code_scanning_tools: + - tool: Sonar + alerts_threshold: none + security_alerts_threshold: medium_or_higher +` + +const REPO_DEMO_SERVICE1_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service1 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: AD-GRP-PAYMENTS-PLATFORM-OWNERS + permission: admin + - name: awesometeam-a-approvers + permission: push + - name: expert-services-developers + permission: push + +branches: + - name: main + protection: + required_status_checks: + strict: true + contexts: [] + required_pull_request_reviews: + required_approving_review_count: 2 + dismiss_stale_reviews: false + require_code_owner_reviews: true + require_last_push_approval: false + bypass_pull_request_allowances: + apps: [] + users: [] + teams: [] + dismissal_restrictions: + users: [] + teams: [] + enforce_admins: true + restrictions: + apps: [] + users: [] + teams: [] + + - name: develop + protection: + required_status_checks: + strict: true + contexts: [] + required_pull_request_reviews: + required_approving_review_count: 1 + dismiss_stale_reviews: false + require_code_owner_reviews: true + require_last_push_approval: false + bypass_pull_request_allowances: + apps: [] + users: [] + teams: [] + dismissal_restrictions: + users: [] + teams: [] + enforce_admins: true + restrictions: + apps: [] + users: [] + teams: [] +` + +const SUBORG_EXPERT_SERVICES_YML = `suborgteams: + - expert-services-developers + +rulesets: + - name: Protect release and production branches + target: branch + enforcement: active + conditions: + ref_name: + include: + - refs/heads/release/* + - refs/heads/production + exclude: [] + bypass_actors: + - actor_type: OrganizationAdmin + bypass_mode: always + rules: + - type: creation + - type: pull_request + parameters: + required_approving_review_count: 1 + dismiss_stale_reviews_on_push: false + require_code_owner_review: false + require_last_push_approval: false + required_review_thread_resolution: false + allowed_merge_methods: + - merge + - squash + - rebase + required_reviewers: + - minimum_approvals: 1 + file_patterns: + - "*.js" + reviewer: + id: 11721733 + type: Team +` + +const REPO_DEMO_SERVICE1_ARCHIVED_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service1 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: true +` + +const REPO_DEMO_SERVICE2_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service2 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: expert-services-developers + permission: push +` + +const SETTINGS_YML_ORG = `# Org-level safe-settings configuration + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +// ─── Test Phases ───────────────────────────────────────────────────────────── + +async function setup () { + logPhase('Phase 0: Setup') + + log('Cleaning up test repos...') + for (const repo of TEST_REPOS) { await deleteRepo(ORG, repo) } + + log('Initializing admin repo with empty settings...') + const defaultBranch = await getDefaultBranch() + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, '# empty\n', defaultBranch, 'Initialize empty settings.yml for smoke test') + + log('Cleaning up repos/ and suborgs/ directories...') + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos`) + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs`) + + startSafeSettings() + log('Waiting for safe-settings to initialize...') + await sleep(15000) + log('Setup complete') +} + +async function phase1CreateRepo () { + logPhase('Phase 1: Create test repo via test.yml') + const branch = 'smoke-test-phase1' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + log('Created branch: ' + branch) + + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_TEST_YML, branch, 'Add test repo config') + log('Added test.yml to branch') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add test repo', branch, defaultBranch) + + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr.number) + await sleep(WEBHOOK_SETTLE_MS) + + // Validate repo + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'test' })).data } catch { return null } + }, { desc: 'repo test to be created' }) + + assert(repo !== null, 'Repo "test" was created') + if (repo) { + assert(repo.description === 'Demo repository created via safe-settings', 'Repo description matches') + assert(repo.private === true, 'Repo is private') + assert(repo.has_issues === true, 'has_issues enabled') + assert(repo.has_projects === false, 'has_projects disabled') + assert(repo.has_wiki === false, 'has_wiki disabled') + assert(repo.delete_branch_on_merge === true, 'delete_branch_on_merge is true') + assert(repo.allow_squash_merge === true, 'allow_squash_merge is true') + assert(repo.allow_merge_commit === false, 'allow_merge_commit is false') + assert(repo.allow_rebase_merge === true, 'allow_rebase_merge is true') + } + + // Validate team (poll — safe-settings may still be processing) + const esTeam = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'test' }) + return teams.find(t => t.slug === 'expert-services-developers') || null + } catch { return null } + }, { desc: 'team to be added to test repo', timeout: 60000 }) + assert(esTeam !== null, 'Team expert-services-developers added') + if (esTeam) assert(esTeam.permission === 'push', `Team has push permission (got: ${esTeam.permission})`) + + // Validate custom properties (poll) + const propsOk = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + const propList = Array.isArray(props) ? props : [] + const ownership = propList.find(p => p.property_name === 'ent-ownership') + const supervisory = propList.find(p => p.property_name === 'ent-supervisory-org') + return (ownership && ownership.value === 'expert-services' && supervisory && supervisory.value === 'expert-services') || null + } catch { return null } + }, { desc: 'custom properties to be set', timeout: 60000 }) + assert(propsOk, 'Custom properties ent-ownership and ent-supervisory-org set') + + // Validate rulesets (poll) + const rulesetsOk = await poll(async () => { + try { + const { data: rulesets } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'test' }) + const synk = rulesets.find(r => r.name === 'synk') + const sonar = rulesets.find(r => r.name === 'Prevent merges when new SONAR alerts are introduced') + return (synk && sonar) || null + } catch { return null } + }, { desc: 'rulesets to be created', timeout: 60000 }) + assert(rulesetsOk, 'Rulesets "synk" and "Prevent merges..." created') + + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase2DriftTeam () { + logPhase('Phase 2: Drift remediation - Team removal') + + // Use gh CLI with user PAT so the event sender is a Human, not Bot + log('Removing expert-services-developers from test repo (as user)...') + if (!GH_TOKEN) throw new Error('GH_TOKEN env var is required for drift tests (set to a fine-grained PAT)') + try { + execSync(`gh api /orgs/${ORG}/teams/expert-services-developers/repos/${ORG}/test --method DELETE`, { + encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] + }) + } catch (e) { logFail(`Could not remove team: ${e.message}`); return } + + log('Waiting for safe-settings to remediate...') + await sleep(WEBHOOK_SETTLE_MS) + + const team = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'test' }) + return teams.find(t => t.slug === 'expert-services-developers') || null + } catch { return null } + }, { desc: 'team to be re-added', timeout: 60000 }) + + assert(team !== null, 'Team re-added after drift') +} + +async function phase3DriftRuleset () { + logPhase('Phase 3: Drift remediation - Rogue ruleset') + + // Use gh CLI with user PAT so the event sender is a Human, not Bot + log('Creating rogue ruleset on test repo (as user)...') + const body = JSON.stringify({ + name: 'rogue-ruleset', target: 'branch', enforcement: 'active', + conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } }, + rules: [{ type: 'deletion' }] + }) + try { + execSync(`gh api /repos/${ORG}/test/rulesets --method POST --input -`, { + encoding: 'utf8', input: body, stdio: ['pipe', 'pipe', 'pipe'] + }) + } catch (e) { logFail(`Could not create rogue ruleset: ${e.message}`); return } + + log('Waiting for safe-settings to remove rogue ruleset...') + await sleep(WEBHOOK_SETTLE_MS) + + const removed = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'test' }) + return !rs.find(r => r.name === 'rogue-ruleset') + } catch { return false } + }, { desc: 'rogue ruleset to be removed', timeout: 90000 }) + + assert(removed, 'Rogue ruleset removed by safe-settings') +} + +async function phase4DemoRepo1 () { + logPhase('Phase 4: Create demo-repo-service1') + const branch = 'smoke-test-phase4' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service1.yml`, REPO_DEMO_SERVICE1_YML, branch, 'Add demo-repo-service1 config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add demo-repo-service1', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr.number) + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'demo-repo-service1' })).data } catch { return null } + }, { desc: 'demo-repo-service1 to be created' }) + + assert(repo !== null, 'Repo "demo-repo-service1" created') + if (repo) { + assert(repo.description === 'Repository 2 sample', 'Description matches') + assert(repo.private === true, 'Repo is private') + assert(repo.archived === false, 'Repo is not archived') + } + + const teamsOk = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service1' }) + const t1 = teams.find(t => t.slug === 'ad-grp-payments-platform-owners') + const t2 = teams.find(t => t.slug === 'awesometeam-a-approvers') + const t3 = teams.find(t => t.slug === 'expert-services-developers') + return (t1 && t2 && t3) ? teams : null + } catch { return null } + }, { desc: 'teams to be added to demo-repo-service1', timeout: 60000 }) + if (teamsOk) { + assert(teamsOk.find(t => t.slug === 'ad-grp-payments-platform-owners') !== undefined, 'Team AD-GRP-PAYMENTS-PLATFORM-OWNERS added') + assert(teamsOk.find(t => t.slug === 'awesometeam-a-approvers') !== undefined, 'Team awesometeam-a-approvers added') + assert(teamsOk.find(t => t.slug === 'expert-services-developers') !== undefined, 'Team expert-services-developers added') + } else { logFail('Teams not added to demo-repo-service1 in time') } + + const topicsOk = await poll(async () => { + try { + const { data: topics } = await octokit.rest.repos.getAllTopics({ owner: ORG, repo: 'demo-repo-service1' }) + return (topics.names.includes('topic1') && topics.names.includes('topic2')) ? topics : null + } catch { return null } + }, { desc: 'topics to be set on demo-repo-service1', timeout: 120000 }) + assert(topicsOk, 'Topics topic1 and topic2 set') + + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase5Suborg () { + logPhase('Phase 5: Create suborg config') + const branch = 'smoke-test-phase5' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/expert-services.yml`, SUBORG_EXPERT_SERVICES_YML, branch, 'Add expert-services suborg config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add expert-services suborg', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr.number) + await sleep(WEBHOOK_SETTLE_MS) + + log('Checking suborg ruleset on demo-repo-service1...') + const ruleset = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'demo-repo-service1' }) + return rs.find(r => r.name === 'Protect release and production branches') || null + } catch { return null } + }, { desc: 'suborg ruleset on demo-repo-service1', timeout: 60000 }) + + assert(ruleset !== null, 'Suborg ruleset applied to demo-repo-service1') + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase6Archive () { + logPhase('Phase 6: Archive demo-repo-service1') + const branch = 'smoke-test-phase6' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service1.yml`, REPO_DEMO_SERVICE1_ARCHIVED_YML, branch, 'Archive demo-repo-service1') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: archive demo-repo-service1', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr.number) + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { + const { data } = await octokit.rest.repos.get({ owner: ORG, repo: 'demo-repo-service1' }) + return data.archived ? data : null + } catch { return null } + }, { desc: 'demo-repo-service1 to be archived' }) + + assert(repo !== null && repo.archived === true, 'Repo demo-repo-service1 is archived') + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase7DemoRepo2 () { + logPhase('Phase 7: Create demo-repo-service2') + const branch = 'smoke-test-phase7' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service2.yml`, REPO_DEMO_SERVICE2_YML, branch, 'Add demo-repo-service2 config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add demo-repo-service2', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr.number) + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'demo-repo-service2' })).data } catch { return null } + }, { desc: 'demo-repo-service2 to be created' }) + + assert(repo !== null, 'Repo "demo-repo-service2" created') + if (repo) { + assert(repo.archived === false, 'Repo is not archived') + assert(repo.private === true, 'Repo is private') + } + + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service2' }) + assert(teams.find(t => t.slug === 'expert-services-developers') !== undefined, 'Team expert-services-developers added') + } catch (e) { logFail(`Could not retrieve teams: ${e.message}`) } + + log('Checking suborg ruleset on demo-repo-service2...') + const ruleset = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'demo-repo-service2' }) + return rs.find(r => r.name === 'Protect release and production branches') || null + } catch { return null } + }, { desc: 'suborg ruleset on demo-repo-service2', timeout: 60000 }) + + assert(ruleset !== null, 'Suborg ruleset applied to demo-repo-service2') + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase8OrgSettings () { + logPhase('Phase 8: Org-level settings') + const branch = 'smoke-test-phase8' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_ORG, branch, 'Add org-level settings') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: org-level settings', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr.number) + await sleep(WEBHOOK_SETTLE_MS) + + log('Checking custom repository roles...') + const role = await poll(async () => { + try { + const { data } = await octokit.request('GET /orgs/{org}/custom-repository-roles', { org: ORG }) + return (data.custom_roles || []).find(r => r.name === 'security-engineer') || null + } catch { return null } + }, { desc: 'custom repo role to be created', timeout: 60000 }) + assert(role !== null, 'Custom repository role "security-engineer" created') + + log('Checking org rulesets...') + const orgRuleset = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /orgs/{org}/rulesets', { org: ORG }) + return rs.find(r => r.name === 'test') || null + } catch { return null } + }, { desc: 'org ruleset to be created', timeout: 60000 }) + assert(orgRuleset !== null, 'Org ruleset "test" created') + + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function teardown () { + logPhase('Phase 9: Teardown') + + stopSafeSettings() + + log('Deleting test repos...') + try { await octokit.rest.repos.update({ owner: ORG, repo: 'demo-repo-service1', archived: false }) } catch { /* ok */ } + for (const repo of TEST_REPOS) { await deleteRepo(ORG, repo) } + + log('Deleting test teams...') + for (const team of TEST_TEAMS) { await deleteTeam(ORG, team.toLowerCase()) } + + log('Deleting custom repository role...') + try { + const { data } = await octokit.request('GET /orgs/{org}/custom-repository-roles', { org: ORG }) + const secRole = (data.custom_roles || []).find(r => r.name === 'security-engineer') + if (secRole) await octokit.request('DELETE /orgs/{org}/custom-repository-roles/{role_id}', { org: ORG, role_id: secRole.id }) + } catch { /* ok */ } + + log('Deleting org rulesets...') + try { + const { data: rs } = await octokit.request('GET /orgs/{org}/rulesets', { org: ORG }) + const testRs = rs.find(r => r.name === 'test') + if (testRs) await octokit.request('DELETE /orgs/{org}/rulesets/{ruleset_id}', { org: ORG, ruleset_id: testRs.id }) + } catch { /* ok */ } + + log('Resetting admin repo settings...') + const defaultBranch = await getDefaultBranch() + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, '# empty\n', defaultBranch, 'Reset settings.yml after smoke test') + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos`) + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs`) + + log('Teardown complete') +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +async function main () { + const { App } = await import('octokit') + const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY }) + + // Find installation for our org + let installationId + for await (const { installation } of app.eachInstallation.iterator()) { + if (installation.account && installation.account.login.toLowerCase() === ORG.toLowerCase()) { + installationId = installation.id + break + } + } + if (!installationId) throw new Error(`No installation found for org ${ORG}`) + + octokit = await app.getInstallationOctokit(installationId) + log('Authenticated as GitHub App installation') + + console.log(` +\x1b[36m╔══════════════════════════════════════╗ +║ Safe-Settings Smoke Test ║ +║ Org: ${ORG.padEnd(28)}║ +║ Admin Repo: ${ADMIN_REPO.padEnd(22)}║ +╚══════════════════════════════════════╝\x1b[0m +`) + + try { + await setup() + await phase1CreateRepo() + await phase2DriftTeam() + await phase3DriftRuleset() + await phase4DemoRepo1() + await phase5Suborg() + await phase6Archive() + await phase7DemoRepo2() + await phase8OrgSettings() + } catch (err) { + console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`) + console.error(err.stack) + } finally { + await teardown() + } + + console.log(` +\x1b[36m╔══════════════════════════════════════╗ +║ Results ║ +╚══════════════════════════════════════╝\x1b[0m + \x1b[32mPassed: ${passCount}\x1b[0m + \x1b[31mFailed: ${failCount}\x1b[0m +`) + + if (failures.length > 0) { + console.log('\x1b[31mFailures:\x1b[0m') + failures.forEach((f, i) => console.log(` ${i + 1}. ${f}`)) + console.log() + } + + process.exit(failCount > 0 ? 1 : 0) +} + +main().catch(err => { + console.error(err) + stopSafeSettings() + process.exit(1) +}) diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index 39aac216d..23d8ef84d 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -458,4 +458,185 @@ repository: ); }); }); + + describe('getAllMatchingSubOrgSources', () => { + it('returns an empty set when subOrgConfigs is undefined', () => { + const settings = createSettings({}) + settings.subOrgConfigs = undefined + const result = settings.getAllMatchingSubOrgSources('any-repo') + expect(result).toBeInstanceOf(Set) + expect(result.size).toBe(0) + }) + + it('returns an empty set when no suborg matches', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + 'frontend-*': { source: '.github/suborgs/frontend.yml' } + } + const result = settings.getAllMatchingSubOrgSources('backend-repo') + expect(result.size).toBe(0) + }) + + it('returns a single-entry set when one suborg glob matches', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + 'frontend-*': { source: '.github/suborgs/frontend.yml' }, + 'backend-*': { source: '.github/suborgs/backend.yml' } + } + const result = settings.getAllMatchingSubOrgSources('frontend-app') + expect(result.size).toBe(1) + expect(result.has('.github/suborgs/frontend.yml')).toBe(true) + }) + + it('does not alter getSubOrgConfig single-match behavior', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + 'frontend-*': { source: '.github/suborgs/frontend.yml', tag: 'A' } + } + const before = settings.getSubOrgConfig('frontend-app') + settings.getAllMatchingSubOrgSources('frontend-app') + const after = settings.getSubOrgConfig('frontend-app') + expect(after).toBe(before) + expect(after.tag).toBe('A') + }) + }) + + describe('shouldConsiderReevaluation', () => { + let settings + const repo = { owner: 'o', repo: 'foo' } + beforeEach(() => { + settings = createSettings({}) + settings.repoConfigs = {} + }) + + describe('with changeSignals (preferred path)', () => { + it('returns true when teams plugin reported changes', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { teamsChanged: true })).toBe(true) + }) + + it('returns true when custom_properties plugin reported changes', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { propertiesChanged: true })).toBe(true) + }) + + it('returns true on repository rename', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { renamed: true })).toBe(true) + }) + + it('returns true on repository create', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { created: true })).toBe(true) + }) + + it('returns false when all change signals are false (steady state)', () => { + // Pre-existing team that is already on the repo -> diffable reports no + // changes -> we must NOT trigger a re-eval reload. + settings.repoConfigs = { 'foo.yml': { teams: [{ name: 'core' }] } } + const signals = { teamsChanged: false, propertiesChanged: false, renamed: false, created: false } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' }, signals)).toBe(false) + }) + }) + + describe('without changeSignals (fallback)', () => { + it('returns false when there is no repo-yml entry', () => { + expect(settings.shouldConsiderReevaluation(repo, null)).toBe(false) + expect(settings.shouldConsiderReevaluation(repo, undefined)).toBe(false) + }) + + it('returns false when repo-yml has no teams/properties and no rename', () => { + settings.repoConfigs = { 'foo.yml': { repository: { name: 'foo' } } } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' })).toBe(false) + }) + + it('returns true when repo-yml has teams', () => { + settings.repoConfigs = { 'foo.yml': { teams: [{ name: 'core' }] } } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' })).toBe(true) + }) + + it('returns true when repo-yml has custom_properties', () => { + settings.repoConfigs = { 'foo.yaml': { custom_properties: [{ name: 'EDP', value: 'true' }] } } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' })).toBe(true) + }) + + it('returns true on rename via repo.oldname', () => { + expect(settings.shouldConsiderReevaluation({ owner: 'o', repo: 'new', oldname: 'old' }, null)).toBe(true) + }) + + it('returns true on rename via repoConfig.oldname', () => { + expect(settings.shouldConsiderReevaluation(repo, { name: 'new', oldname: 'old' })).toBe(true) + }) + }) + }) + + describe('maybeReevaluateSuborg', () => { + it('is a no-op when reevaluateOnChange is false', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = false + settings.repoConfigs = { 'r.yml': { teams: [{ name: 'core' }] } } + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r' }, { name: 'r' }, new Set()) + expect(reloadSpy).not.toHaveBeenCalled() + }) + + it('is a no-op when repo-yml has no triggers (teams/properties/rename)', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.repoConfigs = { 'r.yml': { repository: { name: 'r' } } } + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r' }, { name: 'r' }, new Set()) + expect(reloadSpy).not.toHaveBeenCalled() + }) + + it('is a no-op when changeSignals report no plugin changes (preexisting team)', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + // repo-yml has teams, but plugin reported no change (team already on repo) + settings.repoConfigs = { 'r.yml': { teams: [{ name: 'core' }] } } + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + const signals = { teamsChanged: false, propertiesChanged: false, renamed: false, created: false } + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r' }, { name: 'r' }, new Set(), signals) + expect(reloadSpy).not.toHaveBeenCalled() + expect(updateSpy).not.toHaveBeenCalled() + }) + + it('stops when the matched suborg source set is stable (no new sources)', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.subOrgConfigs = { 'r*': { source: '.github/suborgs/x.yml' } } + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + // pre = post = {x.yml} -> stable, no recursion + const pre = new Set(['.github/suborgs/x.yml']) + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, pre, { teamsChanged: true }) + expect(updateSpy).not.toHaveBeenCalled() + }) + + it('recurses once when a new suborg source appears, then stops at depth cap', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + // After reload, a new suborg matches r1 + settings.subOrgConfigs = { 'r*': { source: '.github/suborgs/new.yml' } } + settings.repoConfigs = { 'r1.yml': { teams: [{ name: 't' }] } } + jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + jest.spyOn(settings, 'getRepoConfigs').mockResolvedValue({ 'r1.yml': { teams: [{ name: 't' }] } }) + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + const pre = new Set() // pre-apply: nothing matched + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, pre, { teamsChanged: true }) + expect(updateSpy).toHaveBeenCalledTimes(1) + expect(settings.reevaluationDepth.get('r1')).toBe(1) + }) + + it('respects MAX_REEVALUATION_DEPTH and logs a warning', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.reevaluationDepth.set('r1', 1) // already at cap + settings.repoConfigs = { 'r1.yml': { teams: [{ name: 't' }] } } + stubContext.log.warn = jest.fn() + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, new Set(), { teamsChanged: true }) + expect(reloadSpy).not.toHaveBeenCalled() + expect(updateSpy).not.toHaveBeenCalled() + expect(stubContext.log.warn).toHaveBeenCalledWith(expect.stringContaining('max depth')) + }) + }) }) // Settings Tests From baaa9d5c2c0c0ec87e658d910b5cc61f46569919 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Tue, 19 May 2026 14:19:41 -0400 Subject: [PATCH 17/22] Add external group linking functionality for teams and update smoke tests --- docs/github-settings/4. teams.md | 13 ++ lib/plugins/teams.js | 137 ++++++++++++++++++++ smoke-test.js | 127 ++++++++++++++++++- test/unit/lib/plugins/teams.test.js | 186 ++++++++++++++++++++++++++++ 4 files changed, 462 insertions(+), 1 deletion(-) diff --git a/docs/github-settings/4. teams.md b/docs/github-settings/4. teams.md index 496b30a32..56f1a4e86 100644 --- a/docs/github-settings/4. teams.md +++ b/docs/github-settings/4. teams.md @@ -48,5 +48,18 @@ teams: permission: maintain ``` + + +

    external_groupstring

    +

    Optional. The display name of an external IdP group (as listed under your organization's external groups) to link to the team. safe-settings looks up the group's id by display name via GET /orgs/{org}/external-groups and links the team via PATCH /orgs/{org}/teams/{team_slug}/external-groups. The link is reconciled on every sync and is idempotent (it skips the PATCH when the team is already linked to the same group). The external-groups list is fetched at most once per org per sync, only when at least one team entry uses this property. If the named group does not exist for the org, an error is logged and the team-repo association still applies.

    + + +```yaml +teams: + - name: expert-services-developers + permission: push + external_group: "Engineering - Expert Services" +``` + diff --git a/lib/plugins/teams.js b/lib/plugins/teams.js index 4d7f79273..0d655363a 100644 --- a/lib/plugins/teams.js +++ b/lib/plugins/teams.js @@ -2,7 +2,30 @@ const Diffable = require('./diffable') const NopCommand = require('../nopcommand') const teamRepoEndpoint = '/orgs/:owner/teams/:team_slug/repos/:owner/:repo' +const listExternalGroupsEndpoint = 'GET /orgs/{org}/external-groups' +const teamExternalGroupsEndpoint = '/orgs/{org}/teams/{team_slug}/external-groups' + module.exports = class Teams extends Diffable { + // Override Diffable.sync to also reconcile the optional `external_group` + // link on each team entry after the normal team-repo permission sync. + // This runs regardless of whether the team-repo association was added, + // updated, or already in sync -- so updating only `external_group` on a + // team that already has correct repo permissions still triggers the link. + async sync () { + const res = await super.sync() + if (!this.entries) return res + + const filtered = this.filterEntries() + const entriesWithExternalGroup = filtered.filter(e => e && e.external_group) + if (entriesWithExternalGroup.length === 0) return res + + const nopCommands = Array.isArray(res) ? res : [] + for (const attrs of entriesWithExternalGroup) { + await this.syncExternalGroup(attrs, this.nop ? nopCommands : undefined) + } + return this.nop ? nopCommands : res + } + async find () { this.log.debug(`Finding teams for ${this.repo.owner}/${this.repo.repo}`) return this.github.paginate(this.github.repos.listTeams, this.repo).then(res => { @@ -138,4 +161,118 @@ module.exports = class Teams extends Diffable { permission: attrs.permission } } + + // Resolve the org's external-group display name -> group_id. Lazily builds + // a per-org Map (name -> id) the first time it's needed within a sync, and + // caches it on the shared `github` client so multiple repos / teams in the + // same sync only paginate `GET /orgs/{org}/external-groups` once per org. + // Returns null when the named group does not exist for the org (logs an + // error so the user can correct their yaml). + async resolveExternalGroupId (groupName) { + if (!this.github.__externalGroupsCache) { + this.github.__externalGroupsCache = new Map() + } + const cache = this.github.__externalGroupsCache + const org = this.repo.owner + if (!cache.has(org)) { + try { + // The external-groups endpoint returns { total_count, groups: [...] } + // and is not in Octokit's known-pagination list, so we must pass a + // map function that extracts the `groups` array from each page; + // otherwise paginate() yields the raw response objects and we'd + // silently fail to find any names. + const groups = await this.github.paginate( + listExternalGroupsEndpoint, + { org, per_page: 100 }, + (response) => (response && response.data && response.data.groups) || [] + ) + const byName = new Map() + for (const g of groups) { + if (g && g.group_name) byName.set(g.group_name, g.group_id) + } + this.log.debug(`Loaded ${byName.size} external group(s) for org ${org}: ${JSON.stringify(Array.from(byName.keys()))}`) + cache.set(org, byName) + } catch (e) { + this.logError(`Error listing external groups for org ${org}: ${e}`) + // Cache an empty map so we don't retry-storm the API within this sync. + cache.set(org, new Map()) + } + } + const id = cache.get(org).get(groupName) + if (id === undefined) { + return null + } + return id + } + + // Link a team to an external IdP group identified by display name. Only + // acts when the team entry carries an `external_group` property. Idempotent: + // checks the current link first and skips the PATCH if already linked to + // the same group_id. Sets `this.hasChanges = true` only when a PATCH + // actually fires, so the suborg re-evaluation logic in lib/settings.js sees + // a real change signal. + async syncExternalGroup (attrs, nopCommands) { + const groupName = attrs && attrs.external_group + if (!groupName) return + + const groupId = await this.resolveExternalGroupId(groupName) + if (groupId === null) { + const msg = `External group '${groupName}' not found for org ${this.repo.owner} (team '${attrs.name}').` + // logError: feeds the synchronous-run end-of-run errors summary. + this.logError(msg) + // For PR dry-run / nop mode, also surface the failure in the check_run + // output -- which is built from results entries with type === 'ERROR'. + if (this.nop && Array.isArray(nopCommands)) { + nopCommands.push(new NopCommand(this.constructor.name, this.repo, null, msg, 'ERROR')) + } + return + } + + const linkParams = { + org: this.repo.owner, + team_slug: attrs.name, + group_id: groupId + } + + if (this.nop) { + if (Array.isArray(nopCommands)) { + nopCommands.push(new NopCommand( + this.constructor.name, + this.repo, + this.github.request.endpoint(`PATCH ${teamExternalGroupsEndpoint}`, linkParams), + `Link team ${attrs.name} to external group '${groupName}'` + )) + } + return + } + + // Idempotency: skip the PATCH if the team is already linked to this group. + try { + const current = await this.github.request(`GET ${teamExternalGroupsEndpoint}`, { + org: this.repo.owner, + team_slug: attrs.name + }) + const currentGroups = (current && current.data && current.data.groups) || [] + if (currentGroups.some(g => g.group_id === groupId)) { + this.log.debug(`Team ${attrs.name} is already linked to external group '${groupName}' (id=${groupId}); skipping.`) + return + } + } catch (e) { + // 404 here means no current link; fall through to PATCH. Any other + // error is non-fatal -- the PATCH itself is idempotent on the server. + if (e.status !== 404) { + this.logError(`Error fetching current external group for team ${attrs.name}: ${e}`) + } + } + + try { + await this.github.request(`PATCH ${teamExternalGroupsEndpoint}`, linkParams) + this.log.debug(`Linked team ${attrs.name} to external group '${groupName}' (id=${groupId}).`) + // Surface this change so suborg re-evaluation (in lib/settings.js) and + // other consumers see that the team plugin made a real change. + this.hasChanges = true + } catch (e) { + this.logError(`Error linking team ${attrs.name} to external group '${groupName}' (id=${groupId}): ${e}`) + } + } } diff --git a/smoke-test.js b/smoke-test.js index dcb4df8af..d881972da 100644 --- a/smoke-test.js +++ b/smoke-test.js @@ -66,7 +66,7 @@ const APP_ID = process.env.APP_ID const PRIVATE_KEY = (process.env.PRIVATE_KEY || '').replace(/\\n/g, '\n') const TEST_REPOS = ['test', 'demo-repo-service1', 'demo-repo-service2'] -const TEST_TEAMS = ['AD-GRP-PAYMENTS-PLATFORM-OWNERS', 'awesometeam-a-approvers'] +const TEST_TEAMS = ['AD-GRP-PAYMENTS-PLATFORM-OWNERS', 'awesometeam-a-approvers', 'jefeish-edj-test'] const POLL_INTERVAL_MS = 5000 const MAX_POLL_MS = 120000 @@ -453,6 +453,49 @@ teams: permission: push ` +const REPO_DEMO_SERVICE2_EXTERNAL_GROUP_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service2 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: expert-services-developers + permission: push + - name: jefeish-edj-test + permission: push + external_group: jefeish-edj-test +` + +const REPO_DEMO_SERVICE2_NO_EXTERNAL_GROUP_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service2 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: expert-services-developers + permission: push +` + const SETTINGS_YML_ORG = `# Org-level safe-settings configuration rulesets: @@ -801,6 +844,87 @@ async function phase7DemoRepo2 () { await deleteBranch(ORG, ADMIN_REPO, branch) } +async function phase7bExternalGroupTeam () { + logPhase('Phase 7b: Add team with external_group to demo-repo-service2') + const branch = 'smoke-test-phase7b' + const defaultBranch = await getDefaultBranch() + + // ── Step 1: Add the team with external_group mapping ── + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service2.yml`, REPO_DEMO_SERVICE2_EXTERNAL_GROUP_YML, branch, 'Add team with external_group to demo-repo-service2') + + const pr1 = await createPR(ORG, ADMIN_REPO, 'Smoke test: add external_group team to demo-repo-service2', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun1 = await waitForCheckRun(ORG, ADMIN_REPO, pr1.head.sha) + assert(checkRun1 !== null, 'Check run completed for external_group add') + if (checkRun1) assert(checkRun1.conclusion === 'success', `Check run conclusion is success (got: ${checkRun1.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr1.number) + await sleep(WEBHOOK_SETTLE_MS) + + // Verify team is created and assigned to the repo + log('Checking team jefeish-edj-test is added to demo-repo-service2...') + const team = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service2' }) + return teams.find(t => t.slug === 'jefeish-edj-test') || null + } catch { return null } + }, { desc: 'team jefeish-edj-test to be added to demo-repo-service2' }) + + assert(team !== null, 'Team jefeish-edj-test added to demo-repo-service2') + + // Verify the external group (IdP) mapping exists on the team + log('Checking external group mapping on team jefeish-edj-test...') + const externalGroup = await poll(async () => { + try { + const { data } = await octokit.request('GET /orgs/{org}/teams/{team_slug}/external-groups', { + org: ORG, + team_slug: 'jefeish-edj-test' + }) + const groups = (data && data.groups) || [] + return groups.find(g => g.group_name === 'jefeish-edj-test') || null + } catch { return null } + }, { desc: 'external group mapping on jefeish-edj-test', timeout: 60000 }) + + assert(externalGroup !== null, 'External group jefeish-edj-test mapped to team jefeish-edj-test') + + await deleteBranch(ORG, ADMIN_REPO, branch) + + // ── Step 2: Remove the team from the YAML and verify removal ── + log('Removing team jefeish-edj-test from demo-repo-service2 config...') + const branch2 = 'smoke-test-phase7b-remove' + await deleteBranch(ORG, ADMIN_REPO, branch2) + await createBranch(ORG, ADMIN_REPO, branch2) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service2.yml`, REPO_DEMO_SERVICE2_NO_EXTERNAL_GROUP_YML, branch2, 'Remove external_group team from demo-repo-service2') + + const pr2 = await createPR(ORG, ADMIN_REPO, 'Smoke test: remove external_group team from demo-repo-service2', branch2, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun2 = await waitForCheckRun(ORG, ADMIN_REPO, pr2.head.sha) + assert(checkRun2 !== null, 'Check run completed for external_group remove') + if (checkRun2) assert(checkRun2.conclusion === 'success', `Check run conclusion is success (got: ${checkRun2.conclusion})`) + + log('Merging PR...') + await mergePR(ORG, ADMIN_REPO, pr2.number) + await sleep(WEBHOOK_SETTLE_MS) + + // Verify team is removed from the repo + log('Checking team jefeish-edj-test is removed from demo-repo-service2...') + const removedTeam = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service2' }) + return teams.find(t => t.slug === 'jefeish-edj-test') ? false : true + } catch { return null } + }, { desc: 'team jefeish-edj-test to be removed from demo-repo-service2' }) + + assert(removedTeam === true, 'Team jefeish-edj-test removed from demo-repo-service2') + + await deleteBranch(ORG, ADMIN_REPO, branch2) +} + async function phase8OrgSettings () { logPhase('Phase 8: Org-level settings') const branch = 'smoke-test-phase8' @@ -913,6 +1037,7 @@ async function main () { await phase5Suborg() await phase6Archive() await phase7DemoRepo2() + await phase7bExternalGroupTeam() await phase8OrgSettings() } catch (err) { console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`) diff --git a/test/unit/lib/plugins/teams.test.js b/test/unit/lib/plugins/teams.test.js index 60ef23dbc..a521e0ce3 100644 --- a/test/unit/lib/plugins/teams.test.js +++ b/test/unit/lib/plugins/teams.test.js @@ -96,4 +96,190 @@ describe('Teams', () => { ) } }) + + describe('external_group linking', () => { + const externalGroupName = 'Engineering - Expert Services' + const externalGroupId = 42 + + beforeEach(() => { + // request: default to no-current-link (404) so PATCH fires; override per-test as needed. + github.request = jest.fn().mockImplementation((endpoint) => { + if (typeof endpoint === 'string' && endpoint.startsWith('GET /orgs/{org}/teams/')) { + const err = new Error('not found') + err.status = 404 + return Promise.reject(err) + } + return Promise.resolve({ data: {} }) + }) + github.request.endpoint = jest.fn().mockReturnValue('endpoint-stub') + + // paginate: route the external-groups list call to a single page; keep + // the original implementation for other paginated endpoints. The real + // production code passes a map-function (3rd arg) that extracts the + // `groups` array from each page response -- we mimic the same response + // shape so that mapFn gets exercised. + const externalGroupsResponse = { + data: { + total_count: 2, + groups: [ + { group_id: externalGroupId, group_name: externalGroupName }, + { group_id: 99, group_name: 'Some Other Group' } + ] + } + } + github.paginate = jest.fn().mockImplementation(async (fetchOrEndpoint, params, mapFn) => { + if (fetchOrEndpoint === 'GET /orgs/{org}/external-groups') { + if (typeof mapFn === 'function') { + return mapFn(externalGroupsResponse) + } + return externalGroupsResponse.data.groups + } + if (typeof fetchOrEndpoint === 'function') { + const response = await fetchOrEndpoint() + return response.data + } + return [] + }) + }) + + it('looks up the group id by name and PATCHes the team link', async () => { + when(github.teams.getByName) + .defaultResolvedValue({}) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' }, + { name: addedTeamName, permission: 'pull', external_group: externalGroupName } + ]) + + await plugin.sync() + + expect(github.paginate).toHaveBeenCalledWith( + 'GET /orgs/{org}/external-groups', + { org, per_page: 100 }, + expect.any(Function) + ) + expect(github.request).toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + { org, team_slug: addedTeamName, group_id: externalGroupId } + ) + expect(plugin.hasChanges).toBe(true) + }) + + it('skips the PATCH when the team is already linked to the same group', async () => { + github.request = jest.fn().mockImplementation((endpoint, params) => { + if (endpoint === 'GET /orgs/{org}/teams/{team_slug}/external-groups') { + return Promise.resolve({ data: { groups: [{ group_id: externalGroupId, group_name: externalGroupName }] } }) + } + return Promise.resolve({ data: {} }) + }) + github.request.endpoint = jest.fn().mockReturnValue('endpoint-stub') + + const plugin = configure([ + { name: unchangedTeamName, permission: 'push', external_group: externalGroupName } + ]) + + await plugin.sync() + + expect(github.request).toHaveBeenCalledWith( + 'GET /orgs/{org}/teams/{team_slug}/external-groups', + { org, team_slug: unchangedTeamName } + ) + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + }) + + it('logs an error and skips when the external group name is not found', async () => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push', external_group: 'Nonexistent Group' } + ]) + + await plugin.sync() + + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + // logError pushes onto the errors array + expect(plugin.errors.some(e => /Nonexistent Group/.test(JSON.stringify(e)))).toBe(true) + }) + + it('in nop mode, emits an ERROR NopCommand when the external group is not found (so it appears in the PR check_run)', async () => { + const log = { debug: jest.fn(), error: console.error } + const errors = [] + const Teams = require('../../../../lib/plugins/teams') + const plugin = new Teams(true, github, { owner: org, repo: 'test' }, [ + { name: unchangedTeamName, permission: 'push', external_group: 'Nonexistent Group' } + ], log, errors) + + const result = await plugin.sync() + + expect(Array.isArray(result)).toBe(true) + const errorCmd = result.find(c => c && c.type === 'ERROR' && /Nonexistent Group/.test(JSON.stringify(c))) + expect(errorCmd).toBeDefined() + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + }) + + it('paginates the external-groups list only once per org across multiple syncs sharing the github client', async () => { + when(github.teams.getByName) + .defaultResolvedValue({}) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + const plugin1 = configure([ + { name: unchangedTeamName, permission: 'push', external_group: externalGroupName } + ]) + const plugin2 = configure([ + { name: addedTeamName, permission: 'pull', external_group: externalGroupName } + ]) + + await plugin1.sync() + await plugin2.sync() + + const listCalls = github.paginate.mock.calls.filter(c => c[0] === 'GET /orgs/{org}/external-groups') + expect(listCalls).toHaveLength(1) + }) + + it('does not call the external-groups list endpoint when no entry uses external_group', async () => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' } + ]) + + await plugin.sync() + + const listCalls = github.paginate.mock.calls.filter(c => c[0] === 'GET /orgs/{org}/external-groups') + expect(listCalls).toHaveLength(0) + }) + + it('in nop mode, emits a NopCommand and makes no PATCH', async () => { + const log = { debug: jest.fn(), error: console.error } + const errors = [] + const Teams = require('../../../../lib/plugins/teams') + const plugin = new Teams(true, github, { owner: org, repo: 'test' }, [ + { name: unchangedTeamName, permission: 'push', external_group: externalGroupName } + ], log, errors) + + const result = await plugin.sync() + + expect(Array.isArray(result)).toBe(true) + expect(result.some(c => /external group/.test(c.action) || /external group/.test(JSON.stringify(c)))).toBe(true) + // In nop mode no real linkage should be performed -- neither the + // idempotency GET nor the PATCH should hit the team-external-groups + // endpoint. + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + expect(github.request).not.toHaveBeenCalledWith( + 'GET /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + }) + }) }) From 6ca72a7e43d97be125194e73d857e17752ff1a75 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Sat, 23 May 2026 21:42:51 -0400 Subject: [PATCH 18/22] feat: add disable_plugins configuration to settings schema - Introduced a new "disable_plugins" property in the settings schema to allow disabling specific plugins at various configuration layers. - Each entry can be a plugin name or an object specifying the plugin and its target layer (self, children, all). - Updated smoke-test.js to include interactive mode for manual validation during test phases. - Implemented new test cases for the disable_plugins feature, covering normalization, strip map computation, and integration with updateOrg and updateRepos functions. - Added tests to ensure proper handling of valid and invalid disable_plugins configurations. --- README.md | 76 ++++ docs/README.md | 2 + .../sample-deployment-settings.yml | 11 + docs/sample-settings/settings.yml | 14 + docs/sample-settings/suborg.yml | 12 + lib/commentmessage.js | 14 + lib/settings.js | 337 +++++++++++++-- package.json | 5 +- schema/dereferenced/settings.json | 398 +++++++++++++----- schema/settings.json | 58 +++ smoke-test.js | 253 +++++++++-- test/unit/lib/settings.test.js | 365 ++++++++++++++++ 12 files changed, 1378 insertions(+), 167 deletions(-) diff --git a/README.md b/README.md index b7382de34..892f97bb8 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,82 @@ And the `checkrun` page will look like this: image

    +### Disabling plugins (`disable_plugins`) + +Any settings file (deployment-settings, org `settings.yml`, suborg, or repo) can +contain a top-level `disable_plugins` list to turn off one or more safe-settings +plugins for a given scope. Each entry is either: + +- A plugin name string (shorthand for `{ plugin: , target: all }`), or +- An object `{ plugin: , target: self | children | all }` (default `target: all`). + +Valid plugin names: `repository`, `labels`, `collaborators`, `teams`, +`milestones`, `branches`, `autolinks`, `validator`, `rulesets`, `environments`, +`custom_properties`, `custom_repository_roles`, `variables`, `archive`. + +#### Strip matrix (which source layers are removed before merge) + +| Declared at | `target: self` | `target: children` | `target: all` | +| -------------------------- | ------------------ | ------------------------- | ----------------------------- | +| deployment-settings | deployment | org + suborg + repo | deployment + org + suborg + repo | +| org `settings.yml` | org | suborg + repo | org + suborg + repo | +| suborgs/`*.yml` (matched) | suborg | repo | suborg + repo | +| repos/`*.yml` | repo | (no-op) | repo | + +When safe-settings builds the merged configuration for a repo, it strips the +disabled plugin's keys from the indicated source layers before merging. For +repo-level execution points (the `repository` and `archive` plugins) and +org-level execution points (`rulesets`, `custom_repository_roles`), a disable +that targets the corresponding layer also short-circuits the plugin run, and +the skip is recorded as an INFO `NopCommand` in NOP mode (PR check run). + +#### Cascade rules + +- **Union-only.** Strips accumulate across layers; a lower-level config can add + more strips but can never undo a strip declared above it. +- **No re-enable.** If `disable_plugins: [labels]` is set at the org layer, a + repo cannot re-enable `labels` for itself. + +#### Important limitation + +Because strips operate on **source layers**, a lower-level disable cannot +remove configuration contributed by a higher layer. For example, if `branches` +is defined at the org layer and a suborg adds +`disable_plugins: [{plugin: branches, target: all}]`, the suborg's strip +removes the `branches` key only from the suborg and repo layers — the org's +`branches` config still merges in, and the branches plugin still runs. + +To fully suppress a plugin for matched repos, declare the disable at (or above) +the layer that contributes the configuration — typically the org layer with +`target: all`, or at the deployment layer. + +#### Examples + +Org `settings.yml` — disable `custom_repository_roles` only at the org execution +point (rulesets still run): + +```yaml +disable_plugins: + - plugin: custom_repository_roles + target: self +``` + +Org `settings.yml` — disable `branches` everywhere (shorthand): + +```yaml +disable_plugins: + - branches +``` + +Suborg `suborgs/team-x.yml` — strip `labels` for matched repos (effective only +if `labels` is not also defined at the org layer): + +```yaml +disable_plugins: + - plugin: labels + target: all +``` + ### The Settings Files The settings files can be used to set the policies at the `org`, `suborg` or `repo` level. diff --git a/docs/README.md b/docs/README.md index 6d1f17436..76c9ee10f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,3 +10,5 @@ | Configure deployment environments | [Deployment Environments](github-settings/6.%20deployment-environments.md) | | Configure auto-link references | [AutoLinks](github-settings/7.%20autolinks.md) | | Configure pre-defined labels for issues and pull requests | [Labels](github-settings/8.%20labels.md) | + +For information on disabling plugins, see [Disabling plugins](../README.md#disabling-plugins-disable_plugins) in the root README. diff --git a/docs/sample-settings/sample-deployment-settings.yml b/docs/sample-settings/sample-deployment-settings.yml index 6164d4389..f3c6e8b37 100644 --- a/docs/sample-settings/sample-deployment-settings.yml +++ b/docs/sample-settings/sample-deployment-settings.yml @@ -38,3 +38,14 @@ overridevalidators: Some error script: | return true + +# disable_plugins (optional) — disable safe-settings plugins at the deployment layer. +# Each entry is either a plugin name (shorthand for target: all) or { plugin, target }. +# target is one of: self | children | all (default: all). +# Declared here, target: all strips the plugin from every level below for every repo. +# See docs/README.md ("Disabling plugins") for the full strip matrix and limitations. +# +# disable_plugins: +# - plugin: rulesets # disables rulesets everywhere +# target: all +# - milestones # shorthand → { plugin: milestones, target: all } diff --git a/docs/sample-settings/settings.yml b/docs/sample-settings/settings.yml index 7e19d3354..fadd9f664 100644 --- a/docs/sample-settings/settings.yml +++ b/docs/sample-settings/settings.yml @@ -397,3 +397,17 @@ rulesets: negate: false operator: regex pattern: ".*\/.*" + +# disable_plugins (optional) — disable safe-settings plugins at the org layer. +# Declared here: +# - target: self → strips from the org layer only (affects org-level runs: +# rulesets, custom_repository_roles). +# - target: children → strips from suborg + repo layers (per-repo runs). +# - target: all → strips from org + suborg + repo layers. +# Lower levels can never undo a strip declared at a higher level (union-only cascade). +# See docs/README.md ("Disabling plugins") for the full strip matrix. +# +# disable_plugins: +# - plugin: custom_repository_roles +# target: self +# - branches # shorthand → { plugin: branches, target: all } diff --git a/docs/sample-settings/suborg.yml b/docs/sample-settings/suborg.yml index a509847cc..42e822993 100644 --- a/docs/sample-settings/suborg.yml +++ b/docs/sample-settings/suborg.yml @@ -14,3 +14,15 @@ suborgproperties: - EDP: true # Every other property is the same as the org level settings and can be overridden here + +# disable_plugins (optional) — disable safe-settings plugins for repos matched +# by this suborg. Declared here, target values mean: +# - self → strip from the suborg layer only. +# - children → strip from the repo layer for matched repos. +# - all → strip from suborg + repo layers for matched repos. +# Note: a suborg-level disable cannot strip config defined at the org layer. +# See docs/README.md ("Disabling plugins") for details. +# +# disable_plugins: +# - plugin: labels +# target: all diff --git a/lib/commentmessage.js b/lib/commentmessage.js index b54f81bb4..66f4862c8 100644 --- a/lib/commentmessage.js +++ b/lib/commentmessage.js @@ -27,5 +27,19 @@ module.exports = `* Run on: \` <%= new Date() %> \` * <%= plugin.msg %> <% }) %> + <% }) %> +<% } %> + +### Informational messages (disabled plugins) + +<% if (!it.infos || Object.keys(it.infos).length === 0) { %> +\`None\` +<% } else { %> + <% Object.keys(it.infos).forEach(repo => { %> + <%_= repo %>: + <% it.infos[repo].forEach(msg => { %> + * ℹ️ <%= msg %> + <% }) %> + <% }) %> <% } %>` diff --git a/lib/settings.js b/lib/settings.js index d104efa5b..f1584fa97 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -6,7 +6,37 @@ const Glob = require('./glob') const NopCommand = require('./nopcommand') const MergeDeep = require('./mergeDeep') const Archive = require('./plugins/archive') +const DeploymentConfig = require('./deploymentConfig') const env = require('./env') + +// Valid `target` values for a disable_plugins entry. +const DISABLE_TARGETS = new Set(['self', 'children', 'all']) +// Valid declaration layers (where a disable_plugins entry can be authored). +const DISABLE_LEVELS = ['deployment', 'org', 'suborg', 'repo'] +// For each declared layer + target, the set of layers from which to STRIP the +// named plugin's config. See plan-v3 matrix. +const DISABLE_STRIP_MATRIX = { + deployment: { + self: ['deployment'], + children: ['org', 'suborg', 'repo'], + all: ['deployment', 'org', 'suborg', 'repo'] + }, + org: { + self: ['org'], + children: ['suborg', 'repo'], + all: ['org', 'suborg', 'repo'] + }, + suborg: { + self: ['suborg'], + children: ['repo'], + all: ['suborg', 'repo'] + }, + repo: { + self: ['repo'], + children: ['repo'], // normalized; repo has no children + all: ['repo'] + } +} const CONFIG_PATH = env.CONFIG_PATH const eta = new Eta({ views: path.join(__dirname) }) const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting @@ -203,6 +233,14 @@ class Settings { msg, plugin: this.constructor.name }) + // In NOP mode, also surface the error as an ERROR NopCommand so the NOP + // check run conclusion reflects the failure. Without this, errors caught + // by the syncAll/syncSelectedRepos top-level catch (e.g. invalid + // disable_plugins entries) would go unnoticed by PR reviewers. + if (this.nop) { + const nopcommand = new NopCommand(this.constructor.name, this.repo, null, msg, 'ERROR') + this.appendToResults([nopcommand]) + } } async handleResults () { @@ -215,43 +253,42 @@ class Settings { return } - // remove duplicate rows in this.results + // Remove duplicate rows. The key includes endpoint so that distinct + // per-operation NopCommands (individual add/update/remove from diffable + // plugins) survive alongside the overall diff-summary NopCommand. this.results = this.results.filter((thing, index, self) => { return index === self.findIndex((t) => { - return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin + return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin && t.endpoint === thing.endpoint }) }) let error = false - // Different logic const stats = { - // noOfReposProcessed: new Map(), reposProcessed: {}, changes: {}, - errors: {} + errors: {}, + // Informational entries (type === 'INFO', all-null diff fields), e.g. + // disable_plugins skip messages. Keyed by repo. + infos: {} } - /* - Result fields - res.type - res.plugin - res.repo - res.endpoint - res.body - res.action - */ this.results.forEach(res => { if (res) { stats.reposProcessed[res.repo] = true - // if (res.action.additions === null && res.action.deletions === null && res.action.modifications === null) { - // // No changes - // } else if (res.type === 'ERROR') { error = true if (!stats.errors[res.repo]) { stats.errors[res.repo] = [] } stats.errors[res.repo].push(res.action) - } else if (!(res.action?.additions === null && res.action?.deletions === null && res.action?.modifications === null)) { + } else if (res.action?.additions === null && res.action?.deletions === null && res.action?.modifications === null) { + // No diff data — informational message (e.g. disable_plugins skip). + if (res.action?.msg) { + if (!stats.infos[res.repo]) { + stats.infos[res.repo] = [] + } + stats.infos[res.repo].push(`[${res.plugin}] ${res.action.msg}`) + } + } else { if (!stats.changes[res.plugin]) { stats.changes[res.plugin] = {} } @@ -292,17 +329,20 @@ ${this.results.reduce((x, y) => { if (y.type === 'ERROR') { error = true return `${x} - ❗ ${y.action.msg} ${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)} ` - } else if (y.action.additions === null && y.action.deletions === null && y.action.modifications === null) { - return `${x}` + ❗ ${y.action.msg} ${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)} ` + } else if (y.action?.additions === null && y.action?.deletions === null && y.action?.modifications === null) { + if (!y.action?.msg) return `${x}` + return `${x} + ℹ️ ${y.action.msg} ${y.plugin} ${prettify(y.repo)} — — — ` } else { if (y.action === undefined) { return `${x}` } return `${x} - ✋ ${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)} ` + ✋ ${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)} ` } }, table)} + ` const pullRequest = payload.check_run.check_suite.pull_requests[0] @@ -337,21 +377,197 @@ ${this.results.reduce((x, y) => { this.repoConfigs = await this.getRepoConfigs(repo) } + // ──────────────────────────────────────────────────────────────────────── + // disable_plugins helpers + // ──────────────────────────────────────────────────────────────────────── + + // Returns the set of plugin names that are valid `disable_plugins` targets. + static getValidDisablePluginNames () { + return new Set([...Object.keys(Settings.PLUGINS), 'repository', 'archive']) + } + + // Normalize a raw `disable_plugins` list (mixed strings / objects) into + // [{ plugin, target, declaredAt }]. Validates plugin names and target + // values; throws on invalid entries. For declaredAt='repo', `children` + // collapses to `all` (repo has no children). + normalizeDisableEntries (rawList, declaredAt) { + if (rawList === undefined || rawList === null) return [] + if (!Array.isArray(rawList)) { + throw new Error(`disable_plugins at ${declaredAt} must be an array; got ${typeof rawList}`) + } + if (!DISABLE_LEVELS.includes(declaredAt)) { + throw new Error(`Internal: invalid declaredAt '${declaredAt}'`) + } + const validPlugins = Settings.getValidDisablePluginNames() + const normalized = [] + for (const raw of rawList) { + let plugin + let target = 'all' + if (typeof raw === 'string') { + plugin = raw + } else if (raw && typeof raw === 'object') { + plugin = raw.plugin + if (raw.target !== undefined) target = raw.target + } else { + throw new Error(`disable_plugins entry at ${declaredAt} must be a string or {plugin, target}; got ${JSON.stringify(raw)}`) + } + if (!plugin || typeof plugin !== 'string') { + throw new Error(`disable_plugins entry at ${declaredAt} is missing a valid 'plugin' name: ${JSON.stringify(raw)}`) + } + if (!validPlugins.has(plugin)) { + throw new Error(`disable_plugins at ${declaredAt}: unknown plugin '${plugin}'. Valid: ${[...validPlugins].sort().join(', ')}`) + } + if (!DISABLE_TARGETS.has(target)) { + throw new Error(`disable_plugins at ${declaredAt} for plugin '${plugin}': invalid target '${target}'. Valid: ${[...DISABLE_TARGETS].join(', ')}`) + } + if (declaredAt === 'repo' && target === 'children') { + this.log.debug(`disable_plugins: normalizing repo-level target 'children' to 'all' for plugin '${plugin}' (repo has no children)`) + target = 'all' + } + normalized.push({ plugin, target, declaredAt }) + } + return normalized + } + + // Aggregate disable_plugins entries from all four layers (deployment, org, + // suborg matching repoName, repo override for repoName) and expand them via + // the strip matrix into a Map>. If repoName is + // undefined, only deployment + org layers contribute (used by updateOrg). + computeStripMap (repoName) { + const stripMap = new Map() + for (const level of DISABLE_LEVELS) stripMap.set(level, new Set()) + + const layers = [] + // Deployment layer (singleton) + const deploymentRaw = (DeploymentConfig && DeploymentConfig.config && DeploymentConfig.config.disable_plugins) || null + if (deploymentRaw) layers.push(['deployment', deploymentRaw]) + // Org layer + if (this.config && this.config.disable_plugins) { + layers.push(['org', this.config.disable_plugins]) + } + if (repoName !== undefined && repoName !== null) { + const suborg = this.getSubOrgConfig(repoName) + if (suborg && suborg.disable_plugins) { + layers.push(['suborg', suborg.disable_plugins]) + } + const repoOverride = this.getRepoOverrideConfig(repoName) + if (repoOverride && repoOverride.disable_plugins) { + layers.push(['repo', repoOverride.disable_plugins]) + } + } + + for (const [declaredAt, rawList] of layers) { + const entries = this.normalizeDisableEntries(rawList, declaredAt) + for (const { plugin, target } of entries) { + const affected = DISABLE_STRIP_MATRIX[declaredAt][target] || [] + for (const lvl of affected) { + stripMap.get(lvl).add(plugin) + } + } + } + this.log.debug(`disable_plugins stripMap for repo=${repoName || ''}: ${JSON.stringify([...stripMap].map(([k, v]) => [k, [...v]]))}`) + return stripMap + } + + // True if the given plugin appears in ANY layer of the stripMap. Used by + // gates around `repository` / `archive` (and updateOrg's rulesets / + // custom_repository_roles) where the plugin runs per-org or per-repo and + // there's no merge-time pipeline to strip into. + isPluginDisabledAnywhere (stripMap, pluginName) { + if (!stripMap) return false + for (const set of stripMap.values()) { + if (set.has(pluginName)) return true + } + return false + } + + // Returns the declaredAt layer(s) responsible for disabling `pluginName` + // in the given stripMap. Used to build informative NopCommand / log + // messages. Note: stripMap layers are *target* layers, not declaration + // layers — to report the source we re-walk the raw disable_plugins lists. + whoDisabled (pluginName, repoName) { + const sources = [] + const probe = (declaredAt, raw) => { + if (!raw) return + let entries = [] + try { entries = this.normalizeDisableEntries(raw, declaredAt) } catch { return } + for (const e of entries) { + if (e.plugin === pluginName) sources.push(`${declaredAt}(target=${e.target})`) + } + } + probe('deployment', DeploymentConfig && DeploymentConfig.config && DeploymentConfig.config.disable_plugins) + probe('org', this.config && this.config.disable_plugins) + if (repoName !== undefined && repoName !== null) { + const suborg = this.getSubOrgConfig(repoName) + probe('suborg', suborg && suborg.disable_plugins) + const repoOverride = this.getRepoOverrideConfig(repoName) + probe('repo', repoOverride && repoOverride.disable_plugins) + } + return sources + } + + // Apply strips to a `{ deployment, org, suborg, repo }` map of cloned + // configs. Mutates clones in place and returns them. Emits NopCommand + // entries when in nop mode. + applyStrips (stripMap, sources, repoName) { + if (!stripMap) return sources + for (const [level, pluginSet] of stripMap) { + const layer = sources[level] + if (!layer) continue + for (const plugin of pluginSet) { + if (Object.prototype.hasOwnProperty.call(layer, plugin)) { + delete layer[plugin] + this.log.debug(`disable_plugins: stripped '${plugin}' from ${level} layer (repo=${repoName || ''})`) + if (this.nop) { + const declaredBy = this.whoDisabled(plugin, repoName).join(', ') + const nopcommand = new NopCommand('disable_plugins', this.repo, null, `Plugin '${plugin}' stripped from ${level} layer (declared by: ${declaredBy || 'unknown'})`, 'INFO') + this.appendToResults([nopcommand]) + } + } + } + } + return sources + } + + // Emit a NopCommand recording that a per-execution-point plugin + // (rulesets / custom_repository_roles / repository / archive) was skipped + // because it appears in the stripMap. + emitDisableSkip (pluginName, repoName) { + if (!this.nop) return + const declaredBy = this.whoDisabled(pluginName, repoName).join(', ') + const nopcommand = new NopCommand('disable_plugins', this.repo, null, `Plugin '${pluginName}' skipped (declared by: ${declaredBy || 'unknown'})`, 'INFO') + this.appendToResults([nopcommand]) + } + async updateOrg () { + // Org-execution stripMap: no repo context, so only deployment + org + // disable_plugins contribute. + const stripMap = this.computeStripMap() + const rulesetsConfig = this.config.rulesets if (rulesetsConfig) { - const RulesetsPlugin = Settings.PLUGINS.rulesets - await new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync().then(res => { - this.appendToResults(res) - }) + if (this.isPluginDisabledAnywhere(stripMap, 'rulesets')) { + this.log.debug("disable_plugins: skipping org-level 'rulesets' plugin") + this.emitDisableSkip('rulesets') + } else { + const RulesetsPlugin = Settings.PLUGINS.rulesets + await new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync().then(res => { + this.appendToResults(res) + }) + } } const customRepositoryRolesConfig = this.config.custom_repository_roles if (customRepositoryRolesConfig) { - const CustomRepositoryRolesPlugin = Settings.PLUGINS.custom_repository_roles - await new CustomRepositoryRolesPlugin(this.nop, this.github, this.repo, customRepositoryRolesConfig, this.log, this.errors).sync().then(res => { - this.appendToResults(res) - }) + if (this.isPluginDisabledAnywhere(stripMap, 'custom_repository_roles')) { + this.log.debug("disable_plugins: skipping org-level 'custom_repository_roles' plugin") + this.emitDisableSkip('custom_repository_roles') + } else { + const CustomRepositoryRolesPlugin = Settings.PLUGINS.custom_repository_roles + await new CustomRepositoryRolesPlugin(this.nop, this.github, this.repo, customRepositoryRolesConfig, this.log, this.errors).sync().then(res => { + this.appendToResults(res) + }) + } } } @@ -395,6 +611,12 @@ ${this.results.reduce((x, y) => { repoConfig = this.mergeDeep.mergeDeep({}, repoConfig, overrideRepoConfig) } if (repoConfig) { + // Per-repo disable_plugins stripMap (used to gate repository + archive + // plugins, which run per-repo outside the childPluginsList pipeline). + const repoStripMap = this.computeStripMap(repo.repo) + const repositoryDisabled = this.isPluginDisabledAnywhere(repoStripMap, 'repository') + const archiveDisabled = this.isPluginDisabledAnywhere(repoStripMap, 'archive') + // Track actual change signals from the plugins, used by the suborg // re-evaluation logic below to avoid an unnecessary live API round-trip // when nothing relevant actually changed. @@ -405,8 +627,18 @@ ${this.results.reduce((x, y) => { const childPlugins = this.childPluginsList(repo) const RepoPlugin = Settings.PLUGINS.repository - const archivePlugin = new Archive(this.nop, this.github, repo, repoConfig, this.log) - const { shouldArchive, shouldUnarchive } = await archivePlugin.getState() + let archivePlugin = null + let shouldArchive = false + let shouldUnarchive = false + if (archiveDisabled) { + this.log.debug(`disable_plugins: skipping 'archive' plugin for ${repo.repo}`) + this.emitDisableSkip('archive', repo.repo) + } else { + archivePlugin = new Archive(this.nop, this.github, repo, repoConfig, this.log) + const state = await archivePlugin.getState() + shouldArchive = state.shouldArchive + shouldUnarchive = state.shouldUnarchive + } if (shouldUnarchive) { this.log.debug(`Unarchiving repo ${repo.repo}`) @@ -414,11 +646,16 @@ ${this.results.reduce((x, y) => { this.appendToResults(unArchiveResults) } - const repoPluginInstance = new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors) - const repoResults = await repoPluginInstance.sync() - this.appendToResults(repoResults) - if (repoPluginInstance.renamed) changeSignals.renamed = true - if (repoPluginInstance.created) changeSignals.created = true + if (repositoryDisabled) { + this.log.debug(`disable_plugins: skipping 'repository' plugin for ${repo.repo}`) + this.emitDisableSkip('repository', repo.repo) + } else { + const repoPluginInstance = new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors) + const repoResults = await repoPluginInstance.sync() + this.appendToResults(repoResults) + if (repoPluginInstance.renamed) changeSignals.renamed = true + if (repoPluginInstance.created) changeSignals.created = true + } const childPluginInstances = childPlugins.map(([Plugin, config]) => { return [Plugin, new Plugin(this.nop, this.github, repo, config, this.log, this.errors)] @@ -611,15 +848,39 @@ ${this.results.reduce((x, y) => { const newConfig = Object.assign({}, config) // clone delete newConfig.rulesets delete newConfig.custom_repository_roles + delete newConfig.disable_plugins return newConfig } + // Shallow-clone a config object and strip the `disable_plugins` key (it is + // metadata, not a plugin section that should be merged into child plugins). + cloneAndStripDisableMeta (config) { + if (!config) return {} + const clone = Object.assign({}, config) + delete clone.disable_plugins + return clone + } + childPluginsList (repo) { const repoName = repo.repo const subOrgOverrideConfig = this.getSubOrgConfig(repoName) this.log.debug(`suborg config for ${repoName} is ${JSON.stringify(subOrgOverrideConfig)}`) const repoOverrideConfig = this.getRepoOverrideConfig(repoName) - const overrideConfig = this.mergeDeep.mergeDeep({}, this.returnRepoSpecificConfigs(this.config), subOrgOverrideConfig, repoOverrideConfig) + + // Build clones of each layer and apply disable_plugins strips before the + // existing mergeDeep pipeline runs. The deployment layer's strips affect + // the OTHER three layers (per the matrix); the deployment config itself + // is not merged into per-repo plugin config today. + const stripMap = this.computeStripMap(repoName) + const sources = { + deployment: this.cloneAndStripDisableMeta((DeploymentConfig && DeploymentConfig.config) || {}), + org: this.returnRepoSpecificConfigs(this.config), + suborg: this.cloneAndStripDisableMeta(subOrgOverrideConfig), + repo: this.cloneAndStripDisableMeta(repoOverrideConfig) + } + this.applyStrips(stripMap, sources, repoName) + + const overrideConfig = this.mergeDeep.mergeDeep({}, sources.org, sources.suborg, sources.repo) this.log.debug(`consolidated config is ${JSON.stringify(overrideConfig)}`) diff --git a/package.json b/package.json index f0ae365bf..974308487 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "test:unit:watch": "npm run test:unit -- --watch", "test:integration": "jest --roots=lib --roots=test/integration", "test:integration:debug": "LOG_LEVEL=debug DEBUG=nock run-s test:integration", - "smoke-test": "node smoke-test.js" + "smoke-test": "node smoke-test.js", + "smoke-test:interactive": "node smoke-test.js --interactive" }, "author": "Yadhav Jayaraman", "license": "ISC", @@ -91,4 +92,4 @@ "." ] } -} +} \ No newline at end of file diff --git a/schema/dereferenced/settings.json b/schema/dereferenced/settings.json index e94a66e57..82630e0ad 100644 --- a/schema/dereferenced/settings.json +++ b/schema/dereferenced/settings.json @@ -39,7 +39,17 @@ "properties": { "advanced_security": { "type": "object", - "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository. For more information, see \"[About GitHub Advanced Security](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", "properties": { "status": { "type": "string", @@ -67,6 +77,16 @@ } } }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, "secret_scanning_non_provider_patterns": { "type": "object", "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", @@ -76,6 +96,66 @@ "description": "Can be `enabled` or `disabled`." } } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } } } }, @@ -135,7 +215,7 @@ }, "use_squash_pr_title_as_default": { "type": "boolean", - "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property has been deprecated. Please use `squash_merge_commit_title` instead.", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", "default": false, "deprecated": true }, @@ -338,7 +418,7 @@ }, "maintainers": { "type": "array", - "description": "List GitHub IDs for organization members who will become team maintainers.", + "description": "List GitHub usernames for organization members who will become team maintainers.", "items": { "type": "string" } @@ -368,7 +448,7 @@ }, "permission": { "type": "string", - "description": "**Deprecated**. The permission that new repositories will be added to the team with when none is specified.", + "description": "**Closing down notice**. The permission that new repositories will be added to the team with when none is specified.", "enum": [ "pull", "push" @@ -409,7 +489,7 @@ "contexts": { "type": "array", "deprecated": true, - "description": "**Deprecated**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", "items": { "type": "string" } @@ -663,7 +743,8 @@ "enum": [ "branch", "tag", - "push" + "push", + "repository" ], "default": "branch" }, @@ -690,7 +771,7 @@ "actor_id": { "type": "integer", "nullable": true, - "description": "The ID of the actor that can bypass a ruleset. If `actor_type` is `OrganizationAdmin`, this should be `1`. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." + "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, `Team`, and `User` actor types. If `actor_type` is `OrganizationAdmin`, `actor_id` is ignored. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." }, "actor_type": { "type": "string", @@ -699,16 +780,18 @@ "OrganizationAdmin", "RepositoryRole", "Team", - "DeployKey" + "DeployKey", + "User" ], "description": "The type of actor that can bypass a ruleset." }, "bypass_mode": { "type": "string", - "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets.", + "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.", "enum": [ "always", - "pull_request" + "pull_request", + "exempt" ], "default": "always" } @@ -718,7 +801,7 @@ "conditions": { "title": "Organization ruleset conditions", "type": "object", - "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.", + "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.\nFor repository policy rulesets, the conditions object should only contain the `repository_name`, the `repository_id`, or the `repository_property`.", "oneOf": [ { "type": "object", @@ -1043,83 +1126,6 @@ } } }, - { - "title": "merge_queue", - "description": "Merges must be performed via a merge queue.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "merge_queue" - ] - }, - "parameters": { - "type": "object", - "properties": { - "check_response_timeout_minutes": { - "type": "integer", - "description": "Maximum time for a required status check to report a conclusion. After this much time has elapsed, checks that have not reported a conclusion will be assumed to have failed", - "minimum": 1, - "maximum": 360 - }, - "grouping_strategy": { - "type": "string", - "description": "When set to ALLGREEN, the merge commit created by merge queue for each PR in the group must pass all required checks to merge. When set to HEADGREEN, only the commit at the head of the merge group, i.e. the commit containing changes from all of the PRs in the group, must pass its required checks to merge.", - "enum": [ - "ALLGREEN", - "HEADGREEN" - ] - }, - "max_entries_to_build": { - "type": "integer", - "description": "Limit the number of queued pull requests requesting checks and workflow runs at the same time.", - "minimum": 0, - "maximum": 100 - }, - "max_entries_to_merge": { - "type": "integer", - "description": "The maximum number of PRs that will be merged together in a group.", - "minimum": 0, - "maximum": 100 - }, - "merge_method": { - "type": "string", - "description": "Method to use when merging changes from queued pull requests.", - "enum": [ - "MERGE", - "SQUASH", - "REBASE" - ] - }, - "min_entries_to_merge": { - "type": "integer", - "description": "The minimum number of PRs that will be merged together in a group.", - "minimum": 0, - "maximum": 100 - }, - "min_entries_to_merge_wait_minutes": { - "type": "integer", - "description": "The time merge queue should wait after the first PR is added to the queue for the minimum group size to be met. After this time has elapsed, the minimum group size will be ignored and a smaller group will be merged.", - "minimum": 0, - "maximum": 360 - } - }, - "required": [ - "check_response_timeout_minutes", - "grouping_strategy", - "max_entries_to_build", - "max_entries_to_merge", - "merge_method", - "min_entries_to_merge", - "min_entries_to_merge_wait_minutes" - ] - } - } - }, { "title": "required_deployments", "description": "Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule.", @@ -1184,6 +1190,18 @@ "parameters": { "type": "object", "properties": { + "allowed_merge_methods": { + "type": "array", + "description": "Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled.", + "items": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, "dismiss_stale_reviews_on_push": { "type": "boolean", "description": "New, reviewable commits pushed will dismiss previous pull request review approvals." @@ -1205,6 +1223,55 @@ "required_review_thread_resolution": { "type": "boolean", "description": "All conversations on code must be resolved before a pull request can be merged." + }, + "required_reviewers": { + "type": "array", + "description": "> [!NOTE]\n> `required_reviewers` is in beta and subject to change.\n\nA collection of reviewers and associated file patterns. Each reviewer has a list of file patterns which determine the files that reviewer is required to review.", + "items": { + "title": "RequiredReviewerConfiguration", + "description": "A reviewing team, and file patterns describing which files they must approve changes to.", + "type": "object", + "properties": { + "file_patterns": { + "type": "array", + "description": "Array of file patterns. Pull requests which change matching files must be approved by the specified team. File patterns use fnmatch syntax.", + "items": { + "type": "string" + } + }, + "minimum_approvals": { + "type": "integer", + "description": "Minimum number of approvals required from the specified team. If set to zero, the team will be added to the pull request but approval is optional." + }, + "reviewer": { + "title": "Reviewer", + "description": "A required reviewing team", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the reviewer which must review changes to matching files." + }, + "type": { + "type": "string", + "description": "The type of the reviewer", + "enum": [ + "Team" + ] + } + }, + "required": [ + "id", + "type" + ] + } + }, + "required": [ + "file_patterns", + "minimum_approvals", + "reviewer" + ] + } } }, "required": [ @@ -1307,7 +1374,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1354,7 +1421,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1401,7 +1468,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1448,7 +1515,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1495,7 +1562,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1525,7 +1592,7 @@ }, { "title": "file_path_restriction", - "description": "Prevent commits that include changes in specified file paths from being pushed to the commit graph.", + "description": "Prevent commits that include changes in specified file and folder paths from being pushed to the commit graph. This includes absolute paths that contain file names.", "type": "object", "required": [ "type" @@ -1556,7 +1623,7 @@ }, { "title": "max_file_path_length", - "description": "Prevent commits that include file paths that exceed a specified character limit from being pushed to the commit graph.", + "description": "Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph.", "type": "object", "required": [ "type" @@ -1573,9 +1640,9 @@ "properties": { "max_file_path_length": { "type": "integer", - "description": "The maximum amount of characters allowed in file paths", + "description": "The maximum amount of characters allowed in file paths.", "minimum": 1, - "maximum": 256 + "maximum": 32767 } }, "required": [ @@ -1617,7 +1684,7 @@ }, { "title": "max_file_size", - "description": "Prevent commits that exceed a specified file size limit from being pushed to the commit.", + "description": "Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph.", "type": "object", "required": [ "type" @@ -1768,6 +1835,35 @@ ] } } + }, + { + "title": "copilot_code_review", + "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review and their premium requests quota has not reached the limit.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "copilot_code_review" + ] + }, + "parameters": { + "type": "object", + "properties": { + "review_draft_pull_requests": { + "type": "boolean", + "description": "Copilot automatically reviews draft pull requests before they are marked as ready for review." + }, + "review_on_push": { + "type": "boolean", + "description": "Copilot automatically reviews each new push to the pull request." + } + } + } + } } ] } @@ -1778,6 +1874,112 @@ "enforcement" ] } + }, + "custom_repository_roles": { + "description": "Org-level custom repository roles. Only valid in the org-level settings.yml.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "base_role", + "permissions" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the custom role." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "A short description of the role." + }, + "base_role": { + "type": "string", + "enum": [ + "read", + "triage", + "write", + "maintain" + ], + "description": "The system role from which this role inherits permissions." + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional fine-grained permissions included in this role." + } + } + } + }, + "disable_plugins": { + "description": "List of plugins to disable at this configuration layer. Each entry is either a plugin name (string shorthand, equivalent to target: all) or an object {plugin, target}. target=self disables the plugin at this layer only; target=children disables it at all lower layers; target=all disables it at this layer and all lower layers. Cascade is union-only; lower layers cannot re-enable a disabled plugin.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive" + ] + }, + { + "type": "object", + "required": [ + "plugin" + ], + "additionalProperties": false, + "properties": { + "plugin": { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive" + ] + }, + "target": { + "type": "string", + "enum": [ + "self", + "children", + "all" + ], + "default": "all" + } + } + } + ] + } } } } \ No newline at end of file diff --git a/schema/settings.json b/schema/settings.json index 3649d88ea..45f85340d 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -233,6 +233,64 @@ } } } + }, + "disable_plugins": { + "description": "List of plugins to disable at this configuration layer. Each entry is either a plugin name (string shorthand, equivalent to target: all) or an object {plugin, target}. target=self disables the plugin at this layer only; target=children disables it at all lower layers; target=all disables it at this layer and all lower layers. Cascade is union-only; lower layers cannot re-enable a disabled plugin.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive" + ] + }, + { + "type": "object", + "required": ["plugin"], + "additionalProperties": false, + "properties": { + "plugin": { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive" + ] + }, + "target": { + "type": "string", + "enum": ["self", "children", "all"], + "default": "all" + } + } + } + ] + } } } } \ No newline at end of file diff --git a/smoke-test.js b/smoke-test.js index d881972da..9725ea712 100644 --- a/smoke-test.js +++ b/smoke-test.js @@ -9,6 +9,7 @@ * This is required for drift-remediation tests (Phases 2 & 3) so that * changes appear as a human (not Bot) and trigger safe-settings webhooks. * 3. Run: `node smoke-test.js` + * Add --interactive to pause after each phase for manual validation. * Set SMOKE_VERBOSE=1 for live safe-settings logs. * * Auth: @@ -19,6 +20,7 @@ const { execSync, spawn } = require('child_process') const fs = require('fs') const path = require('path') +const readline = require('readline') // ─── Configuration ─────────────────────────────────────────────────────────── @@ -75,6 +77,16 @@ const WEBHOOK_SETTLE_MS = 15000 // Fine-grained PAT for drift tests (must appear as a human, not Bot) const GH_TOKEN = process.env.GH_TOKEN || '' +// Interactive mode: pause after each phase for manual validation +const INTERACTIVE = process.argv.includes('--interactive') + +class InteractiveExit extends Error { + constructor (action) { + super(`interactive:${action}`) + this.action = action + } +} + // ─── Octokit client (initialized in main) ──────────────────────────────────── let octokit = null @@ -109,6 +121,76 @@ async function poll (fn, { timeout = MAX_POLL_MS, interval = POLL_INTERVAL_MS, d return null } +// ─── Interactive mode ───────────────────────────────────────────────────────── + +let skipNext = false + +async function pause (phaseName) { + return new Promise((resolve) => { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + process.stdout.write( + `\n\x1b[33m[interactive] "${phaseName}" complete.\x1b[0m\n` + + ` \x1b[90mPress Enter to continue, 's' skip next, 'q' quit+teardown, 'a' abort: \x1b[0m` + ) + rl.once('line', (answer) => { + const input = answer.trim().toLowerCase() + if (input === 's') resolve('skip') + else if (input === 'q') resolve('quit') + else if (input === 'a') resolve('abort') + else resolve('continue') + rl.close() + }) + rl.once('close', () => resolve('continue')) + }) +} + +async function runPhase (label, fn) { + if (skipNext) { + log(`\x1b[33m[interactive] Skipping ${label}\x1b[0m`) + skipNext = false + return 'skipped' + } + await fn() + if (!INTERACTIVE) return 'continue' + const action = await pause(label) + if (action === 'skip') skipNext = true + return action +} + +async function confirmMerge (owner, repo, prNumber) { + return new Promise((resolve) => { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + process.stdout.write( + `\n\x1b[33m[interactive] PR #${prNumber} is ready to merge.\x1b[0m\n` + + ` \x1b[90mPress Enter to merge, 'c' to close PR, 'q' quit+teardown, 'a' abort: \x1b[0m` + ) + rl.once('line', (answer) => { + const input = answer.trim().toLowerCase() + if (input === 'c') resolve('close') + else if (input === 'q') resolve('quit') + else if (input === 'a') resolve('abort') + else resolve('merge') + rl.close() + }) + rl.once('close', () => resolve('merge')) + }) +} + +async function safeMerge (owner, repo, prNumber) { + if (INTERACTIVE) { + const action = await confirmMerge(owner, repo, prNumber) + if (action !== 'merge') { + try { await octokit.rest.pulls.update({ owner, repo, pull_number: prNumber, state: 'closed' }) } catch { /* ok */ } + log(`\x1b[33m[interactive] PR #${prNumber} closed.\x1b[0m`) + if (action === 'quit' || action === 'abort') throw new InteractiveExit(action) + return false + } + } + log('Merging PR...') + await mergePR(owner, repo, prNumber) + return true +} + // ─── GitHub API helpers ────────────────────────────────────────────────────── async function getDefaultBranch () { @@ -523,6 +605,51 @@ custom_repository_roles: - delete_alerts_code_scanning ` +// Phase 10a: settings.yml that disables custom_repository_roles at org-self, +// and tries to add a NEW role ("disabled-role"). The new role must NOT be created. +const SETTINGS_YML_DISABLE_CRR = `# Org-level settings with disable_plugins (custom_repository_roles) + +disable_plugins: + - plugin: custom_repository_roles + target: self + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning + - name: disabled-role + description: This role MUST NOT be created (custom_repository_roles disabled) + base_role: read + permissions: + - delete_alerts_code_scanning +` + +// Phase 10b: settings.yml with invalid disable_plugins entry — should fail validation +const SETTINGS_YML_INVALID_DISABLE = `# Org-level settings with invalid disable_plugins + +disable_plugins: + - not-a-real-plugin +` + // ─── Test Phases ───────────────────────────────────────────────────────────── async function setup () { @@ -565,8 +692,7 @@ async function phase1CreateRepo () { assert(checkRun !== null, 'Check run completed') if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return await sleep(WEBHOOK_SETTLE_MS) // Validate repo @@ -693,8 +819,7 @@ async function phase4DemoRepo1 () { assert(checkRun !== null, 'Check run completed') if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return await sleep(WEBHOOK_SETTLE_MS) const repo = await poll(async () => { @@ -750,8 +875,7 @@ async function phase5Suborg () { assert(checkRun !== null, 'Check run completed') if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return await sleep(WEBHOOK_SETTLE_MS) log('Checking suborg ruleset on demo-repo-service1...') @@ -782,8 +906,7 @@ async function phase6Archive () { assert(checkRun !== null, 'Check run completed') if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return await sleep(WEBHOOK_SETTLE_MS) const repo = await poll(async () => { @@ -813,8 +936,7 @@ async function phase7DemoRepo2 () { assert(checkRun !== null, 'Check run completed') if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return await sleep(WEBHOOK_SETTLE_MS) const repo = await poll(async () => { @@ -861,8 +983,7 @@ async function phase7bExternalGroupTeam () { assert(checkRun1 !== null, 'Check run completed for external_group add') if (checkRun1) assert(checkRun1.conclusion === 'success', `Check run conclusion is success (got: ${checkRun1.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr1.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr1.number)) return await sleep(WEBHOOK_SETTLE_MS) // Verify team is created and assigned to the repo @@ -907,8 +1028,7 @@ async function phase7bExternalGroupTeam () { assert(checkRun2 !== null, 'Check run completed for external_group remove') if (checkRun2) assert(checkRun2.conclusion === 'success', `Check run conclusion is success (got: ${checkRun2.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr2.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr2.number)) return await sleep(WEBHOOK_SETTLE_MS) // Verify team is removed from the repo @@ -941,8 +1061,7 @@ async function phase8OrgSettings () { assert(checkRun !== null, 'Check run completed') if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) - log('Merging PR...') - await mergePR(ORG, ADMIN_REPO, pr.number) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return await sleep(WEBHOOK_SETTLE_MS) log('Checking custom repository roles...') @@ -966,6 +1085,66 @@ async function phase8OrgSettings () { await deleteBranch(ORG, ADMIN_REPO, branch) } +async function phase10DisablePlugins () { + logPhase('Phase 10: disable_plugins') + + const defaultBranch = await getDefaultBranch() + + // ── 10a: Org disables custom_repository_roles at target:self ── + // Add a NEW role "disabled-role" + keep existing "security-engineer". + // Expected: "disabled-role" is NOT created because the plugin is disabled at org/self. + { + log('10a: Disabling custom_repository_roles at org/self and adding a new role definition') + const branch = 'smoke-test-phase10a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_DISABLE_CRR, branch, '10a: disable custom_repository_roles') + + const pr = await createPR(ORG, ADMIN_REPO, '10a: disable custom_repository_roles', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '10a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `10a: NOP check run is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // Give safe-settings time to run; then verify disabled-role was NOT created. + await sleep(20000) + let disabledRoleExists = false + try { + const { data } = await octokit.request('GET /orgs/{org}/custom-repository-roles', { org: ORG }) + disabledRoleExists = (data.custom_roles || []).some(r => r.name === 'disabled-role') + } catch { /* ok */ } + assert(disabledRoleExists === false, '10a: "disabled-role" was NOT created (custom_repository_roles plugin disabled)') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 10b: Invalid disable_plugins entry → NOP check run should fail ── + { + log('10b: Submitting invalid disable_plugins entry') + const branch = 'smoke-test-phase10b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_INVALID_DISABLE, branch, '10b: invalid disable_plugins') + + const pr = await createPR(ORG, ADMIN_REPO, '10b: invalid disable_plugins', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '10b: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion !== 'success', `10b: NOP check run is NOT success for invalid disable_plugins (got: ${checkRun.conclusion})`) + } + + // Close PR without merging — invalid config should never be merged. + try { await octokit.rest.pulls.update({ owner: ORG, repo: ADMIN_REPO, pull_number: pr.number, state: 'closed' }) } catch { /* ok */ } + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + async function teardown () { logPhase('Phase 9: Teardown') @@ -1028,22 +1207,38 @@ async function main () { ╚══════════════════════════════════════╝\x1b[0m `) + if (INTERACTIVE) log('\x1b[33m[interactive] Mode enabled — will pause after each phase.\x1b[0m') + + let doTeardown = true try { - await setup() - await phase1CreateRepo() - await phase2DriftTeam() - await phase3DriftRuleset() - await phase4DemoRepo1() - await phase5Suborg() - await phase6Archive() - await phase7DemoRepo2() - await phase7bExternalGroupTeam() - await phase8OrgSettings() + const phases = [ + ['Phase 0: Setup', setup], + ['Phase 1: Create test repo', phase1CreateRepo], + ['Phase 2: Drift remediation - Team removal', phase2DriftTeam], + ['Phase 3: Drift remediation - Rogue ruleset', phase3DriftRuleset], + ['Phase 4: Create demo-repo-service1', phase4DemoRepo1], + ['Phase 5: Create suborg config', phase5Suborg], + ['Phase 6: Archive demo-repo-service1', phase6Archive], + ['Phase 7: Create demo-repo-service2', phase7DemoRepo2], + ['Phase 7b: External group team', phase7bExternalGroupTeam], + ['Phase 8: Org-level settings', phase8OrgSettings], + ['Phase 10: disable_plugins', phase10DisablePlugins] + ] + for (const [label, fn] of phases) { + const action = await runPhase(label, fn) + if (action === 'abort') { doTeardown = false; break } + if (action === 'quit') break + } } catch (err) { - console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`) - console.error(err.stack) + if (err instanceof InteractiveExit) { + if (err.action === 'abort') doTeardown = false + } else { + console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`) + console.error(err.stack) + } } finally { - await teardown() + if (doTeardown) await teardown() + else log('\x1b[33m[interactive] Aborted — teardown skipped.\x1b[0m') } console.log(` diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index 23d8ef84d..a56296310 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -639,4 +639,369 @@ repository: expect(stubContext.log.warn).toHaveBeenCalledWith(expect.stringContaining('max depth')) }) }) + + // ──────────────────────────────────────────────────────────────────────── + // disable_plugins + // ──────────────────────────────────────────────────────────────────────── + describe('disable_plugins', () => { + const DeploymentConfig = require('../../../lib/deploymentConfig') + let savedDeploymentDisable + let savedPlugins + + beforeEach(() => { + savedDeploymentDisable = DeploymentConfig.config && DeploymentConfig.config.disable_plugins + if (DeploymentConfig.config) delete DeploymentConfig.config.disable_plugins + savedPlugins = { ...Settings.PLUGINS } + }) + + afterEach(() => { + if (DeploymentConfig.config) { + if (savedDeploymentDisable !== undefined) { + DeploymentConfig.config.disable_plugins = savedDeploymentDisable + } else { + delete DeploymentConfig.config.disable_plugins + } + } + Object.keys(Settings.PLUGINS).forEach(k => { Settings.PLUGINS[k] = savedPlugins[k] }) + }) + + // ── normalizeDisableEntries ────────────────────────────────────────── + describe('normalizeDisableEntries', () => { + it('1. string shorthand defaults target=all and sets declaredAt', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries(['labels'], 'org') + expect(out).toEqual([{ plugin: 'labels', target: 'all', declaredAt: 'org' }]) + }) + + it('2. object form preserves each of self|children|all', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries([ + { plugin: 'rulesets', target: 'self' }, + { plugin: 'branches', target: 'children' }, + { plugin: 'labels', target: 'all' } + ], 'org') + expect(out).toEqual([ + { plugin: 'rulesets', target: 'self', declaredAt: 'org' }, + { plugin: 'branches', target: 'children', declaredAt: 'org' }, + { plugin: 'labels', target: 'all', declaredAt: 'org' } + ]) + }) + + it('3. unknown plugin name throws descriptive error', () => { + const settings = createSettings({}) + expect(() => settings.normalizeDisableEntries(['nope'], 'org')) + .toThrow(/unknown plugin 'nope'/) + }) + + it('4. invalid target throws', () => { + const settings = createSettings({}) + expect(() => settings.normalizeDisableEntries([{ plugin: 'labels', target: 'bogus' }], 'org')) + .toThrow(/invalid target 'bogus'/) + }) + + it('5. repository and archive are accepted as plugin names', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries(['repository', 'archive'], 'org') + expect(out.map(e => e.plugin).sort()).toEqual(['archive', 'repository']) + }) + + it('6. at declaredAt=repo, target=children normalizes to all', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries([{ plugin: 'labels', target: 'children' }], 'repo') + expect(out).toEqual([{ plugin: 'labels', target: 'all', declaredAt: 'repo' }]) + }) + }) + + // ── computeStripMap ────────────────────────────────────────────────── + describe('computeStripMap', () => { + const repoName = 'my-repo' + + it('7. empty configs produce empty map (all four levels are empty sets)', () => { + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + for (const level of ['deployment', 'org', 'suborg', 'repo']) { + expect(sm.get(level).size).toBe(0) + } + }) + + it('8. org target:self for rulesets strips only the org layer', () => { + const settings = createSettings({ disable_plugins: [{ plugin: 'rulesets', target: 'self' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect([...sm.get('org')]).toEqual(['rulesets']) + expect(sm.get('suborg').size).toBe(0) + expect(sm.get('repo').size).toBe(0) + expect(sm.get('deployment').size).toBe(0) + }) + + it('9. org target:children for branches strips suborg+repo', () => { + const settings = createSettings({ disable_plugins: [{ plugin: 'branches', target: 'children' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect(sm.get('org').size).toBe(0) + expect([...sm.get('suborg')]).toEqual(['branches']) + expect([...sm.get('repo')]).toEqual(['branches']) + }) + + it('10. org target:all for labels strips org+suborg+repo', () => { + const settings = createSettings({ disable_plugins: ['labels'] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect([...sm.get('org')]).toEqual(['labels']) + expect([...sm.get('suborg')]).toEqual(['labels']) + expect([...sm.get('repo')]).toEqual(['labels']) + }) + + it('11. suborg target:all contributes only when a suborg matches the repo', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + [repoName]: { disable_plugins: ['teams'], source: '.github/suborgs/x.yml' } + } + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect([...sm.get('suborg')]).toEqual(['teams']) + expect([...sm.get('repo')]).toEqual(['teams']) + + const sm2 = settings.computeStripMap('other-repo') + expect(sm2.get('suborg').size).toBe(0) + expect(sm2.get('repo').size).toBe(0) + }) + + it('12. repo-declared target:all only strips repo layer', () => { + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = { [`${repoName}.yml`]: { disable_plugins: ['labels'] } } + const sm = settings.computeStripMap(repoName) + expect(sm.get('org').size).toBe(0) + expect(sm.get('suborg').size).toBe(0) + expect([...sm.get('repo')]).toEqual(['labels']) + }) + + it('13. deployment target:children strips org+suborg+repo', () => { + DeploymentConfig.config.disable_plugins = [{ plugin: 'milestones', target: 'children' }] + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect(sm.get('deployment').size).toBe(0) + expect([...sm.get('org')]).toEqual(['milestones']) + expect([...sm.get('suborg')]).toEqual(['milestones']) + expect([...sm.get('repo')]).toEqual(['milestones']) + }) + + it('14. union across layers: org self + repo all → org and repo both contain plugin', () => { + const settings = createSettings({ disable_plugins: [{ plugin: 'labels', target: 'self' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = { [`${repoName}.yml`]: { disable_plugins: ['labels'] } } + const sm = settings.computeStripMap(repoName) + expect([...sm.get('org')]).toEqual(['labels']) + expect(sm.get('suborg').size).toBe(0) + expect([...sm.get('repo')]).toEqual(['labels']) + }) + }) + + // ── childPluginsList integration ───────────────────────────────────── + describe('childPluginsList integration', () => { + it('15. org disables custom_properties (target:all) → not in plugin list even with repo override', () => { + const settings = createSettings({ + disable_plugins: ['custom_properties'], + custom_properties: [{ property_name: 'a', value: '1' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = { 'foo.yml': { custom_properties: [{ property_name: 'b', value: '2' }] } } + const list = settings.childPluginsList({ repo: 'foo' }) + const pluginNames = list.map(([P]) => Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(pluginNames).not.toContain('custom_properties') + }) + + it('16. suborg-declared branches + suborg disable_plugins:branches → stripped for matched repo only', () => { + // Per the matrix, suborg target:all strips suborg+repo (NOT org). + // So we put branches only at suborg level for a meaningful test. + const settings = createSettings({}) + settings.subOrgConfigs = { + 'matched-repo': { + disable_plugins: ['branches'], + branches: [{ name: 'main', protection: {} }], + source: '.github/suborgs/x.yml' + }, + 'other-repo': { + // different suborg without disable; still declares branches + branches: [{ name: 'main', protection: {} }], + source: '.github/suborgs/y.yml' + } + } + settings.repoConfigs = {} + const matched = settings.childPluginsList({ repo: 'matched-repo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + const other = settings.childPluginsList({ repo: 'other-repo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(matched).not.toContain('branches') + expect(other).toContain('branches') + }) + + it('17. repo-level labels + repo disable_plugins:labels → stripped for that repo only', () => { + // Repo target:all strips only the repo layer (matrix). To demonstrate + // scoping we put labels in each repo's own yml so the strip is effective. + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = { + 'foo.yml': { disable_plugins: ['labels'], labels: { include: [{ name: 'bug' }] } }, + 'bar.yml': { labels: { include: [{ name: 'bug' }] } } + } + const foo = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + const bar = settings.childPluginsList({ repo: 'bar' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(foo).not.toContain('labels') + expect(bar).toContain('labels') + }) + + it('18. org target:children for variables: org-level variables still run per-repo (documented nuance)', () => { + // target:children strips from suborg+repo only; merged repo plugin + // config still inherits the org-level variables → plugin DOES run. + const settings = createSettings({ + disable_plugins: [{ plugin: 'variables', target: 'children' }], + variables: [{ name: 'FOO', value: 'bar' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const names = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(names).toContain('variables') + }) + + it('19. org target:all for variables: variables plugin is fully suppressed', () => { + const settings = createSettings({ + disable_plugins: [{ plugin: 'variables', target: 'all' }], + variables: [{ name: 'FOO', value: 'bar' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const names = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(names).not.toContain('variables') + }) + }) + + // ── updateOrg integration ──────────────────────────────────────────── + describe('updateOrg integration', () => { + function stubPlugin () { + const sync = jest.fn().mockResolvedValue([]) + const ctor = jest.fn().mockImplementation(() => ({ sync })) + return { ctor, sync } + } + + it('20. org disable rulesets (target:self) → rulesets plugin NOT invoked', async () => { + const { ctor } = stubPlugin() + Settings.PLUGINS.rulesets = ctor + const settings = createSettings({ + disable_plugins: [{ plugin: 'rulesets', target: 'self' }], + rulesets: [{ name: 'foo' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + expect(ctor).not.toHaveBeenCalled() + }) + + it('21. org disable custom_repository_roles (shorthand) → plugin NOT invoked', async () => { + const { ctor } = stubPlugin() + Settings.PLUGINS.custom_repository_roles = ctor + const settings = createSettings({ + disable_plugins: ['custom_repository_roles'], + custom_repository_roles: [{ name: 'sec' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + expect(ctor).not.toHaveBeenCalled() + }) + + it('22. deployment disable rulesets overrides org config that wants rulesets', async () => { + DeploymentConfig.config.disable_plugins = ['rulesets'] + const { ctor } = stubPlugin() + Settings.PLUGINS.rulesets = ctor + const settings = createSettings({ rulesets: [{ name: 'foo' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + expect(ctor).not.toHaveBeenCalled() + }) + }) + + // ── updateRepos integration ────────────────────────────────────────── + describe('updateRepos integration', () => { + it('23. org disable repository → RepoPlugin not instantiated', async () => { + const repoSync = jest.fn().mockResolvedValue([]) + const repoCtor = jest.fn().mockImplementation(() => ({ sync: repoSync, renamed: false, created: false })) + Settings.PLUGINS.repository = repoCtor + const settings = createSettings({ + disable_plugins: ['repository'], + repository: { name: 'will-not-be-used' } + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + // Avoid running child plugins (their internal logic isn't under test). + jest.spyOn(settings, 'childPluginsList').mockReturnValue([]) + await settings.updateRepos({ owner: 'o', repo: 'r' }) + expect(repoCtor).not.toHaveBeenCalled() + }) + + it('24. org disable archive → archive plugin getState NOT invoked', async () => { + const Archive = require('../../../lib/plugins/archive') + const getStateSpy = jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ shouldArchive: false, shouldUnarchive: false }) + // RepoPlugin still runs; stub it to a no-op constructor. + const repoSync = jest.fn().mockResolvedValue([]) + Settings.PLUGINS.repository = jest.fn().mockImplementation(() => ({ sync: repoSync, renamed: false, created: false })) + const settings = createSettings({ + disable_plugins: ['archive'], + repository: { name: 'r' } + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + jest.spyOn(settings, 'childPluginsList').mockReturnValue([]) + await settings.updateRepos({ owner: 'o', repo: 'r' }) + expect(getStateSpy).not.toHaveBeenCalled() + getStateSpy.mockRestore() + }) + }) + + // ── cascade enforcement ────────────────────────────────────────────── + describe('cascade enforcement', () => { + it('25. org target:all labels; repo declares empty disable_plugins → labels still disabled', () => { + const settings = createSettings({ + disable_plugins: ['labels'], + labels: { include: [{ name: 'bug' }] } + }) + settings.subOrgConfigs = {} + settings.repoConfigs = { 'foo.yml': { disable_plugins: [] } } + const names = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(names).not.toContain('labels') + }) + }) + + // ── NOP mode ───────────────────────────────────────────────────────── + describe('NOP mode', () => { + it('26. each strip produces a NopCommand with type=INFO and plugin/level info', () => { + const settings = new Settings(true, stubContext, mockRepo, { + disable_plugins: ['labels'], + labels: { include: [{ name: 'bug' }] } + }, mockRef) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + settings.childPluginsList({ repo: 'foo' }) + const nopEntries = settings.results.filter(r => r && r.plugin === 'disable_plugins') + expect(nopEntries.length).toBeGreaterThan(0) + expect(nopEntries[0].type).toBe('INFO') + expect(nopEntries[0].action.msg).toMatch(/labels/) + expect(nopEntries[0].action.msg).toMatch(/declared by/) + }) + }) + }) }) // Settings Tests From 3cac68babbe4743b2f902eaae8ba5b881ddcb8b3 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Sat, 23 May 2026 21:56:52 -0400 Subject: [PATCH 19/22] fix: add action.msg to dedup key so multiple disable_plugins NopCommands survive Without action.msg in the dedup key, multiple disable_plugins NopCommands for the same repo (e.g. skipping 'labels' AND 'teams') all share the same type+repo+plugin+endpoint key and only the first one survives, silently dropping the rest from the PR comment and check-run output. Adding action.msg to the key ensures each unique informational message is retained while still deduplicating exact duplicates. Also adds test 27 to cover this case. --- lib/settings.js | 10 ++++++---- test/unit/lib/settings.test.js | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/settings.js b/lib/settings.js index f1584fa97..ebab5fde9 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -253,12 +253,14 @@ class Settings { return } - // Remove duplicate rows. The key includes endpoint so that distinct - // per-operation NopCommands (individual add/update/remove from diffable - // plugins) survive alongside the overall diff-summary NopCommand. + // Remove duplicate rows. The key includes endpoint + action.msg so that: + // - per-operation NopCommands (individual add/update/remove from diffable + // plugins) survive alongside the overall diff-summary NopCommand, and + // - distinct disable_plugins skip messages (each with a unique msg but + // the same empty endpoint) are all retained. this.results = this.results.filter((thing, index, self) => { return index === self.findIndex((t) => { - return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin && t.endpoint === thing.endpoint + return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin && t.endpoint === thing.endpoint && t.action?.msg === thing.action?.msg }) }) diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index a56296310..1a4366f25 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -1002,6 +1002,26 @@ repository: expect(nopEntries[0].action.msg).toMatch(/labels/) expect(nopEntries[0].action.msg).toMatch(/declared by/) }) + + it('27. dedup retains all disable_plugins NopCommands when multiple plugins are disabled for the same repo', () => { + // Disable both labels and teams at org level for all layers. + const settings = new Settings(true, stubContext, mockRepo, { + disable_plugins: ['labels', 'teams'], + labels: [{ name: 'bug', color: 'red' }], + teams: [{ name: 'core', permission: 'push' }] + }, mockRef) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + settings.childPluginsList({ repo: 'foo' }) + const nopEntries = settings.results.filter(r => r && r.plugin === 'disable_plugins') + // Both 'labels' and 'teams' disable messages must survive; the old + // dedup (key = type+repo+plugin+endpoint) would drop one of them + // because they share the same empty endpoint. The new key adds + // action.msg, so each unique message is kept. + const msgs = nopEntries.map(r => r.action.msg) + expect(msgs.some(m => /labels/.test(m))).toBe(true) + expect(msgs.some(m => /teams/.test(m))).toBe(true) + }) }) }) }) // Settings Tests From 331279561904cb058bb8deef60bbec17cf560db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Mon, 1 Jun 2026 12:04:15 -0400 Subject: [PATCH 20/22] added base_url support --- .env.example | 5 + docs/hubSyncHandler/BASE_PATH.md | 141 +++++++++++ lib/env.js | 8 +- lib/routes.js | 34 ++- package-lock.json | 222 ++++++++---------- package.json | 6 +- test/unit/lib/routes.test.js | 6 + ui/next.config.js | 9 + ui/src/app/components/EnvVariables.jsx | 23 +- ui/src/app/components/HubOrgGraph.jsx | 3 +- ui/src/app/components/OrganizationsTable.jsx | 15 +- .../components/Safe-settings-hubContent.jsx | 7 +- ui/src/app/components/TitleBar.jsx | 28 +-- ui/src/app/not-found.jsx | 3 +- ui/src/app/utils/basePath.js | 36 +++ 15 files changed, 384 insertions(+), 162 deletions(-) create mode 100644 docs/hubSyncHandler/BASE_PATH.md create mode 100644 ui/src/app/utils/basePath.js diff --git a/.env.example b/.env.example index 7f8a45ee3..1084e9837 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,11 @@ CONFIG_PATH=.github SETTINGS_FILE_PATH=settings.yml +# URL prefix for deployment behind a proxy (appears in browser address bar) +# Default: /safe-settings +# Set to empty string for root path deployment: SAFE_SETTINGS_HUB_URL_PREFIX= +# SAFE_SETTINGS_HUB_URL_PREFIX=/safe-settings + # Configuration support for Hub-Sync safe-settings feature # SAFE_SETTINGS_HUB_REPO=safe-settings-config-master # SAFE_SETTINGS_HUB_ORG=foo-training diff --git a/docs/hubSyncHandler/BASE_PATH.md b/docs/hubSyncHandler/BASE_PATH.md new file mode 100644 index 000000000..548fbae76 --- /dev/null +++ b/docs/hubSyncHandler/BASE_PATH.md @@ -0,0 +1,141 @@ +# URL Prefix Configuration + +Safe Settings supports deployment behind a reverse proxy (like NGINX) that routes to the application using a custom URL prefix. + +## Overview + +By default, Safe Settings serves its UI and API from `/safe-settings`: +- Dashboard: `http://localhost:3000/safe-settings/dashboard` +- API: `http://localhost:3000/safe-settings/api/safe-settings/...` + +You can customize this by setting the `SAFE_SETTINGS_HUB_URL_PREFIX` environment variable, or set it to an empty string for root path deployment: +- Dashboard (root): `http://localhost:3000/dashboard` +- API (root): `http://localhost:3000/api/safe-settings/...` + +## Configuration + +### Default Behavior + +Safe Settings defaults to `SAFE_SETTINGS_HUB_URL_PREFIX=/safe-settings`. No configuration needed for this default. + +### Customizing the URL Prefix + +To use a different URL prefix, add to your `.env` file: + +```bash +SAFE_SETTINGS_HUB_URL_PREFIX=/my-custom-path +``` + +### Root Path Deployment + +To deploy at the root path instead, set SAFE_SETTINGS_HUB_URL_PREFIX to an empty string: + +```bash +SAFE_SETTINGS_HUB_URL_PREFIX= +``` + +**Important:** +- The SAFE_SETTINGS_HUB_URL_PREFIX will automatically add a leading `/` if you forget it +- Examples: `/safe-settings`, `safe-settings`, `/apps/safe-settings`, `custom-prefix` (all work!) +- Do NOT end with `/` +- Set to empty string or `/` for root path deployment + +### 2. Rebuild the UI + +After changing the SAFE_SETTINGS_HUB_URL_PREFIX, you must rebuild the Next.js UI: + +```bash +cd ui +npm run build +cd .. +``` + +### 3. Restart the application + +```bash +npm start +# or +npm run dev +``` + +## NGINX Configuration Example + +Here's an example NGINX configuration for routing requests to Safe Settings at `/safe-settings`: + +Make sure to set `SAFE_SETTINGS_HUB_URL_PREFIX=/safe-settings` in your `.env` file before starting the application. + +```nginx +server { + listen 80; + server_name your-domain.com; + + # Route /safe-settings to Safe Settings application + location /safe-settings { + proxy_pass http://localhost:3000/safe-settings; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Route other paths to different applications + location /other-app { + proxy_pass http://localhost:4000; + # ... other proxy settings + } +} +``` + +## How It Works + +The SAFE_SETTINGS_HUB_URL_PREFIX configuration affects three layers: + +1. **Backend Routing** (`lib/routes.js`): Express router is mounted at the SAFE_SETTINGS_HUB_URL_PREFIX instead of root +2. **Next.js Configuration** (`ui/next.config.js`): The `basePath` setting tells Next.js to generate assets with the correct URL prefix +3. **Frontend Links & API Calls** (`ui/src/app/**`): Navigation links and API fetch calls use the `withBasePath()` utility to prepend the URL prefix + +All API endpoints in the frontend components (`EnvVariables.jsx`, `OrganizationsTable.jsx`, `Safe-settings-hubContent.jsx`, `HubOrgGraph.jsx`) have been updated to use `withBasePath()` for proper routing. + +## Testing Locally + +To test the default SAFE_SETTINGS_HUB_URL_PREFIX locally without NGINX: + +1. No configuration needed (defaults to `/safe-settings`) +2. Build UI: `cd ui && npm run build && cd ..` +3. Start app: `npm run dev` +4. Access at: `http://localhost:3000/safe-settings/dashboard` + +To test a custom SAFE_SETTINGS_HUB_URL_PREFIX: + +1. Set `SAFE_SETTINGS_HUB_URL_PREFIX=/your-path` in `.env` +2. Rebuild UI: `cd ui && npm run build && cd ..` +3. Start app: `npm run dev` +4. Access at: `http://localhost:3000/your-path/dashboard` + +## Troubleshooting + +### Assets not loading +- Make sure you rebuilt the UI after changing SAFE_SETTINGS_HUB_URL_PREFIX +- Check browser console for 404 errors +- Verify NGINX is correctly proxying all paths under the URL prefix + +### API calls failing +- Ensure your proxy passes the full path including SAFE_SETTINGS_HUB_URL_PREFIX +- Check that relative API URLs are being used (not absolute URLs) + +### Navigation broken +- Verify all `` tags use `withBasePath()` utility +- Check that `pathname` comparisons account for the URL prefix + +## Deploying at Root Path + +To deploy at root path instead of the default `/safe-settings`: + +1. Set `SAFE_SETTINGS_HUB_URL_PREFIX=` (empty string) in `.env` +2. Rebuild UI: `cd ui && npm run build && cd ..` +3. Restart application +4. Access at: `http://localhost:3000/dashboard` diff --git a/lib/env.js b/lib/env.js index 8bea61a73..91cbb5984 100644 --- a/lib/env.js +++ b/lib/env.js @@ -12,5 +12,11 @@ module.exports = { CREATE_PR_COMMENT: process.env.CREATE_PR_COMMENT || 'true', CREATE_ERROR_ISSUE: process.env.CREATE_ERROR_ISSUE || 'true', BLOCK_REPO_RENAME_BY_HUMAN: process.env.BLOCK_REPO_RENAME_BY_HUMAN || 'false', - FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true' + FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true', + SAFE_SETTINGS_HUB_URL_PREFIX: (() => { + const prefix = process.env.SAFE_SETTINGS_HUB_URL_PREFIX || '/safe-settings' + // Normalize: add leading '/' if missing, treat '/' as empty string for root path + if (!prefix || prefix === '/') return '' + return prefix.startsWith('/') ? prefix : `/${prefix}` + })() } diff --git a/lib/routes.js b/lib/routes.js index e85f99ca8..5499ebfbe 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -38,8 +38,12 @@ function setCachedCommitMeta (key, meta) { } function setupRoutes (robot, getRouter) { + // Support for URL prefix (e.g., /safe-settings) when behind a proxy + const basePath = env.SAFE_SETTINGS_HUB_URL_PREFIX || '' + const mountPath = basePath || '/' + // Root-level mount - const router = getRouter('/') + const router = getRouter(mountPath) // Ensure JSON/urlencoded body parsing is enabled for API endpoints router.use(express.json({ limit: '1mb' })) @@ -593,9 +597,35 @@ function setupRoutes (robot, getRouter) { try { // Define a blacklist of sensitive environment variable keys to exclude const ENV_BLACKLIST = ['PRIVATE_KEY_PATH']; + + // Parse .env file to extract comments + const envFilePath = path.join(rootDir, '.env'); + const commentMap = new Map(); + + try { + const envFileContent = fs.readFileSync(envFilePath, 'utf8'); + const lines = envFileContent.split(/\r?\n/); + + lines.forEach(line => { + // Match lines like: KEY=value # comment + const match = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*[^#]*#\s*(.+)$/); + if (match) { + const [, key, comment] = match; + commentMap.set(key, comment.trim()); + } + }); + } catch (e) { + // If .env file doesn't exist or can't be read, continue without comments + robot.log && robot.log.debug && robot.log.debug('Could not read .env file for comments:', e.message); + } + const variables = Object.entries(env) .filter(([key]) => !ENV_BLACKLIST.includes(key)) - .map(([key, value]) => ({ key, value })) + .map(([key, value]) => ({ + key, + value, + description: commentMap.get(key) || 'NA' + })) .sort((a, b) => a.key.localeCompare(b.key)); return res.json({ updatedAt: new Date().toISOString(), count: variables.length, variables }); } catch (e) { diff --git a/package-lock.json b/package-lock.json index 000a563ef..ca4330887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "@apidevtools/json-schema-ref-parser": "^12.0.2", "@octokit/auth-app": "^8.0.2", "@probot/adapter-aws-lambda-serverless": "^4.0.3", + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", "deepmerge": "^4.3.1", "eta": "^3.5.0", "js-yaml": "^4.1.0", @@ -22,6 +25,7 @@ "octokit": "^5.0.2", "probot": "^13.4.4", "proxy-from-env": "^1.1.0", + "swr": "^2.4.1", "undici": "^7.7.0" }, "devDependencies": { @@ -2286,19 +2290,6 @@ "node": ">= 10" } }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3972,16 +3963,6 @@ "@opentelemetry/api": "^1.1.0" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, "node_modules/@prisma/instrumentation": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz", @@ -4838,13 +4819,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5588,16 +5562,6 @@ "node": ">=18" } }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5643,13 +5607,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -5795,6 +5752,69 @@ "node": ">=18" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -5996,6 +6016,15 @@ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -6024,17 +6053,6 @@ "node": ">=8" } }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -7509,24 +7527,6 @@ "node": ">= 6" } }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13996,54 +13996,6 @@ } } }, - "node_modules/superagent": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", - "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.1", - "cookiejar": "^2.1.4", - "debug": "^4.3.7", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.4", - "formidable": "^3.5.4", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.2" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/superagent/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/supertest": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", - "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^10.2.3" - }, - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -14067,6 +14019,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14513,6 +14478,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index 503f43fbc..7fea27b0e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "@apidevtools/json-schema-ref-parser": "^12.0.2", "@octokit/auth-app": "^8.0.2", "@probot/adapter-aws-lambda-serverless": "^4.0.3", + "d3-drag": "^3.0.0", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", "deepmerge": "^4.3.1", "eta": "^3.5.0", "js-yaml": "^4.1.0", @@ -40,6 +43,7 @@ "octokit": "^5.0.2", "probot": "^13.4.4", "proxy-from-env": "^1.1.0", + "swr": "^2.4.1", "undici": "^7.7.0" }, "devDependencies": { @@ -94,4 +98,4 @@ "." ] } -} \ No newline at end of file +} diff --git a/test/unit/lib/routes.test.js b/test/unit/lib/routes.test.js index 1c5427b61..0ad5a20ef 100644 --- a/test/unit/lib/routes.test.js +++ b/test/unit/lib/routes.test.js @@ -112,6 +112,12 @@ describe('GET /api/safe-settings/app/env', () => { // Should return 13 variables expect(res.body.count).toBe(13); expect(res.body.variables.length).toBe(13); + // Each variable should have key, value, and description + res.body.variables.forEach(v => { + expect(v).toHaveProperty('key'); + expect(v).toHaveProperty('value'); + expect(v).toHaveProperty('description'); + }); }); }); diff --git a/ui/next.config.js b/ui/next.config.js index 428ac9ab8..bcf5fa482 100644 --- a/ui/next.config.js +++ b/ui/next.config.js @@ -1,6 +1,15 @@ +// Normalize URL prefix: add leading '/' if missing, treat '/' as empty for root +const normalizePrefix = (prefix) => { + if (!prefix || prefix === '/') return ''; + return prefix.startsWith('/') ? prefix : `/${prefix}`; +}; + +const basePath = normalizePrefix(process.env.SAFE_SETTINGS_HUB_URL_PREFIX || '/safe-settings'); + const nextConfig = { output: "export", + basePath: basePath, // Disable Next.js ESLint checks during builds eslint: { ignoreDuringBuilds: true, diff --git a/ui/src/app/components/EnvVariables.jsx b/ui/src/app/components/EnvVariables.jsx index aab69cfe1..c68a0405d 100644 --- a/ui/src/app/components/EnvVariables.jsx +++ b/ui/src/app/components/EnvVariables.jsx @@ -1,7 +1,8 @@ 'use client'; -import React, { useEffect, useState, useMemo } from 'react'; -import { SearchIcon, SyncIcon, EyeClosedIcon, EyeIcon, ShieldIcon, CopyIcon, ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react'; +import { ChevronDownIcon, ChevronUpIcon, CopyIcon, SearchIcon, ShieldIcon } from '@primer/octicons-react'; +import { useEffect, useMemo, useState } from 'react'; import { useHydrated } from '../hooks/useHydrated'; +import { withBasePath } from '../utils/basePath'; const SENSITIVE_REGEX = /(secret|token|key|password|private)/i; @@ -19,7 +20,7 @@ export default function EnvVariables() { const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - fetch(`/api/safe-settings/app/env${includeInfra ? '?includeInfra=true' : ''}`) + fetch(withBasePath(`/api/safe-settings/app/env${includeInfra ? '?includeInfra=true' : ''}`)) .then(r => { if (!r.ok) { throw new Error(`Unable to retrieve environment variables (HTTP ${r.status}). Please try again later.`); @@ -39,7 +40,11 @@ export default function EnvVariables() { const filtered = useMemo(() => { if (!search) return rows; const q = search.toLowerCase(); - return rows.filter(r => r.key.toLowerCase().includes(q) || (r.value + '').toLowerCase().includes(q)); + return rows.filter(r => + r.key.toLowerCase().includes(q) || + (r.value + '').toLowerCase().includes(q) || + (r.description || '').toLowerCase().includes(q) + ); }, [rows, search]); const sorted = useMemo(() => { @@ -95,7 +100,7 @@ export default function EnvVariables() {
    - setSearch(e.target.value)} /> + setSearch(e.target.value)} />
    {/* Removed options and buttons section for a cleaner environment page UI */} @@ -110,8 +115,9 @@ export default function EnvVariables() { - - + + + @@ -122,9 +128,10 @@ export default function EnvVariables() { return ( - +
    cycleSort('key')} className="theme-text-primary user-select-none" style={{ width: '28%', cursor: 'pointer' }}>Key {renderSortIcon('key')} cycleSort('value')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Value {renderSortIcon('value')} cycleSort('key')} className="theme-text-primary user-select-none" style={{ width: '20%', cursor: 'pointer' }}>Key {renderSortIcon('key')} cycleSort('value')} className="theme-text-primary user-select-none" style={{ width: '30%', cursor: 'pointer' }}>Value {renderSortIcon('value')} cycleSort('description')} className="theme-text-primary user-select-none" style={{ cursor: 'pointer' }}>Description {renderSortIcon('description')}
    {r.key} + {maskedValue(r.key, r.value)} {r.description || 'NA'} {sensitive && } diff --git a/ui/src/app/components/HubOrgGraph.jsx b/ui/src/app/components/HubOrgGraph.jsx index bd3b42e3b..201259323 100644 --- a/ui/src/app/components/HubOrgGraph.jsx +++ b/ui/src/app/components/HubOrgGraph.jsx @@ -1,12 +1,13 @@ 'use client'; import { useEffect, useRef } from "react"; import useSWR from "swr"; +import { withBasePath } from "../utils/basePath"; const fetcher = (...args) => fetch(...args).then(res => res.json()); export default function HubOrgGraph({ width = 640, height = 320 }) { const vizRef = useRef(null); - const { data, error } = useSWR("/api/safe-settings/installation", fetcher); + const { data, error } = useSWR(withBasePath("/api/safe-settings/installation"), fetcher); const orgs = Array.isArray(data?.installations) ? data.installations.filter(i => i.type === "Organization") : []; diff --git a/ui/src/app/components/OrganizationsTable.jsx b/ui/src/app/components/OrganizationsTable.jsx index f6e394ae5..4a8f621b0 100644 --- a/ui/src/app/components/OrganizationsTable.jsx +++ b/ui/src/app/components/OrganizationsTable.jsx @@ -1,13 +1,14 @@ "use client"; -import React, { useState, useMemo, useEffect, useRef } from "react"; import { - ChevronUpIcon, - ChevronDownIcon, - SearchIcon, - InfoIcon, + ChevronDownIcon, + ChevronUpIcon, + InfoIcon, + SearchIcon, } from "@primer/octicons-react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useHydrated } from "../hooks/useHydrated"; +import { withBasePath } from "../utils/basePath"; // Mock organizations used when /api/safe-settings/installation returns 404 const MOCK_ORGS = [ @@ -58,7 +59,7 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { let cancelled = false; setLoading(true); - fetch("/api/safe-settings/installation") + fetch(withBasePath("/api/safe-settings/installation")) .then((r) => { if (!r.ok) { throw new Error( @@ -249,7 +250,7 @@ const OrganizationsTable = ({ organizations: propOrganizations = [] }) => { setRetrieveError(null); setRetrievingFiles(true); try { - const res = await fetch("/api/safe-settings/hub/import", { + const res = await fetch(withBasePath("/api/safe-settings/hub/import"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ orgs: orgNames }), diff --git a/ui/src/app/components/Safe-settings-hubContent.jsx b/ui/src/app/components/Safe-settings-hubContent.jsx index ada553614..0c0610890 100644 --- a/ui/src/app/components/Safe-settings-hubContent.jsx +++ b/ui/src/app/components/Safe-settings-hubContent.jsx @@ -1,7 +1,8 @@ 'use client'; -import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { SearchIcon, FileIcon, FileDirectoryIcon, ChevronDownIcon, ChevronRightIcon } from '@primer/octicons-react'; +import { ChevronDownIcon, ChevronRightIcon, FileDirectoryIcon, FileIcon, SearchIcon } from '@primer/octicons-react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useHydrated } from '../hooks/useHydrated'; +import { withBasePath } from '../utils/basePath'; // Match the left index width and reuse for the search input const LEFT_COL_WIDTH = 320; @@ -20,7 +21,7 @@ export default function SafeSettingsHubContent3b() { const fetchData = () => { if (!hydrated) return; setLoading(true); setError(null); - fetch('/api/safe-settings/hub/content?fetchContent=true') + fetch(withBasePath('/api/safe-settings/hub/content?fetchContent=true')) .then(r => { if (!r.ok) throw new Error(`Unable to retrieve safe-settings hub content (HTTP ${r.status})`); return r.json(); diff --git a/ui/src/app/components/TitleBar.jsx b/ui/src/app/components/TitleBar.jsx index 893537339..f3751ca8f 100644 --- a/ui/src/app/components/TitleBar.jsx +++ b/ui/src/app/components/TitleBar.jsx @@ -1,14 +1,14 @@ "use client"; -import { usePathname } from "next/navigation"; -import React from "react"; import { - GlobeIcon, - GearIcon, - ListUnorderedIcon, - SunIcon, - MoonIcon, - NoteIcon, + GearIcon, + GlobeIcon, + ListUnorderedIcon, + MoonIcon, + NoteIcon, + SunIcon, } from "@primer/octicons-react"; +import { usePathname } from "next/navigation"; +import { withBasePath } from "../utils/basePath"; import { useTheme } from "./ThemeContext"; import "./TitleBar.css"; @@ -37,7 +37,7 @@ export default function TitleBar() { @@ -65,7 +65,7 @@ export default function TitleBar() { className={`nav-link fw-light d-flex align-items-center position-relative menu-hover nav-link-custom${ isDark ? " dark-font" : " light-font" }`} - href="/dashboard/safe-settings-hub" + href={withBasePath("/dashboard/safe-settings-hub")} > @@ -81,7 +81,7 @@ export default function TitleBar() { className={`nav-link fw-light d-flex align-items-center position-relative menu-hover nav-link-custom${ isDark ? " dark-font" : " light-font" }`} - href="/dashboard/organizations" + href={withBasePath("/dashboard/organizations")} > @@ -97,7 +97,7 @@ export default function TitleBar() { className={`nav-link fw-light d-flex align-items-center position-relative menu-hover nav-link-custom${ isDark ? " dark-font" : " light-font" }`} - href="/dashboard/env" + href={withBasePath("/dashboard/env")} > @@ -113,7 +113,7 @@ export default function TitleBar() { className={`btn btn-sm ms-auto nav-link fw-light d-flex position-relative menu-hover nav-link-custom${ isDark ? " dark-font" : " light-font" }`} - href="/dashboard/help" + href={withBasePath("/dashboard/help")} > @@ -129,7 +129,7 @@ export default function TitleBar() { className={`btn btn-sm ms-auto nav-link fw-light d-flex position-relative menu-hover nav-link-custom${ isDark ? " dark-font" : " light-font" }`} - href="/dashboard/logs" + href={withBasePath("/dashboard/logs")} > diff --git a/ui/src/app/not-found.jsx b/ui/src/app/not-found.jsx index da79f865d..1ced869a6 100644 --- a/ui/src/app/not-found.jsx +++ b/ui/src/app/not-found.jsx @@ -1,5 +1,6 @@ "use client"; import TitleBar from "./components/TitleBar"; +import { withBasePath } from "./utils/basePath"; export default function NotFound() { return ( @@ -8,7 +9,7 @@ export default function NotFound() {

    404

    Sorry, the page you are looking for does not exist.

    -
    Go to Dashboard + Go to Dashboard
    ); diff --git a/ui/src/app/utils/basePath.js b/ui/src/app/utils/basePath.js new file mode 100644 index 000000000..ac223a054 --- /dev/null +++ b/ui/src/app/utils/basePath.js @@ -0,0 +1,36 @@ +/** + * URL prefix utility for handling deployment behind proxies + * Reads from Next.js basePath configuration + */ + +// Normalize URL prefix: add leading '/' if missing, treat '/' as empty for root +const normalizePrefix = (prefix) => { + if (!prefix || prefix === '/') return ''; + return prefix.startsWith('/') ? prefix : `/${prefix}`; +}; + +// Next.js automatically handles basePath for routing and asset loading +// We expose this for manual link construction (URL prefix in browser) +export const BASE_PATH = typeof window !== 'undefined' + ? (window.__NEXT_DATA__?.basePath || '/safe-settings') + : normalizePrefix(process.env.NEXT_PUBLIC_SAFE_SETTINGS_HUB_URL_PREFIX || process.env.SAFE_SETTINGS_HUB_URL_PREFIX || '/safe-settings'); + +/** + * Prepend base path to a URL + * Note: Next.js Link component and router.push already handle basePath automatically + * This is primarily for tags and manual URL construction + * @param {string} path - The path to prepend base path to + * @returns {string} The full path with base path + */ +export function withBasePath(path) { + // Ensure path starts with / + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + + // If no base path, return as-is + if (!BASE_PATH) return normalizedPath; + + // Remove trailing slash from base path if present + const cleanBasePath = BASE_PATH.endsWith('/') ? BASE_PATH.slice(0, -1) : BASE_PATH; + + return `${cleanBasePath}${normalizedPath}`; +} From bfbd874dedef71c84060cff09dd8fdc14b00fe37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Mon, 1 Jun 2026 14:57:37 -0400 Subject: [PATCH 21/22] updated docs --- docs/hubSyncHandler/README.md | 65 +++++++ hubSyncHandler.log | 318 +++++++++++++++++----------------- 2 files changed, 224 insertions(+), 159 deletions(-) diff --git a/docs/hubSyncHandler/README.md b/docs/hubSyncHandler/README.md index 1f35935c9..ab772122d 100644 --- a/docs/hubSyncHandler/README.md +++ b/docs/hubSyncHandler/README.md @@ -10,6 +10,8 @@ One central **master admin repository** (the hub) serves as the authoritative so **Note:** When something changes in the 'Master' repo (the hub), only those changed files are copied to each affected ORG’s admin repo, so everything stays in sync. +#### :warning: In order for `Safe-Settings Hub-Sync` to enforce Policy sync in the controlled ORGs/Repos, **Safe-Settings** needs to be given **Bypass authority** for any branch protection Ruleset that prevents 'direct pushes'. + ## Sync Lifecycle (High Level) ```mermaid @@ -20,6 +22,69 @@ A1(HUB Admin Repo) --> C(ORG Admin Repo) A1(HUB Admin Repo) --> D(ORG Admin Repo) ``` +## Initial Setup + +Before getting started with the hub-sync configuration, you'll need to deploy and build the Safe Settings App, including the UI. + +### Deploy the Application + +To deploy the Safe Settings application to a hosting environment, choose one of the following deployment options: + +#### Option 1: Virtual Machine (VM) +Deploy Safe Settings on a VM (AWS EC2, Azure VM, Google Compute Engine, etc.): + +1. Set up a VM with Node.js installed +2. Clone the Safe Settings repository to the VM +3. Copy your built UI files to the VM +4. Configure environment variables (see [Environment Variables](#environment-variables--inputs-specific-to-the-hub-sync-feature)) +5. Start the application: + ```bash + npm start + ``` + + :warning: Follow the standard GitHub Probot App [deployment steps](https://probot.github.io/docs/deployment/) + +#### Option 2: Container Deployment +Deploy using Docker: + +1. Use the provided `Dockerfile` or `docker-compose.yml` +2. Build and run the container: + ```bash + docker-compose up -d + ``` + +#### Option 3: Serverless Deployment +Deploy to AWS Lambda or similar serverless platforms using the provided `serverless.yml` configuration. See [AWS deployment documentation](../AWS-README.md) for details. + +#### Option 4: Kubernetes +Deploy to a Kubernetes cluster using the provided Helm charts in the `helm/safe-settings/` directory. + +**Note:** Ensure your deployment environment has network access to GitHub APIs and can receive webhook events from GitHub. + + +### Build the UI + +The Safe Settings dashboard includes a Next.js-based UI that must be compiled before deployment. + +1. Navigate to the UI directory: + ```bash + cd safe-settings/ui/ + ``` + +2. Install dependencies (if not already done): + ```bash + npm install + ``` + +3. Build the Next.js application: + ```bash + npm run build + ``` + +This creates an optimized production build of the dashboard UI that will be served by the Safe Settings application. + +--- + ## Gettings Started >**Note:** for the standard setup lets assume that Safe-Settings configuration on the Admin Config Repos (Spokes) are stored in `.github/` diff --git a/hubSyncHandler.log b/hubSyncHandler.log index 141449883..661cc6625 100644 --- a/hubSyncHandler.log +++ b/hubSyncHandler.log @@ -1,162 +1,3 @@ -2026-05-06T21:33:39.184Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:39.184Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-159 -2026-05-06T21:33:39.184Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-159 -2026-05-06T21:33:39.520Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:39.520Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:40.568Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:40.568Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:41.153Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:41.153Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-160 -2026-05-06T21:33:41.153Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-160 -2026-05-06T21:33:41.311Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:41.310Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:42.592Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:42.592Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:43.244Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:43.245Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-161 -2026-05-06T21:33:43.245Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-161 -2026-05-06T21:33:43.401Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:43.401Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:44.679Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:44.679Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:45.190Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-162 -2026-05-06T21:33:45.190Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-162 -2026-05-06T21:33:45.190Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:45.310Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:45.310Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:46.540Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:46.540Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:47.174Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-163 -2026-05-06T21:33:47.175Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-163 -2026-05-06T21:33:47.174Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:47.288Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:47.288Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:48.525Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:48.525Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:49.183Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-164 -2026-05-06T21:33:49.183Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-164 -2026-05-06T21:33:49.183Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:49.463Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:49.463Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:50.442Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:50.442Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:51.134Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:51.134Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-166 -2026-05-06T21:33:51.134Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-166 -2026-05-06T21:33:51.400Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:51.400Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:52.528Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:52.528Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:53.073Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-168 -2026-05-06T21:33:53.073Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-168 -2026-05-06T21:33:53.073Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:53.418Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:53.418Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:54.559Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:54.559Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:55.325Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:55.325Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-169 -2026-05-06T21:33:55.325Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-169 -2026-05-06T21:33:55.430Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:55.430Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:56.585Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:56.585Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:57.375Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-167 -2026-05-06T21:33:57.375Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:57.375Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-167 -2026-05-06T21:33:57.716Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:57.716Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:58.535Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:58.535Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:33:59.175Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-165 -2026-05-06T21:33:59.175Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:33:59.175Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-165 -2026-05-06T21:33:59.404Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:33:59.404Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:34:00.506Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:34:00.506Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:34:00.690Z [TRACE] Fetching installations -2026-05-06T21:34:00.690Z [DEBUG] running a task every minute -2026-05-06T21:34:01.031Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} -2026-05-06T21:34:01.125Z [DEBUG] Found repo with security settings {"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true} -2026-05-06T21:34:01.125Z [DEBUG] Enabling Dependabot alerts for owner: jefeish-test and repo repo-170 -2026-05-06T21:34:01.125Z [DEBUG] Enabling Dependabot security updates for owner: jefeish-test and repo repo-170 -2026-05-06T21:34:01.308Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} -2026-05-06T21:34:01.308Z [DEBUG] In getSubOrgConfigMap {"repo":"safe-settings-config","owner":"jefeish-test1"} -2026-05-06T21:34:01.308Z [DEBUG] In loadConfigMap {"owner":"jefeish-test1","repo":"safe-settings-config","path":".github/suborgs"} -2026-05-06T21:34:01.325Z [DEBUG] Repository Edited by a Bot -2026-05-06T21:34:01.325Z [DEBUG] repository.edited payload from {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} -2026-05-06T21:34:01.567Z [DEBUG] Error getting settings {"owner":"jefeish-test1","repo":"safe-settings-config","path":".github/suborgs"} HttpError: Not Found - https://docs.github.com/rest/repos/contents#get-repository-content -2026-05-06T21:34:01.567Z [DEBUG] In getRepoConfigMap {"repo":"safe-settings-config","owner":"jefeish-test1"} -2026-05-06T21:34:01.801Z [DEBUG] repo configs = {} -2026-05-06T21:34:01.802Z [DEBUG] { - method: 'GET', - baseUrl: 'https://api.github.com', - headers: { - accept: 'application/vnd.github.v3+json', - 'user-agent': 'probot/13.4.4 octokit-core.js/5.2.0 Node.js/23', - 'x-github-api-version': '2022-11-28' - }, - mediaType: { format: '', previews: [] }, - request: { hook: [Function: bound bound register], retryCount: 1 }, - url: '/orgs/{org}/rulesets', - org: 'jefeish-test1' -} -2026-05-06T21:34:01.801Z [DEBUG] No repos directory in the safe-settings-config/.github -2026-05-06T21:34:01.802Z [DEBUG] Getting all rulesets for the org undefined -2026-05-06T21:34:02.123Z [DEBUG] [] - - [ - { - "name": "Template", - "target": "branch", - "enforcement": "active", - "bypass_actors": [ - { - "actor_id": 1, - "actor_type": "OrganizationAdmin", - "bypass_mode": "always" - }, - { - "actor_id": 7898, - "actor_type": "RepositoryRole", - "bypass_mode": "always" - }, - { - "actor_id": 210920, - "actor_type": "Integration", - "bypass_mode": "always" - } - ], - "conditions": { - "ref_name": { - "include": [ - "~DEFAULT_BRANCH" - ], - "exclude": [ - "refs/heads/oldmaster" - ] - }, - "repository_name": { - "include": [ - "test*" - ], - "exclude": [ - "test", - "test1" - ], - "protected": true - } - }, - "rules": [ - { - "type": "creation" - }, - { - "type": "update", - "parameters": { - "update_allows_fetch_and_merge": true - } }, { "type": "deletion" @@ -998,3 +839,162 @@ 2026-05-06T21:44:43.619Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found 2026-05-06T21:44:45.456Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found 2026-05-06T21:44:49.024Z [WARN] Hub repository jefeish-training/safe-settings-config-master or branch 'main' not found +2026-06-01T18:26:42.016Z [INFO] Received 'pull_request.closed' event: 53 +2026-06-01T18:26:42.017Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2026-06-01T18:26:42.190Z [DEBUG] Is Admin repo event false +2026-06-01T18:26:42.190Z [DEBUG] Not working on the Admin repo, returning... +2026-06-01T18:26:42.906Z [INFO] Files changed in PR #53: .github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:26:42.907Z [INFO] Organization: jefeish-migration-test, Destination Repo: safe-settings-config, Destination Folder: .github +2026-06-01T18:26:42.906Z [INFO] Orgs updated in PR #53: jefeish-migration-test +2026-06-01T18:26:42.906Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). +2026-06-01T18:26:42.907Z [INFO] Syncing safe settings for organization: jefeish-migration-test +2026-06-01T18:26:42.907Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' +2026-06-01T18:26:42.907Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2026-06-01T18:26:43.380Z [INFO] DEBUG: PR #53 contains 1 changed file(s) +2026-06-01T18:26:43.380Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:26:43.380Z [INFO] DEBUG: first file object = { + "sha": "6928a64c36d64aeb96da968a8d250dfe08315435", + "filename": ".github/safe-settings/organizations/jefeish-migration-test/settings.yml", + "status": "modified", + "additions": 1, + "deletions": 0, + "changes": 1, + "blob_url": "https://github.com/jefeish-training/safe-settings-config-master/blob/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml", + "raw_url": "https://github.com/jefeish-training/safe-settings-config-master/raw/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml", + "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=c01de93ada8652a04aa58af10f5de44ebf4b8f84", + "patch": "@@ -400,3 +400,4 @@ rulesets:\n \n # --- test 1\n # --- test 2\n+# --- test 3" +} +2026-06-01T18:26:43.380Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2026-06-01T18:26:43.381Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:26:43.380Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 +2026-06-01T18:26:43.380Z [INFO] DEBUG: FILE[0] raw={"sha":"6928a64c36d64aeb96da968a8d250dfe08315435","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=c01de93ada8652a04aa58af10f5de44ebf4b8f84","patch":"@@ -400,3 +400,4 @@ rulesets:\n \n # --- test 1\n # --- test 2\n+# --- test 3"} +2026-06-01T18:26:43.381Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-migration-test +2026-06-01T18:26:43.381Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-migration-test +2026-06-01T18:26:43.691Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #53 to jefeish-migration-test/safe-settings-config@main under .github (directPush=true) +2026-06-01T18:27:03.334Z [ERROR] Failed to sync file .github/settings.yml: Could not update file: Changes must be made through a pull request. - https://docs.github.com/articles/about-protected-branches +2026-06-01T18:27:03.334Z [ERROR] syncSafeSettingConfig error for org jefeish-migration-test: Could not update file: Changes must be made through a pull request. - https://docs.github.com/articles/about-protected-branches +2026-06-01T18:28:42.190Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2026-06-01T18:28:42.190Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2026-06-01T18:28:42.190Z [DEBUG] Branch Protection edited by a Human +2026-06-01T18:28:42.837Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"ruleset-policyset","value":"standard"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"baseline-main-protection","target":"branch","enforcement":"active","conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"]}},"rules":[{"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"ci/build"}]}}]},{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2026-06-01T18:28:53.243Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:28:53.243Z [DEBUG] Branch Protection edited by Bot +2026-06-01T18:28:55.626Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:28:55.626Z [DEBUG] Repository member edited by Bot +2026-06-01T18:28:56.621Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:28:56.621Z [DEBUG] Repository member edited by Bot +2026-06-01T18:28:57.527Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:28:57.527Z [DEBUG] Repository member edited by Bot +2026-06-01T18:28:58.598Z [DEBUG] Repository member edited by Bot +2026-06-01T18:28:58.598Z [DEBUG] Repository member edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:29:00.008Z [DEBUG] Check run was created! +2026-06-01T18:29:00.008Z [DEBUG] Not triggered by Safe-settings... +2026-06-01T18:29:54.373Z [DEBUG] Is Admin repo event false +2026-06-01T18:29:54.373Z [DEBUG] Not working on the Admin repo, returning... +2026-06-01T18:30:29.539Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2026-06-01T18:30:29.539Z [DEBUG] Branch Protection edited by a Human +2026-06-01T18:30:29.539Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2026-06-01T18:30:29.890Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"ruleset-policyset","value":"standard"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"baseline-main-protection","target":"branch","enforcement":"active","conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"]}},"rules":[{"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"ci/build"}]}}]},{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2026-06-01T18:30:37.865Z [DEBUG] Not working on the Admin repo, returning... +2026-06-01T18:30:37.865Z [DEBUG] Is Admin repo event false +2026-06-01T18:30:37.865Z [DEBUG] Pull_request opened ! +2026-06-01T18:30:41.469Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:30:41.469Z [DEBUG] Branch Protection edited by Bot +2026-06-01T18:30:42.794Z [DEBUG] Not triggered by Safe-settings... +2026-06-01T18:30:42.794Z [DEBUG] Check run was created! +2026-06-01T18:30:58.015Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2026-06-01T18:30:58.015Z [DEBUG] Branch Protection edited by {"login":"jefeish_fabrikam","id":90713677,"node_id":"MDQ6VXNlcjkwNzEzNjc3","avatar_url":"https://avatars.githubusercontent.com/u/90713677?v=4","gravatar_id":"","url":"https://api.github.com/users/jefeish_fabrikam","html_url":"https://github.com/jefeish_fabrikam","followers_url":"https://api.github.com/users/jefeish_fabrikam/followers","following_url":"https://api.github.com/users/jefeish_fabrikam/following{/other_user}","gists_url":"https://api.github.com/users/jefeish_fabrikam/gists{/gist_id}","starred_url":"https://api.github.com/users/jefeish_fabrikam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefeish_fabrikam/subscriptions","organizations_url":"https://api.github.com/users/jefeish_fabrikam/orgs","repos_url":"https://api.github.com/users/jefeish_fabrikam/repos","events_url":"https://api.github.com/users/jefeish_fabrikam/events{/privacy}","received_events_url":"https://api.github.com/users/jefeish_fabrikam/received_events","type":"User","user_view_type":"public","site_admin":false} +2026-06-01T18:30:58.015Z [DEBUG] Branch Protection edited by a Human +2026-06-01T18:30:58.443Z [DEBUG] config for ref undefined is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"ruleset-policyset","value":"standard"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"baseline-main-protection","target":"branch","enforcement":"active","conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"]}},"rules":[{"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"ci/build"}]}}]},{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2026-06-01T18:31:09.996Z [DEBUG] Branch Protection edited by {"login":"fabrikam-safe-settings[bot]","id":223158109,"node_id":"BOT_kgDODU0fXQ","avatar_url":"https://avatars.githubusercontent.com/in/1680061?v=4","gravatar_id":"","url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D","html_url":"https://github.com/apps/fabrikam-safe-settings","followers_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/followers","following_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/repos","events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/fabrikam-safe-settings%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false} +2026-06-01T18:31:09.996Z [DEBUG] Branch Protection edited by Bot +2026-06-01T18:31:10.276Z [INFO] Received 'pull_request.closed' event: 54 +2026-06-01T18:31:10.277Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2026-06-01T18:31:10.724Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). +2026-06-01T18:31:10.725Z [INFO] Organization: jefeish-migration-test, Destination Repo: safe-settings-config, Destination Folder: .github +2026-06-01T18:31:10.725Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' +2026-06-01T18:31:10.725Z [INFO] Syncing safe settings for organization: jefeish-migration-test +2026-06-01T18:31:10.724Z [INFO] Files changed in PR #54: .github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:31:10.724Z [INFO] Orgs updated in PR #54: jefeish-migration-test +2026-06-01T18:31:10.725Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2026-06-01T18:31:11.118Z [INFO] DEBUG: PR #54 contains 1 changed file(s) +2026-06-01T18:31:11.118Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:31:11.118Z [INFO] DEBUG: first file object = { + "sha": "4886a71655233cb557464ed47ce306079bbd170d", + "filename": ".github/safe-settings/organizations/jefeish-migration-test/settings.yml", + "status": "modified", + "additions": 0, + "deletions": 1, + "changes": 1, + "blob_url": "https://github.com/jefeish-training/safe-settings-config-master/blob/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml", + "raw_url": "https://github.com/jefeish-training/safe-settings-config-master/raw/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml", + "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=daab9b95fb0626f8a0cbace114a6029e93c39636", + "patch": "@@ -400,4 +400,3 @@ rulesets:\n \n # --- test 1\n # --- test 2\n-# --- test 3" +} +2026-06-01T18:31:11.118Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2026-06-01T18:31:11.118Z [INFO] DEBUG: FILE[0] raw={"sha":"4886a71655233cb557464ed47ce306079bbd170d","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":0,"deletions":1,"changes":1,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=daab9b95fb0626f8a0cbace114a6029e93c39636","patch":"@@ -400,4 +400,3 @@ rulesets:\n \n # --- test 1\n # --- test 2\n-# --- test 3"} +2026-06-01T18:31:11.118Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 +2026-06-01T18:31:11.119Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:31:11.119Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-migration-test +2026-06-01T18:31:11.119Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-migration-test +2026-06-01T18:31:11.364Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #54 to jefeish-migration-test/safe-settings-config@main under .github (directPush=true) +2026-06-01T18:31:11.529Z [DEBUG] Is Admin repo event false +2026-06-01T18:31:11.529Z [DEBUG] Not working on the Admin repo, returning... +2026-06-01T18:31:11.552Z [DEBUG] Not triggered by Safe-settings... +2026-06-01T18:31:11.552Z [DEBUG] Check run was created! +2026-06-01T18:31:30.416Z [ERROR] Failed to sync file .github/settings.yml: Could not update file: Changes must be made through a pull request. - https://docs.github.com/articles/about-protected-branches +2026-06-01T18:31:30.417Z [ERROR] syncSafeSettingConfig error for org jefeish-migration-test: Could not update file: Changes must be made through a pull request. - https://docs.github.com/articles/about-protected-branches +2026-06-01T18:35:27.019Z [INFO] Received 'pull_request.closed' event: 55 +2026-06-01T18:35:27.019Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) +2026-06-01T18:35:27.673Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). +2026-06-01T18:35:27.674Z [INFO] Organization: jefeish-migration-test, Destination Repo: safe-settings-config, Destination Folder: .github +2026-06-01T18:35:27.673Z [INFO] Files changed in PR #55: .github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:35:27.674Z [INFO] Syncing safe settings for organization: jefeish-migration-test +2026-06-01T18:35:27.673Z [INFO] Orgs updated in PR #55: jefeish-migration-test +2026-06-01T18:35:27.674Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' +2026-06-01T18:35:27.675Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2026-06-01T18:35:27.985Z [DEBUG] Not working on the Admin repo, returning... +2026-06-01T18:35:27.985Z [DEBUG] Is Admin repo event false +2026-06-01T18:35:28.092Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:35:28.093Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2026-06-01T18:35:28.093Z [INFO] DEBUG: FILE[0] raw={"sha":"785f2c88eba8195abb95a25febb1ff099a844516","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=80e54cbb65a2ef75e9da0937604a1f8308f944e7","patch":"@@ -399,4 +399,4 @@ rulesets:\n pattern: \".*\\/.*\"\n \n # --- test 1\n-# --- test 2\n+# --- test 2 "} +2026-06-01T18:35:28.092Z [INFO] DEBUG: PR #55 contains 1 changed file(s) +2026-06-01T18:35:28.093Z [INFO] DEBUG: first file object = { + "sha": "785f2c88eba8195abb95a25febb1ff099a844516", + "filename": ".github/safe-settings/organizations/jefeish-migration-test/settings.yml", + "status": "modified", + "additions": 1, + "deletions": 1, + "changes": 2, + "blob_url": "https://github.com/jefeish-training/safe-settings-config-master/blob/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml", + "raw_url": "https://github.com/jefeish-training/safe-settings-config-master/raw/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml", + "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=80e54cbb65a2ef75e9da0937604a1f8308f944e7", + "patch": "@@ -399,4 +399,4 @@ rulesets:\n pattern: \".*\\/.*\"\n \n # --- test 1\n-# --- test 2\n+# --- test 2 " +} +2026-06-01T18:35:28.093Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 +2026-06-01T18:35:28.093Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:35:28.093Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-migration-test +2026-06-01T18:35:28.093Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-migration-test +2026-06-01T18:35:28.321Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #55 to jefeish-migration-test/safe-settings-config@main under .github (directPush=false) +2026-06-01T18:35:30.051Z [INFO] Created branch safe-settings-sync/pr-55-jefeish-migration-test-1780338928321 in jefeish-migration-test/safe-settings-config +2026-06-01T18:35:31.423Z [DEBUG] Not working on the default branch, returning... +2026-06-01T18:35:31.818Z [INFO] Committed .github/settings.yml to jefeish-migration-test/safe-settings-config@safe-settings-sync/pr-55-jefeish-migration-test-1780338928321 +2026-06-01T18:35:33.062Z [DEBUG] Not working on the default branch, returning... +2026-06-01T18:35:33.428Z [INFO] Created PR https://github.com/jefeish-migration-test/safe-settings-config/pull/1 in jefeish-migration-test/safe-settings-config +2026-06-01T18:35:33.927Z [DEBUG] Is Admin repo event true +2026-06-01T18:35:34.503Z [DEBUG] {"status":201,"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs","headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset, Warning","cache-control":"private, max-age=60, s-maxage=60","content-length":"2694","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Mon, 01 Jun 2026 18:35:34 GMT","etag":"\"52960c7ec0ddea0fe3c1b7b46fb0b9ea236c4f708cac7c0441e96569540ed8ba\"","location":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs/78921692128","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"checks=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"F9BF:6DA66:3EAF2CD:3B3AA10:6A1DD0F5","x-ratelimit-limit":"15000","x-ratelimit-remaining":"14910","x-ratelimit-reset":"1780341659","x-ratelimit-resource":"core","x-ratelimit-used":"90","x-xss-protection":"0"},"data":{"id":78921692128,"name":"Safe-setting validator","node_id":"CR_kwDOPkMZxM8AAAASYBlz4A","head_sha":"9d46a1131e46d4b0df7ad8a3386d6e8caf9e85a7","external_id":"","url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs/78921692128","html_url":"https://github.com/jefeish-migration-test/safe-settings-config/runs/78921692128","details_url":"https://smee.io/06yPGTAkrajW38Z6","status":"queued","conclusion":null,"started_at":"2026-06-01T18:35:34Z","completed_at":null,"output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs/78921692128/annotations"},"check_suite":{"id":71790842907},"app":{"id":1680061,"client_id":"Iv23li3Tul4N7uJql5JN","slug":"fabrikam-safe-settings","node_id":"A_kwPNFp3OABmivQ","owner":{"login":"fabrikam","id":5789,"node_id":"MDEwOkVudGVycHJpc2U1Nzg5","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"","html_url":"","followers_url":"","following_url":"","gists_url":"","starred_url":"","subscriptions_url":"","organizations_url":"","repos_url":"","events_url":"","received_events_url":"","type":"enterprise","site_admin":false},"name":"fabrikam-safe-settings","description":"","external_url":"https://smee.io/06yPGTAkrajW38Z6","html_url":"https://github.com/apps/fabrikam-safe-settings","created_at":"2025-07-27T19:32:59Z","updated_at":"2026-06-01T18:20:15Z","permissions":{"actions_variables":"write","administration":"write","checks":"write","contents":"write","custom_properties_for_organizations":"write","environments":"write","issues":"write","members":"write","metadata":"read","organization_administration":"write","organization_custom_org_roles":"write","organization_custom_properties":"write","organization_custom_roles":"write","pull_requests":"write","repository_projects":"write","single_file":"write"},"events":["branch_protection_rule","check_run","check_suite","custom_property_values","member","pull_request","push","repository","repository_ruleset","team"]},"pull_requests":[{"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/pulls/1","id":3782953273,"number":1,"head":{"ref":"safe-settings-sync/pr-55-jefeish-migration-test-1780338928321","sha":"9d46a1131e46d4b0df7ad8a3386d6e8caf9e85a7","repo":{"id":1044584900,"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config","name":"safe-settings-config"}},"base":{"ref":"main","sha":"b47bc9d94827554acd51d886e5f92daaff10e947","repo":{"id":1044584900,"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config","name":"safe-settings-config"}}}]}} +2026-06-01T18:35:35.183Z [DEBUG] Check run was created! +2026-06-01T18:35:35.183Z [DEBUG] Updating check run {"owner":"jefeish-migration-test","repo":"safe-settings-config","check_run_id":78921692128,"status":"in_progress","started_at":"2026-06-01T18:35:35.183Z","output":{"title":"Starting NOP","summary":"initiating..."}} +2026-06-01T18:35:35.183Z [DEBUG] Is Admin repo event true +2026-06-01T18:35:35.598Z [DEBUG] Pull_request opened ! +2026-06-01T18:35:35.598Z [DEBUG] Is Admin repo event true +2026-06-01T18:35:36.179Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... +2026-06-01T18:35:36.179Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2026-06-01T18:35:36.610Z [DEBUG] config for ref safe-settings-sync/pr-55-jefeish-migration-test-1780338928321 is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} +2026-06-01T18:35:37.144Z [DEBUG] {"status":201,"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs","headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset, Warning","cache-control":"private, max-age=60, s-maxage=60","content-length":"2694","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Mon, 01 Jun 2026 18:35:37 GMT","etag":"\"0a0b09f8d1683bc98c09c8c640a14923bd5cf257c5b2dbba15498c1afd72aa19\"","location":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs/78921699671","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"checks=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"F1FB:11F52:409BCC7:3D019FC:6A1DD0F8","x-ratelimit-limit":"15000","x-ratelimit-remaining":"14906","x-ratelimit-reset":"1780341659","x-ratelimit-resource":"core","x-ratelimit-used":"94","x-xss-protection":"0"},"data":{"id":78921699671,"name":"Safe-setting validator","node_id":"CR_kwDOPkMZxM8AAAASYBmRVw","head_sha":"9d46a1131e46d4b0df7ad8a3386d6e8caf9e85a7","external_id":"","url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs/78921699671","html_url":"https://github.com/jefeish-migration-test/safe-settings-config/runs/78921699671","details_url":"https://smee.io/06yPGTAkrajW38Z6","status":"queued","conclusion":null,"started_at":"2026-06-01T18:35:37Z","completed_at":null,"output":{"title":null,"summary":null,"text":null,"annotations_count":0,"annotations_url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/check-runs/78921699671/annotations"},"check_suite":{"id":71790842907},"app":{"id":1680061,"client_id":"Iv23li3Tul4N7uJql5JN","slug":"fabrikam-safe-settings","node_id":"A_kwPNFp3OABmivQ","owner":{"login":"fabrikam","id":5789,"node_id":"MDEwOkVudGVycHJpc2U1Nzg5","avatar_url":"https://avatars.githubusercontent.com/b/5789?v=4","gravatar_id":"","url":"","html_url":"","followers_url":"","following_url":"","gists_url":"","starred_url":"","subscriptions_url":"","organizations_url":"","repos_url":"","events_url":"","received_events_url":"","type":"enterprise","site_admin":false},"name":"fabrikam-safe-settings","description":"","external_url":"https://smee.io/06yPGTAkrajW38Z6","html_url":"https://github.com/apps/fabrikam-safe-settings","created_at":"2025-07-27T19:32:59Z","updated_at":"2026-06-01T18:20:15Z","permissions":{"actions_variables":"write","administration":"write","checks":"write","contents":"write","custom_properties_for_organizations":"write","environments":"write","issues":"write","members":"write","metadata":"read","organization_administration":"write","organization_custom_org_roles":"write","organization_custom_properties":"write","organization_custom_roles":"write","pull_requests":"write","repository_projects":"write","single_file":"write"},"events":["branch_protection_rule","check_run","check_suite","custom_property_values","member","pull_request","push","repository","repository_ruleset","team"]},"pull_requests":[{"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config/pulls/1","id":3782953273,"number":1,"head":{"ref":"safe-settings-sync/pr-55-jefeish-migration-test-1780338928321","sha":"9d46a1131e46d4b0df7ad8a3386d6e8caf9e85a7","repo":{"id":1044584900,"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config","name":"safe-settings-config"}},"base":{"ref":"main","sha":"b47bc9d94827554acd51d886e5f92daaff10e947","repo":{"id":1044584900,"url":"https://api.github.com/repos/jefeish-migration-test/safe-settings-config","name":"safe-settings-config"}}}]}} +2026-06-01T18:35:37.942Z [DEBUG] Check run was created! +2026-06-01T18:35:37.942Z [DEBUG] Updating check run {"owner":"jefeish-migration-test","repo":"safe-settings-config","check_run_id":78921699671,"status":"in_progress","started_at":"2026-06-01T18:35:37.942Z","output":{"title":"Starting NOP","summary":"initiating..."}} +2026-06-01T18:35:37.942Z [DEBUG] Is Admin repo event true +2026-06-01T18:35:38.961Z [DEBUG] Changes in '.github/settings.yml' detected, doing a full synch... +2026-06-01T18:35:38.961Z [DEBUG] deploymentConfig is {"restrictedRepos":["admin",".github","safe-settings"]} +2026-06-01T18:35:40.059Z [DEBUG] config for ref safe-settings-sync/pr-55-jefeish-migration-test-1780338928321 is {"restrictedRepos":["admin",".github","safe-settings"],"repository":{"description":"description of the repo","homepage":"https://example.github.io/","auto_init":true,"topics":["new-topic","another-topic"],"security":{"enableVulnerabilityAlerts":true,"enableAutomatedSecurityFixes":true},"private":true,"visibility":"private","has_issues":true,"has_projects":true,"has_wiki":true,"default_branch":"main","gitignore_template":"node","license_template":"mit","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":true,"delete_branch_on_merge":true,"allow_update_branch":true,"archived":false},"labels":{"include":[{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"#336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"#326699"},{"name":"new-label","oldname":"Help Wanted","color":"#326699"}],"exclude":[{"name":"^release"}]},"milestones":[{"title":"milestone-title","description":"milestone-description","state":"open"}],"collaborators":[{"username":"regpaco","permission":"push"},{"username":"beetlejuice","permission":"pull","exclude":["actions-demo"]},{"username":"thor","permission":"push","include":["actions-demo","another-repo"]}],"teams":[{"name":"core","permission":"admin"},{"name":"docss","permission":"push"},{"name":"docs","permission":"pull"},{"name":"globalteam","permission":"push","visibility":"closed"}],"branches":[{"name":"default","protection":{"required_pull_request_reviews":{"required_approving_review_count":1,"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"require_last_push_approval":true,"bypass_pull_request_allowances":{"apps":[],"users":[],"teams":[]},"dismissal_restrictions":{"users":[],"teams":[]}},"required_status_checks":{"strict":true,"contexts":[]},"enforce_admins":true,"restrictions":{"apps":[],"users":[],"teams":[]}}}],"custom_properties":[{"name":"test","value":"test"}],"autolinks":[{"key_prefix":"JIRA-","url_template":"https://jira.github.com/browse/JIRA-","is_alphanumeric":false},{"key_prefix":"MYLINK-","url_template":"https://mywebsite.com/"}],"validator":{"pattern":"[a-zA-Z0-9_-]+"},"rulesets":[{"name":"Template","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":"number","actor_type":"Team","bypass_mode":"pull_request"},{"actor_id":1,"actor_type":"OrganizationAdmin","bypass_mode":"always"},{"actor_id":7898,"actor_type":"RepositoryRole","bypass_mode":"always"},{"actor_id":210920,"actor_type":"Integration","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/oldmaster"]},"repository_name":{"include":["test*"],"exclude":["test","test1"],"protected":true}},"rules":[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"required_signatures"},{"type":"required_deployments","parameters":{"required_deployment_environments":["staging"]}},{"type":"pull_request","parameters":{"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":1,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"CodeQL","integration_id":1234},{"context":"GHAS Compliance","integration_id":1234}]}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/example.yml","repository_id":123456,"ref":"refs/heads/main","sha":"1234567890abcdef"}]}},{"type":"commit_message_pattern","parameters":{"name":"test commit_message_pattern","negate":true,"operator":"starts_with","pattern":"skip*"}},{"type":"commit_author_email_pattern","parameters":{"name":"test commit_author_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"committer_email_pattern","parameters":{"name":"test committer_email_pattern","negate":false,"operator":"regex","pattern":"^.*@example.com$"}},{"type":"branch_name_pattern","parameters":{"name":"test branch_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}},{"type":"tag_name_pattern","parameters":{"name":"test tag_name_pattern","negate":false,"operator":"regex","pattern":".*/.*"}}]}]} From 4cb5e10f757ad83e8aec12452e87d1015b0f5ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Efeish?= Date: Mon, 1 Jun 2026 15:31:14 -0400 Subject: [PATCH 22/22] fixed sync log page --- .gitignore | 3 ++ hubSyncHandler.log | 66 +++++++++++++-------------- lib/hubSyncHandler.js | 26 +++++------ lib/routes.js | 73 ++++++++++++++++++++++++++++++ ui/src/app/dashboard/logs/page.jsx | 70 +++++++++++++++++++++------- 5 files changed, 175 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 9cd65700b..a74e8ccca 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,6 @@ samconfig.toml # test file to be ignored test.log reports + +#all general log files +*.log \ No newline at end of file diff --git a/hubSyncHandler.log b/hubSyncHandler.log index 661cc6625..6a4960d7e 100644 --- a/hubSyncHandler.log +++ b/hubSyncHandler.log @@ -848,11 +848,11 @@ 2026-06-01T18:26:42.906Z [INFO] Orgs updated in PR #53: jefeish-migration-test 2026-06-01T18:26:42.906Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). 2026-06-01T18:26:42.907Z [INFO] Syncing safe settings for organization: jefeish-migration-test -2026-06-01T18:26:42.907Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' -2026-06-01T18:26:42.907Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' -2026-06-01T18:26:43.380Z [INFO] DEBUG: PR #53 contains 1 changed file(s) -2026-06-01T18:26:43.380Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml -2026-06-01T18:26:43.380Z [INFO] DEBUG: first file object = { +2026-06-01T18:26:42.907Z [DEBUG] sourceBase='.github/safe-settings/organizations' +2026-06-01T18:26:42.907Z [DEBUG] env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2026-06-01T18:26:43.380Z [DEBUG] PR #53 contains 1 changed file(s) +2026-06-01T18:26:43.380Z [DEBUG] files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:26:43.380Z [DEBUG] first file object = { "sha": "6928a64c36d64aeb96da968a8d250dfe08315435", "filename": ".github/safe-settings/organizations/jefeish-migration-test/settings.yml", "status": "modified", @@ -864,12 +864,12 @@ "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=c01de93ada8652a04aa58af10f5de44ebf4b8f84", "patch": "@@ -400,3 +400,4 @@ rulesets:\n \n # --- test 1\n # --- test 2\n+# --- test 3" } -2026-06-01T18:26:43.380Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch -2026-06-01T18:26:43.381Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml -2026-06-01T18:26:43.380Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 -2026-06-01T18:26:43.380Z [INFO] DEBUG: FILE[0] raw={"sha":"6928a64c36d64aeb96da968a8d250dfe08315435","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=c01de93ada8652a04aa58af10f5de44ebf4b8f84","patch":"@@ -400,3 +400,4 @@ rulesets:\n \n # --- test 1\n # --- test 2\n+# --- test 3"} -2026-06-01T18:26:43.381Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-migration-test -2026-06-01T18:26:43.381Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-migration-test +2026-06-01T18:26:43.380Z [DEBUG] file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2026-06-01T18:26:43.381Z [DEBUG] files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:26:43.380Z [DEBUG] FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 +2026-06-01T18:26:43.380Z [DEBUG] FILE[0] raw={"sha":"6928a64c36d64aeb96da968a8d250dfe08315435","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":1,"deletions":0,"changes":1,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/c01de93ada8652a04aa58af10f5de44ebf4b8f84/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=c01de93ada8652a04aa58af10f5de44ebf4b8f84","patch":"@@ -400,3 +400,4 @@ rulesets:\n \n # --- test 1\n # --- test 2\n+# --- test 3"} +2026-06-01T18:26:43.381Z [DEBUG] Found 1 changed file(s) relevant to org jefeish-migration-test +2026-06-01T18:26:43.381Z [DEBUG] Path .github/safe-settings/organizations/jefeish-migration-test 2026-06-01T18:26:43.691Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #53 to jefeish-migration-test/safe-settings-config@main under .github (directPush=true) 2026-06-01T18:27:03.334Z [ERROR] Failed to sync file .github/settings.yml: Could not update file: Changes must be made through a pull request. - https://docs.github.com/articles/about-protected-branches 2026-06-01T18:27:03.334Z [ERROR] syncSafeSettingConfig error for org jefeish-migration-test: Could not update file: Changes must be made through a pull request. - https://docs.github.com/articles/about-protected-branches @@ -912,14 +912,14 @@ 2026-06-01T18:31:10.277Z [INFO] Pull request closed on Safe-Settings Hub: (jefeish-training/safe-settings-config-master) 2026-06-01T18:31:10.724Z [DEBUG] Detected changes in the organizations folder. Routing to syncHubOrgUpdate(...). 2026-06-01T18:31:10.725Z [INFO] Organization: jefeish-migration-test, Destination Repo: safe-settings-config, Destination Folder: .github -2026-06-01T18:31:10.725Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' +2026-06-01T18:31:10.725Z [DEBUG] sourceBase='.github/safe-settings/organizations' 2026-06-01T18:31:10.725Z [INFO] Syncing safe settings for organization: jefeish-migration-test 2026-06-01T18:31:10.724Z [INFO] Files changed in PR #54: .github/safe-settings/organizations/jefeish-migration-test/settings.yml 2026-06-01T18:31:10.724Z [INFO] Orgs updated in PR #54: jefeish-migration-test -2026-06-01T18:31:10.725Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' -2026-06-01T18:31:11.118Z [INFO] DEBUG: PR #54 contains 1 changed file(s) -2026-06-01T18:31:11.118Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml -2026-06-01T18:31:11.118Z [INFO] DEBUG: first file object = { +2026-06-01T18:31:10.725Z [DEBUG] env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2026-06-01T18:31:11.118Z [DEBUG] PR #54 contains 1 changed file(s) +2026-06-01T18:31:11.118Z [DEBUG] files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:31:11.118Z [DEBUG] first file object = { "sha": "4886a71655233cb557464ed47ce306079bbd170d", "filename": ".github/safe-settings/organizations/jefeish-migration-test/settings.yml", "status": "modified", @@ -931,12 +931,12 @@ "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=daab9b95fb0626f8a0cbace114a6029e93c39636", "patch": "@@ -400,4 +400,3 @@ rulesets:\n \n # --- test 1\n # --- test 2\n-# --- test 3" } -2026-06-01T18:31:11.118Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch -2026-06-01T18:31:11.118Z [INFO] DEBUG: FILE[0] raw={"sha":"4886a71655233cb557464ed47ce306079bbd170d","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":0,"deletions":1,"changes":1,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=daab9b95fb0626f8a0cbace114a6029e93c39636","patch":"@@ -400,4 +400,3 @@ rulesets:\n \n # --- test 1\n # --- test 2\n-# --- test 3"} -2026-06-01T18:31:11.118Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 -2026-06-01T18:31:11.119Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml -2026-06-01T18:31:11.119Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-migration-test -2026-06-01T18:31:11.119Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-migration-test +2026-06-01T18:31:11.118Z [DEBUG] file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2026-06-01T18:31:11.118Z [DEBUG] FILE[0] raw={"sha":"4886a71655233cb557464ed47ce306079bbd170d","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":0,"deletions":1,"changes":1,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/daab9b95fb0626f8a0cbace114a6029e93c39636/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=daab9b95fb0626f8a0cbace114a6029e93c39636","patch":"@@ -400,4 +400,3 @@ rulesets:\n \n # --- test 1\n # --- test 2\n-# --- test 3"} +2026-06-01T18:31:11.118Z [DEBUG] FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 +2026-06-01T18:31:11.119Z [DEBUG] files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:31:11.119Z [DEBUG] Path .github/safe-settings/organizations/jefeish-migration-test +2026-06-01T18:31:11.119Z [DEBUG] Found 1 changed file(s) relevant to org jefeish-migration-test 2026-06-01T18:31:11.364Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #54 to jefeish-migration-test/safe-settings-config@main under .github (directPush=true) 2026-06-01T18:31:11.529Z [DEBUG] Is Admin repo event false 2026-06-01T18:31:11.529Z [DEBUG] Not working on the Admin repo, returning... @@ -951,15 +951,15 @@ 2026-06-01T18:35:27.673Z [INFO] Files changed in PR #55: .github/safe-settings/organizations/jefeish-migration-test/settings.yml 2026-06-01T18:35:27.674Z [INFO] Syncing safe settings for organization: jefeish-migration-test 2026-06-01T18:35:27.673Z [INFO] Orgs updated in PR #55: jefeish-migration-test -2026-06-01T18:35:27.674Z [INFO] DEBUG: sourceBase='.github/safe-settings/organizations' -2026-06-01T18:35:27.675Z [INFO] DEBUG: env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' +2026-06-01T18:35:27.674Z [DEBUG] sourceBase='.github/safe-settings/organizations' +2026-06-01T18:35:27.675Z [DEBUG] env.CONFIG_PATH='.github', env.SAFE_SETTINGS_HUB_PATH='safe-settings' 2026-06-01T18:35:27.985Z [DEBUG] Not working on the Admin repo, returning... 2026-06-01T18:35:27.985Z [DEBUG] Is Admin repo event false -2026-06-01T18:35:28.092Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml -2026-06-01T18:35:28.093Z [INFO] DEBUG: file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch -2026-06-01T18:35:28.093Z [INFO] DEBUG: FILE[0] raw={"sha":"785f2c88eba8195abb95a25febb1ff099a844516","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=80e54cbb65a2ef75e9da0937604a1f8308f944e7","patch":"@@ -399,4 +399,4 @@ rulesets:\n pattern: \".*\\/.*\"\n \n # --- test 1\n-# --- test 2\n+# --- test 2 "} -2026-06-01T18:35:28.092Z [INFO] DEBUG: PR #55 contains 1 changed file(s) -2026-06-01T18:35:28.093Z [INFO] DEBUG: first file object = { +2026-06-01T18:35:28.092Z [DEBUG] files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:35:28.093Z [DEBUG] file[0] keys = sha, filename, status, additions, deletions, changes, blob_url, raw_url, contents_url, patch +2026-06-01T18:35:28.093Z [DEBUG] FILE[0] raw={"sha":"785f2c88eba8195abb95a25febb1ff099a844516","filename":".github/safe-settings/organizations/jefeish-migration-test/settings.yml","status":"modified","additions":1,"deletions":1,"changes":2,"blob_url":"https://github.com/jefeish-training/safe-settings-config-master/blob/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","raw_url":"https://github.com/jefeish-training/safe-settings-config-master/raw/80e54cbb65a2ef75e9da0937604a1f8308f944e7/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml","contents_url":"https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=80e54cbb65a2ef75e9da0937604a1f8308f944e7","patch":"@@ -399,4 +399,4 @@ rulesets:\n pattern: \".*\\/.*\"\n \n # --- test 1\n-# --- test 2\n+# --- test 2 "} +2026-06-01T18:35:28.092Z [DEBUG] PR #55 contains 1 changed file(s) +2026-06-01T18:35:28.093Z [DEBUG] first file object = { "sha": "785f2c88eba8195abb95a25febb1ff099a844516", "filename": ".github/safe-settings/organizations/jefeish-migration-test/settings.yml", "status": "modified", @@ -971,10 +971,10 @@ "contents_url": "https://api.github.com/repos/jefeish-training/safe-settings-config-master/contents/.github%2Fsafe-settings%2Forganizations%2Fjefeish-migration-test%2Fsettings.yml?ref=80e54cbb65a2ef75e9da0937604a1f8308f944e7", "patch": "@@ -399,4 +399,4 @@ rulesets:\n pattern: \".*\\/.*\"\n \n # --- test 1\n-# --- test 2\n+# --- test 2 " } -2026-06-01T18:35:28.093Z [INFO] DEBUG: FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 -2026-06-01T18:35:28.093Z [INFO] DEBUG: files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml -2026-06-01T18:35:28.093Z [INFO] DEBUG: Found 1 changed file(s) relevant to org jefeish-migration-test -2026-06-01T18:35:28.093Z [INFO] DEBUG: Path .github/safe-settings/organizations/jefeish-migration-test +2026-06-01T18:35:28.093Z [DEBUG] FILE[0] filename=".github/safe-settings/organizations/jefeish-migration-test/settings.yml" length=71 +2026-06-01T18:35:28.093Z [DEBUG] files=.github/safe-settings/organizations/jefeish-migration-test/settings.yml +2026-06-01T18:35:28.093Z [DEBUG] Found 1 changed file(s) relevant to org jefeish-migration-test +2026-06-01T18:35:28.093Z [DEBUG] Path .github/safe-settings/organizations/jefeish-migration-test 2026-06-01T18:35:28.321Z [INFO] Syncing from jefeish-training/safe-settings-config-master PR #55 to jefeish-migration-test/safe-settings-config@main under .github (directPush=false) 2026-06-01T18:35:30.051Z [INFO] Created branch safe-settings-sync/pr-55-jefeish-migration-test-1780338928321 in jefeish-migration-test/safe-settings-config 2026-06-01T18:35:31.423Z [DEBUG] Not working on the default branch, returning... diff --git a/lib/hubSyncHandler.js b/lib/hubSyncHandler.js index 621a36447..d083c598e 100644 --- a/lib/hubSyncHandler.js +++ b/lib/hubSyncHandler.js @@ -126,35 +126,35 @@ async function syncHubOrgUpdate (robot, context, orgName, destRepo, destinationF const pull_number = pr.number const configRoot = env.CONFIG_PATH || '.github/' const sourceBase = (`${configRoot}/${env.SAFE_SETTINGS_HUB_PATH}/organizations`).replace(/\/$/, '') - robot.log.info(`DEBUG: sourceBase='${sourceBase}'`) - robot.log.info(`DEBUG: env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`) + robot.log.debug(`sourceBase='${sourceBase}'`) + robot.log.debug(`env.CONFIG_PATH='${env.CONFIG_PATH}', env.SAFE_SETTINGS_HUB_PATH='${env.SAFE_SETTINGS_HUB_PATH}'`) const files = await context.octokit.paginate( context.octokit.rest.pulls.listFiles, { owner: srcOwner, repo: srcRepo, pull_number, per_page: 100 } ) - robot.log.info(`DEBUG: PR #${pull_number} contains ${files.length} changed file(s)`) - if (files.length) robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) + robot.log.debug(`PR #${pull_number} contains ${files.length} changed file(s)`) + if (files.length) robot.log.debug(`files=${files.map(f => f.filename).join(', ')}`) if (files.length) { try { - robot.log.info(`DEBUG: first file object = ${JSON.stringify(files[0], null, 2)}`) - robot.log.info(`DEBUG: file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`) + robot.log.debug(`first file object = ${JSON.stringify(files[0], null, 2)}`) + robot.log.debug(`file[0] keys = ${Object.keys(files[0] || {}).join(', ')}`) } catch (e) { - robot.log.info(`DEBUG: failed to stringify first file: ${e.message}`) + robot.log.debug(`failed to stringify first file: ${e.message}`) } files.forEach((f, i) => { try { - robot.log.info(`DEBUG: FILE[${i}] raw=${JSON.stringify(f)}`) - robot.log.info(`DEBUG: FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`) + robot.log.debug(`FILE[${i}] raw=${JSON.stringify(f)}`) + robot.log.debug(`FILE[${i}] filename=${JSON.stringify(f.filename)} length=${(f.filename || '').length}`) } catch (e) { - robot.log.info(`DEBUG: FILE[${i}] stringify error: ${e.message}`) + robot.log.debug(`FILE[${i}] stringify error: ${e.message}`) } }) } const orgPrefix = `${sourceBase}/${orgName}/` - robot.log.info(`DEBUG: files=${files.map(f => f.filename).join(', ')}`) - robot.log.info(`DEBUG: Path ${sourceBase}/${orgName}`) + robot.log.debug(`files=${files.map(f => f.filename).join(', ')}`) + robot.log.debug(`Path ${sourceBase}/${orgName}`) const relevant = files.filter(f => f.filename === `${sourceBase}/${orgName}` || f.filename.startsWith(orgPrefix)) - robot.log.info(`DEBUG: Found ${relevant.length} changed file(s) relevant to org ${orgName}`) + robot.log.debug(`Found ${relevant.length} changed file(s) relevant to org ${orgName}`) if (!relevant.length) { robot.log.info(`No files for org ${orgName} in PR #${pull_number}`) files.forEach(f => { diff --git a/lib/routes.js b/lib/routes.js index 5499ebfbe..6139d0b8f 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -84,6 +84,10 @@ function setupRoutes (robot, getRouter) { res.sendFile(path.join(uiPath, 'dashboard', 'help.html')) }) + router.get('/dashboard/logs', (req, res) => { + res.sendFile(path.join(uiPath, 'dashboard', 'logs.html')) + }) + // Apple touch icon (silence 404s). Replace file logic if you add a real 180x180 asset. const APPLE_TOUCH_ICON_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAQAAAA9zQYyAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==' // 180x180 transparent PNG router.get('/apple-touch-icon.png', (req, res) => { @@ -666,6 +670,7 @@ function setupRoutes (robot, getRouter) { const candidates = [] if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE) + candidates.push(path.join(rootDir, 'hubSyncHandler.log')) // Primary log file for hub-sync candidates.push(path.join(rootDir, 'safe-settings.log')) candidates.push(path.join(rootDir, '..', 'safe-settings.log')) candidates.push(path.join(rootDir, 'ui', 'safe-settings.log')) @@ -704,6 +709,74 @@ function setupRoutes (robot, getRouter) { } }) + // Alias for backwards compatibility + router.get('/api/safe-settings/logs', (req, res, next) => { + req.url = '/api/safe-settings/hub/log' + (req._parsedUrl.search || '') + next('route') + }) + + // GET /api/safe-settings/sync-logs + // Returns only sync-related log entries (filtered by [SYNC or containing 'sync') + router.get('/api/safe-settings/sync-logs', async (req, res) => { + const lines = parseInt(req.query.lines || process.env.SAFE_SETTINGS_LOG_FILE_MAX_LINES || '1000', 10) + const levelsQuery = req.query.levels // comma-separated e.g. 'ERROR,WARN' + const allowedLevels = levelsQuery ? new Set(String(levelsQuery).split(',').map(s => s.trim().toUpperCase()).filter(Boolean)) : null + + const candidates = [] + if (process.env.SAFE_SETTINGS_LOG_FILE) candidates.push(process.env.SAFE_SETTINGS_LOG_FILE) + candidates.push(path.join(rootDir, 'hubSyncHandler.log')) // Primary log file for hub-sync + candidates.push(path.join(rootDir, 'safe-settings.log')) + candidates.push(path.join(rootDir, '..', 'safe-settings.log')) + candidates.push(path.join(rootDir, 'ui', 'safe-settings.log')) + + let found = null + for (const p of candidates) { + if (!p) continue + try { + const st = await fs.promises.stat(p) + if (st && st.isFile()) { found = p; break } + } catch (e) { + // ignore + } + } + if (!found) return res.status(404).json({ error: 'Log file not found' }) + + try { + const data = await fs.promises.readFile(found, 'utf8') + const arr = data.split(/\r?\n/).filter(Boolean) + const tail = arr.slice(-lines * 3) // Get more lines to ensure we have enough sync entries + const parsed = tail.map(line => { + // Expecting format: 2025-09-10T12:34:56.789Z [INFO] message + const m = line.match(/^(\d{4}-\d{2}-\d{2}T[^\s]+)\s+\[([A-Z]+)\]\s+(.*)$/) + if (m) { + return { timestamp: m[1], level: m[2], message: m[3], raw: line } + } + // fallback: try to extract level in brackets + const m2 = line.match(/\[([A-Z]+)\]\s*(.*)$/) + if (m2) return { timestamp: null, level: m2[1], message: m2[2], raw: line } + return { timestamp: null, level: 'UNKNOWN', message: line, raw: line } + }) + + // Filter for sync-related entries + const syncFiltered = parsed.filter(p => { + const msg = p.message.toLowerCase() + return msg.includes('sync') || msg.includes('[sync') + }) + + // Apply level filter if specified + const filtered = allowedLevels + ? syncFiltered.filter(p => allowedLevels.has(String(p.level).toUpperCase())) + : syncFiltered + + // Limit to requested number of lines + const final = filtered.slice(-lines) + + return res.json({ count: final.length, entries: final, syncOnly: true }) + } catch (err) { + return res.status(500).json({ error: err && err.message ? err.message : String(err) }) + } + }) + return router } diff --git a/ui/src/app/dashboard/logs/page.jsx b/ui/src/app/dashboard/logs/page.jsx index 184e6da63..de97c144c 100644 --- a/ui/src/app/dashboard/logs/page.jsx +++ b/ui/src/app/dashboard/logs/page.jsx @@ -1,22 +1,35 @@ "use client" +import { useState, useEffect } from 'react' import TitleBar from '../../components/TitleBar' -import { useState } from 'react' +import { withBasePath } from '../../utils/basePath' export default function LogsPage () { - // Static mock data for demonstration - const mockEntries = [ - { timestamp: '2025-09-11T10:00:00.000Z', level: 'INFO', message: 'Safe Settings service started.' }, - { timestamp: '2025-09-11T10:01:05.123Z', level: 'WARN', message: 'Config file missing, using defaults.' }, - { timestamp: '2025-09-11T10:02:10.456Z', level: 'ERROR', message: 'Failed to sync settings: network error.' }, - { timestamp: '2025-09-11T10:03:00.789Z', level: 'DEBUG', message: 'Polling GitHub API for updates.' }, - { timestamp: '2025-09-11T10:04:15.000Z', level: 'INFO', message: 'Sync completed successfully.' }, - { timestamp: '2025-09-11T10:05:00.000Z', level: 'INFO', message: 'SYNC: Organization settings updated.' }, - { timestamp: '2025-09-11T10:06:00.000Z', level: 'ERROR', message: 'SYNC: Failed to update organization settings.' } - ] + const [entries, setEntries] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) const logLevels = ['INFO', 'WARN', 'DEBUG', 'ERROR'] const [selectedLevels, setSelectedLevels] = useState(new Set(logLevels)) const [search, setSearch] = useState('') + const [syncOnly, setSyncOnly] = useState(false) + + useEffect(() => { + async function fetchLogs() { + try { + setLoading(true) + const response = await fetch(withBasePath('/api/safe-settings/hub/log?lines=100')) + if (!response.ok) throw new Error('Failed to fetch logs') + const data = await response.json() + setEntries(data.entries || []) + setError(null) + } catch (err) { + setError(err.message) + } finally { + setLoading(false) + } + } + fetchLogs() + }, []) const toggleLevel = (lvl) => { const next = new Set(selectedLevels) @@ -25,10 +38,18 @@ export default function LogsPage () { setSelectedLevels(next) } - const filtered = mockEntries.filter(e => - selectedLevels.has(e.level.toUpperCase()) && - (search.trim() === '' || e.message.toLowerCase().includes(search.trim().toLowerCase())) - ) + const filtered = entries.filter(e => { + // Filter by log level + if (!selectedLevels.has(e.level.toUpperCase())) return false + + // Filter by search term + if (search.trim() !== '' && !e.message.toLowerCase().includes(search.trim().toLowerCase())) return false + + // Filter by sync-only if enabled + if (syncOnly && !e.message.toLowerCase().includes('sync')) return false + + return true + }) return ( <> @@ -37,8 +58,10 @@ export default function LogsPage () {
    -

    Safe Settings Log

    -

    View recent log entries for Safe Settings operations and syncs.

    +

    Safe Settings Hub-Sync Log

    +

    Last 100 entries from hubSyncHandler.log

    + {loading &&
    Loading logs...
    } + {error &&
    Error: {error}
    }
    @@ -57,6 +80,19 @@ export default function LogsPage () { ))} +
    + +
    Search Message: