Skip to content

Commit ea663c2

Browse files
committed
adding hub-sync feature code
1 parent 861d70f commit ea663c2

38 files changed

+8290
-367
lines changed

.env.example

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,27 @@
1515

1616
# Uncomment this to get GitHub comments for the Pull Request Workflow.
1717
# ENABLE_PR_COMMENT=true
18+
19+
# ADMIN_REPO=safe-settings-config
20+
CONFIG_PATH=.github
21+
SETTINGS_FILE_PATH=settings.yml
22+
23+
# Configuration support for Hub-Sync safe-settings feature
24+
# SAFE_SETTINGS_HUB_REPO=safe-settings-config-master
25+
# SAFE_SETTINGS_HUB_ORG=foo-training
26+
# A subfolder under 'CONFIG_PATH' where the 'organizations/<org>/<repo>' structure is found
27+
# SAFE_SETTINGS_HUB_PATH=safe-settings
28+
# SAFE_SETTINGS_HUB_DIRECT_PUSH=true
29+
30+
31+
32+
# ┌────────────── second (optional)
33+
# │ ┌──────────── minute
34+
# │ │ ┌────────── hour
35+
# │ │ │ ┌──────── day of month
36+
# │ │ │ │ ┌────── month
37+
# │ │ │ │ │ ┌──── day of week
38+
# │ │ │ │ │ │
39+
# │ │ │ │ │ │
40+
# * * * * * *
41+
# CRON=* * * * * # Run every minute

docs/hubSyncHandler/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Safe Settings Organization Sync & Dashboard
2+
3+
This feature provides a centralized approach to managing the Safe-Settings Admin Repo, allowing Safe-Settings configurations to be sync'd across multiple ORGs.
4+
5+
## Overview
6+
7+
This feature adds a hub‑and‑spoke synchronization capability to Safe Settings.
8+
9+
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).
10+
11+
**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.
12+
13+
## Sync Lifecycle (High Level)
14+
15+
```mermaid
16+
graph TD
17+
A0(PR Closed) --> A1(HUB Admin Repo)
18+
A1(ORG Admin Repo) --> B(ORG Admin Repo)
19+
A1(HUB Admin Repo) --> C(ORG Admin Repo)
20+
A1(HUB Admin Repo) --> D(ORG Admin Repo)
21+
```
22+
23+
## Environment Variables & Inputs
24+
25+
Environment variables specific to the 'Sync-Feature'
26+
27+
| Name | Purpose | Default |
28+
|------|---------|---------|
29+
| `SAFE_SETTINGS_HUB_REPO` | Repo for master safe-settings contents | admin-master |
30+
| `SAFE_SETTINGS_HUB_ORG` | Organization that hold the Repo | admin-master-org |
31+
| `SAFE_SETTINGS_HUB_PATH` | source folder | .github/safe-settings |
32+
| `SAFE_SETTINGS_HUB_DIRECT_PUSH` | Use a PR or direct commit | false |
33+
34+

index.js

Lines changed: 30 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,22 @@ const Glob = require('./lib/glob')
66
const ConfigManager = require('./lib/configManager')
77
const NopCommand = require('./lib/nopcommand')
88
const env = require('./lib/env')
9+
const { setupRoutes } = require('./lib/routes')
10+
const { initCache } = require('./lib/installationCache')
11+
const { hubSyncHandler } = require('./lib/hubSyncHandler')
912

1013
let deploymentConfig
1114

1215
module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => {
1316
let appSlug = 'safe-settings'
14-
async function syncAllSettings (nop, context, repo = context.repo(), ref) {
15-
try {
16-
deploymentConfig = await loadYamlFileSystem()
17-
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
18-
const configManager = new ConfigManager(context, ref)
19-
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
20-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
21-
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
22-
if (ref) {
23-
return Settings.syncAll(nop, context, repo, config, ref)
24-
} else {
25-
return Settings.syncAll(nop, context, repo, config)
26-
}
27-
} catch (e) {
28-
if (nop) {
29-
let filename = env.SETTINGS_FILE_PATH
30-
if (!deploymentConfig) {
31-
filename = env.DEPLOYMENT_CONFIG_FILE_PATH
32-
deploymentConfig = {}
33-
}
34-
const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR')
35-
robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`)
36-
Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand)
37-
} else {
38-
throw e
39-
}
40-
}
41-
}
4217

43-
async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) {
44-
try {
45-
deploymentConfig = await loadYamlFileSystem()
46-
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
47-
const configManager = new ConfigManager(context, ref)
48-
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
49-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
50-
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
51-
return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref)
52-
} catch (e) {
53-
if (nop) {
54-
let filename = env.SETTINGS_FILE_PATH
55-
if (!deploymentConfig) {
56-
filename = env.DEPLOYMENT_CONFIG_FILE_PATH
57-
deploymentConfig = {}
58-
}
59-
const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR')
60-
robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`)
61-
Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand)
62-
} else {
63-
throw e
64-
}
65-
}
66-
}
18+
// Initialize all routes (static UI + API) via centralized module
19+
setupRoutes(robot, getRouter)
6720

68-
async function syncSettings (nop, context, repo = context.repo(), ref) {
69-
try {
70-
deploymentConfig = await loadYamlFileSystem()
71-
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
72-
const configManager = new ConfigManager(context, ref)
73-
const runtimeConfig = await configManager.loadGlobalSettingsYaml()
74-
const config = Object.assign({}, deploymentConfig, runtimeConfig)
75-
robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`)
76-
return Settings.sync(nop, context, repo, config, ref)
77-
} catch (e) {
78-
if (nop) {
79-
let filename = env.SETTINGS_FILE_PATH
80-
if (!deploymentConfig) {
81-
filename = env.DEPLOYMENT_CONFIG_FILE_PATH
82-
deploymentConfig = {}
83-
}
84-
const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR')
85-
robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`)
86-
Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand)
87-
} else {
88-
throw e
89-
}
90-
}
91-
}
21+
// Initialize installation cache (env-controlled prefetch)
22+
initCache(robot)
9223

93-
async function renameSync (nop, context, repo = context.repo(), rename, ref) {
24+
async function renameSync(nop, context, repo = context.repo(), rename, ref) {
9425
try {
9526
deploymentConfig = await loadYamlFileSystem()
9627
robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`)
@@ -115,13 +46,14 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
11546
}
11647
}
11748
}
49+
11850
/**
11951
* Loads the deployment config file from file system
12052
* Do this once when the app starts and then return the cached value
12153
*
12254
* @return The parsed YAML file
12355
*/
124-
async function loadYamlFileSystem () {
56+
async function loadYamlFileSystem() {
12557
if (deploymentConfig === undefined) {
12658
const deploymentConfigPath = env.DEPLOYMENT_CONFIG_FILE_PATH
12759
if (fs.existsSync(deploymentConfigPath)) {
@@ -133,7 +65,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
13365
return deploymentConfig
13466
}
13567

136-
function getAllChangedSubOrgConfigs (payload) {
68+
function getAllChangedSubOrgConfigs(payload) {
13769
const pattern = Settings.SUB_ORG_PATTERN
13870

13971
const getMatchingFiles = (commits, type) =>
@@ -150,7 +82,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
15082
}))
15183
}
15284

153-
function getAllChangedRepoConfigs (payload, owner) {
85+
function getAllChangedRepoConfigs(payload, owner) {
15486
const pattern = Settings.REPO_PATTERN
15587

15688
const getMatchingFiles = (commits, type) =>
@@ -167,7 +99,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
16799
}))
168100
}
169101

170-
function getChangedRepoConfigName (files, owner) {
102+
function getChangedRepoConfigName(files, owner) {
171103
const pattern = Settings.REPO_PATTERN
172104

173105
const modifiedFiles = files.filter((s) => pattern.test(s))
@@ -178,7 +110,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
178110
}))
179111
}
180112

181-
function getChangedSubOrgConfigName (files) {
113+
function getChangedSubOrgConfigName(files) {
182114
const pattern = Settings.SUB_ORG_PATTERN
183115

184116
const modifiedFiles = files.filter((s) => pattern.test(s))
@@ -188,7 +120,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
188120
path: modifiedFile
189121
}))
190122
}
191-
async function createCheckRun (context, pull_request, head_sha, head_branch) {
123+
async function createCheckRun(context, pull_request, head_sha, head_branch) {
192124
const { payload } = context
193125
// robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`)
194126
const res = await context.octokit.checks.create({
@@ -200,7 +132,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
200132
robot.log.debug(JSON.stringify(res, null))
201133
}
202134

203-
async function info () {
135+
async function info() {
204136
const github = await robot.auth()
205137
const installations = await github.paginate(
206138
github.apps.listInstallations.endpoint.merge({ per_page: 100 })
@@ -215,7 +147,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
215147
}
216148
}
217149

218-
async function syncInstallation (nop = false) {
150+
async function syncInstallation(nop = false) {
219151
robot.log.trace('Fetching installations')
220152
const github = await robot.auth()
221153

@@ -521,6 +453,19 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) =>
521453
return createCheckRun(context, pull_request, payload.pull_request.head.sha, payload.pull_request.head.ref)
522454
})
523455

456+
/**
457+
* @description Handle pull_request.closed events to support hub synchronization
458+
* @param {Object} context - The context object provided by Probot
459+
*/
460+
robot.on('pull_request.closed', async context => {
461+
try {
462+
await hubSyncHandler(robot, context)
463+
} catch (err) {
464+
robot.log.error(`pull_request.closed handler failed: ${err && err.message ? err.message : err}`)
465+
}
466+
return null
467+
})
468+
524469
robot.on(['check_suite.rerequested'], async context => {
525470
robot.log.debug('Check suite was rerequested!')
526471
return createCheckRun(context)

lib/env.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
module.exports = {
22
ADMIN_REPO: process.env.ADMIN_REPO || 'admin',
3+
SAFE_SETTINGS_HUB_REPO: process.env.SAFE_SETTINGS_HUB_REPO || 'admin-master',
4+
SAFE_SETTINGS_HUB_ORG: process.env.SAFE_SETTINGS_HUB_ORG || 'admin-master-org',
5+
SAFE_SETTINGS_HUB_DIRECT_PUSH: process.env.SAFE_SETTINGS_HUB_DIRECT_PUSH || 'false',
6+
SAFE_SETTINGS_HUB_PATH: process.env.SAFE_SETTINGS_HUB_PATH || '.github/safe-settings',
7+
APP_ID: process.env.APP_ID || null,
8+
PRIVATE_KEY_PATH: process.env.PRIVATE_KEY_PATH || 'private-key.pem',
39
CONFIG_PATH: process.env.CONFIG_PATH || '.github',
410
SETTINGS_FILE_PATH: process.env.SETTINGS_FILE_PATH || 'settings.yml',
511
DEPLOYMENT_CONFIG_FILE_PATH: process.env.DEPLOYMENT_CONFIG_FILE || 'deployment-settings.yml',

0 commit comments

Comments
 (0)