@empire-builder-kit/nx 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +34 -0
- package/executors.json +74 -0
- package/generators.json +40 -0
- package/migrations/standard-pre-v1-to-v1.md +11 -0
- package/package.json +80 -0
- package/src/blueprints/execution.d.ts +31 -0
- package/src/blueprints/execution.js +166 -0
- package/src/blueprints/execution.js.map +1 -0
- package/src/blueprints/index.d.ts +5 -0
- package/src/blueprints/index.js +9 -0
- package/src/blueprints/index.js.map +1 -0
- package/src/blueprints/inputs.d.ts +43 -0
- package/src/blueprints/inputs.js +103 -0
- package/src/blueprints/inputs.js.map +1 -0
- package/src/blueprints/manifest.d.ts +281 -0
- package/src/blueprints/manifest.js +441 -0
- package/src/blueprints/manifest.js.map +1 -0
- package/src/blueprints/resolver.d.ts +28 -0
- package/src/blueprints/resolver.js +218 -0
- package/src/blueprints/resolver.js.map +1 -0
- package/src/blueprints/transitions.d.ts +20 -0
- package/src/blueprints/transitions.js +61 -0
- package/src/blueprints/transitions.js.map +1 -0
- package/src/executors/deploy/deploy.d.ts +4 -0
- package/src/executors/deploy/deploy.js +6 -0
- package/src/executors/deploy/deploy.js.map +1 -0
- package/src/executors/deployment-plan/deployment-plan.d.ts +9 -0
- package/src/executors/deployment-plan/deployment-plan.js +50 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -0
- package/src/executors/deployment-plan/schema.json +24 -0
- package/src/executors/dev/dev.d.ts +4 -0
- package/src/executors/dev/dev.js +6 -0
- package/src/executors/dev/dev.js.map +1 -0
- package/src/executors/dev-doctor/dev-doctor.d.ts +31 -0
- package/src/executors/dev-doctor/dev-doctor.js +260 -0
- package/src/executors/dev-doctor/dev-doctor.js.map +1 -0
- package/src/executors/dev-doctor/schema.json +17 -0
- package/src/executors/dev-setup/dev-setup.d.ts +11 -0
- package/src/executors/dev-setup/dev-setup.js +454 -0
- package/src/executors/dev-setup/dev-setup.js.map +1 -0
- package/src/executors/dev-setup/schema.json +16 -0
- package/src/executors/fleet/fleet.d.ts +62 -0
- package/src/executors/fleet/fleet.js +613 -0
- package/src/executors/fleet/fleet.js.map +1 -0
- package/src/executors/fleet/schema.json +37 -0
- package/src/executors/foundation-preflight/foundation-preflight.d.ts +15 -0
- package/src/executors/foundation-preflight/foundation-preflight.js +116 -0
- package/src/executors/foundation-preflight/foundation-preflight.js.map +1 -0
- package/src/executors/foundation-preflight/schema.json +31 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.d.ts +8 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js +41 -0
- package/src/executors/foundation-profile-preflight/foundation-profile-preflight.js.map +1 -0
- package/src/executors/foundation-profile-preflight/schema.json +21 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.d.ts +13 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js +69 -0
- package/src/executors/foundation-remove-preflight/foundation-remove-preflight.js.map +1 -0
- package/src/executors/foundation-remove-preflight/schema.json +17 -0
- package/src/executors/github-bootstrap/github-bootstrap.d.ts +8 -0
- package/src/executors/github-bootstrap/github-bootstrap.js +30 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -0
- package/src/executors/github-bootstrap/schema.json +25 -0
- package/src/executors/refresh/refresh.d.ts +4 -0
- package/src/executors/refresh/refresh.js +6 -0
- package/src/executors/refresh/refresh.js.map +1 -0
- package/src/executors/remove/remove.d.ts +4 -0
- package/src/executors/remove/remove.js +6 -0
- package/src/executors/remove/remove.js.map +1 -0
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +31 -0
- package/src/executors/sst-lifecycle/run-sst-command.js +226 -0
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -0
- package/src/executors/sst-lifecycle/schema.json +45 -0
- package/src/executors/unlock/unlock.d.ts +4 -0
- package/src/executors/unlock/unlock.js +6 -0
- package/src/executors/unlock/unlock.js.map +1 -0
- package/src/executors/work-markers/schema.json +14 -0
- package/src/executors/work-markers/work-markers.d.ts +6 -0
- package/src/executors/work-markers/work-markers.js +20 -0
- package/src/executors/work-markers/work-markers.js.map +1 -0
- package/src/foundation/aurora-logical-database.d.ts +76 -0
- package/src/foundation/aurora-logical-database.js +419 -0
- package/src/foundation/aurora-logical-database.js.map +1 -0
- package/src/foundation/discovery.d.ts +55 -0
- package/src/foundation/discovery.js +55 -0
- package/src/foundation/discovery.js.map +1 -0
- package/src/foundation/index.d.ts +6 -0
- package/src/foundation/index.js +10 -0
- package/src/foundation/index.js.map +1 -0
- package/src/foundation/local-database-contract.d.ts +30 -0
- package/src/foundation/local-database-contract.js +75 -0
- package/src/foundation/local-database-contract.js.map +1 -0
- package/src/foundation/preflight.d.ts +31 -0
- package/src/foundation/preflight.js +114 -0
- package/src/foundation/preflight.js.map +1 -0
- package/src/foundation/provision-database-bindings.d.ts +30 -0
- package/src/foundation/provision-database-bindings.js +113 -0
- package/src/foundation/provision-database-bindings.js.map +1 -0
- package/src/foundation/provision-logical-database.d.ts +26 -0
- package/src/foundation/provision-logical-database.js +163 -0
- package/src/foundation/provision-logical-database.js.map +1 -0
- package/src/foundation/requirements.d.ts +37 -0
- package/src/foundation/requirements.js +141 -0
- package/src/foundation/requirements.js.map +1 -0
- package/src/foundation/stages.d.ts +14 -0
- package/src/foundation/stages.js +51 -0
- package/src/foundation/stages.js.map +1 -0
- package/src/generators/function/function.d.ts +13 -0
- package/src/generators/function/function.js +71 -0
- package/src/generators/function/function.js.map +1 -0
- package/src/generators/function/schema.d.ts +5 -0
- package/src/generators/function/schema.json +20 -0
- package/src/generators/job/job.d.ts +4 -0
- package/src/generators/job/job.js +5 -0
- package/src/generators/job/job.js.map +1 -0
- package/src/generators/preset/generator.d.ts +5 -0
- package/src/generators/preset/generator.js +41 -0
- package/src/generators/preset/generator.js.map +1 -0
- package/src/generators/preset/schema.d.ts +4 -0
- package/src/generators/preset/schema.json +22 -0
- package/src/generators/preset/workspace-files.d.ts +18 -0
- package/src/generators/preset/workspace-files.js +1605 -0
- package/src/generators/preset/workspace-files.js.map +1 -0
- package/src/generators/repository-policy/repository-policy.d.ts +4 -0
- package/src/generators/repository-policy/repository-policy.js +107 -0
- package/src/generators/repository-policy/repository-policy.js.map +1 -0
- package/src/generators/repository-policy/schema.d.ts +18 -0
- package/src/generators/repository-policy/schema.json +46 -0
- package/src/generators/service/service.d.ts +4 -0
- package/src/generators/service/service.js +5 -0
- package/src/generators/service/service.js.map +1 -0
- package/src/generators/slice/schema.d.ts +7 -0
- package/src/generators/slice/schema.json +38 -0
- package/src/generators/slice/slice.d.ts +26 -0
- package/src/generators/slice/slice.js +1698 -0
- package/src/generators/slice/slice.js.map +1 -0
- package/src/generators/upgrade/schema.d.ts +8 -0
- package/src/generators/upgrade/schema.json +31 -0
- package/src/generators/upgrade/upgrade.d.ts +27 -0
- package/src/generators/upgrade/upgrade.js +682 -0
- package/src/generators/upgrade/upgrade.js.map +1 -0
- package/src/generators/workload/schema.d.ts +6 -0
- package/src/generators/workload/schema.json +21 -0
- package/src/generators/workload/workload.d.ts +16 -0
- package/src/generators/workload/workload.js +157 -0
- package/src/generators/workload/workload.js.map +1 -0
- package/src/git-policy/action-pins.d.ts +28 -0
- package/src/git-policy/action-pins.js +35 -0
- package/src/git-policy/action-pins.js.map +1 -0
- package/src/git-policy/github-bootstrap.d.ts +23 -0
- package/src/git-policy/github-bootstrap.js +759 -0
- package/src/git-policy/github-bootstrap.js.map +1 -0
- package/src/git-policy/index.d.ts +3 -0
- package/src/git-policy/index.js +7 -0
- package/src/git-policy/index.js.map +1 -0
- package/src/git-policy/routing.d.ts +15 -0
- package/src/git-policy/routing.js +77 -0
- package/src/git-policy/routing.js.map +1 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +12 -0
- package/src/index.js.map +1 -0
- package/src/local-dev/contracts.d.ts +31 -0
- package/src/local-dev/contracts.js +126 -0
- package/src/local-dev/contracts.js.map +1 -0
- package/src/local-dev/effects.d.ts +16 -0
- package/src/local-dev/effects.js +63 -0
- package/src/local-dev/effects.js.map +1 -0
- package/src/local-dev/gateway-daemon.d.ts +4 -0
- package/src/local-dev/gateway-daemon.js +39 -0
- package/src/local-dev/gateway-daemon.js.map +1 -0
- package/src/local-dev/gateway.d.ts +60 -0
- package/src/local-dev/gateway.js +368 -0
- package/src/local-dev/gateway.js.map +1 -0
- package/src/local-dev/identity.d.ts +16 -0
- package/src/local-dev/identity.js +101 -0
- package/src/local-dev/identity.js.map +1 -0
- package/src/local-dev/index.d.ts +7 -0
- package/src/local-dev/index.js +11 -0
- package/src/local-dev/index.js.map +1 -0
- package/src/local-dev/portless.d.ts +15 -0
- package/src/local-dev/portless.js +115 -0
- package/src/local-dev/portless.js.map +1 -0
- package/src/local-dev/ports.d.ts +10 -0
- package/src/local-dev/ports.js +39 -0
- package/src/local-dev/ports.js.map +1 -0
- package/src/local-dev/proxy.d.ts +37 -0
- package/src/local-dev/proxy.js +271 -0
- package/src/local-dev/proxy.js.map +1 -0
- package/src/local-dev/tls.d.ts +24 -0
- package/src/local-dev/tls.js +294 -0
- package/src/local-dev/tls.js.map +1 -0
- package/src/ownership/composition.d.ts +16 -0
- package/src/ownership/composition.js +125 -0
- package/src/ownership/composition.js.map +1 -0
- package/src/ownership/foundation-record.d.ts +66 -0
- package/src/ownership/foundation-record.js +108 -0
- package/src/ownership/foundation-record.js.map +1 -0
- package/src/ownership/hash.d.ts +2 -0
- package/src/ownership/hash.js +10 -0
- package/src/ownership/hash.js.map +1 -0
- package/src/ownership/index.d.ts +6 -0
- package/src/ownership/index.js +10 -0
- package/src/ownership/index.js.map +1 -0
- package/src/ownership/json-ownership.d.ts +13 -0
- package/src/ownership/json-ownership.js +147 -0
- package/src/ownership/json-ownership.js.map +1 -0
- package/src/ownership/record.d.ts +186 -0
- package/src/ownership/record.js +336 -0
- package/src/ownership/record.js.map +1 -0
- package/src/ownership/workspace-record.d.ts +66 -0
- package/src/ownership/workspace-record.js +108 -0
- package/src/ownership/workspace-record.js.map +1 -0
- package/src/repository-policy/aurora-preflight.d.ts +15 -0
- package/src/repository-policy/aurora-preflight.js +45 -0
- package/src/repository-policy/aurora-preflight.js.map +1 -0
- package/src/repository-policy/deployment-plan.d.ts +24 -0
- package/src/repository-policy/deployment-plan.js +124 -0
- package/src/repository-policy/deployment-plan.js.map +1 -0
- package/src/repository-policy/foundation-profile.d.ts +20 -0
- package/src/repository-policy/foundation-profile.js +45 -0
- package/src/repository-policy/foundation-profile.js.map +1 -0
- package/src/repository-policy/index.d.ts +4 -0
- package/src/repository-policy/index.js +8 -0
- package/src/repository-policy/index.js.map +1 -0
- package/src/repository-policy/policy.d.ts +59 -0
- package/src/repository-policy/policy.js +274 -0
- package/src/repository-policy/policy.js.map +1 -0
- package/src/schemas/blueprint-input-schema-v1.json +23 -0
- package/src/schemas/blueprint-v1.json +186 -0
- package/src/schemas/foundation-ownership-v2.json +42 -0
- package/src/schemas/foundation-requirement-v1.json +32 -0
- package/src/schemas/generated-ownership-v2.json +117 -0
- package/src/schemas/repository-policy-v1.json +58 -0
- package/src/schemas/workspace-ownership-v2.json +42 -0
- package/src/validation.d.ts +14 -0
- package/src/validation.js +14 -0
- package/src/validation.js.map +1 -0
- package/src/work-markers/index.d.ts +1 -0
- package/src/work-markers/index.js +5 -0
- package/src/work-markers/index.js.map +1 -0
- package/src/work-markers/work-markers.d.ts +39 -0
- package/src/work-markers/work-markers.js +359 -0
- package/src/work-markers/work-markers.js.map +1 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EBK_CODE_OWNER_PLACEHOLDER = exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES = exports.EBK_DEPLOYMENT_ROLE_SECRET = exports.EBK_REQUIRED_CHECKS = exports.EBK_PROTECTED_BRANCH_RULESET_NAME = void 0;
|
|
4
|
+
exports.runGitHubBootstrap = runGitHubBootstrap;
|
|
5
|
+
const index_js_1 = require("../repository-policy/index.js");
|
|
6
|
+
exports.EBK_PROTECTED_BRANCH_RULESET_NAME = 'Empire Builder Kit protected branches';
|
|
7
|
+
exports.EBK_REQUIRED_CHECKS = ['policy', 'verify'];
|
|
8
|
+
exports.EBK_DEPLOYMENT_ROLE_SECRET = 'AWS_DEPLOY_ROLE_ARN';
|
|
9
|
+
exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES = [
|
|
10
|
+
'AWS_EXPECTED_ACCOUNT_ID',
|
|
11
|
+
'AWS_REGION',
|
|
12
|
+
'EBK_ALLOW_PRODUCTION_REMOVE',
|
|
13
|
+
];
|
|
14
|
+
exports.EBK_CODE_OWNER_PLACEHOLDER = '@REPLACE_WITH_GITHUB_CODE_OWNER';
|
|
15
|
+
const GITHUB_API_VERSION = '2022-11-28';
|
|
16
|
+
const PROTECTED_BRANCHES = ['develop', 'main'];
|
|
17
|
+
const ENVIRONMENT_BRANCHES = {
|
|
18
|
+
production: 'main',
|
|
19
|
+
staging: 'develop',
|
|
20
|
+
};
|
|
21
|
+
const PRIVATE_ENVIRONMENT_REVIEWER_PLANS = new Set([
|
|
22
|
+
'business_plus',
|
|
23
|
+
'enterprise',
|
|
24
|
+
'enterprise_cloud',
|
|
25
|
+
]);
|
|
26
|
+
class GitHubApiError extends Error {
|
|
27
|
+
status;
|
|
28
|
+
constructor(status, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.status = status;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class GitHubApi {
|
|
34
|
+
token;
|
|
35
|
+
baseUrl;
|
|
36
|
+
constructor(baseUrl, token) {
|
|
37
|
+
this.token = token;
|
|
38
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
39
|
+
}
|
|
40
|
+
async request(method, path, body) {
|
|
41
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
42
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: 'application/vnd.github+json',
|
|
45
|
+
Authorization: `Bearer ${this.token}`,
|
|
46
|
+
'X-GitHub-Api-Version': GITHUB_API_VERSION,
|
|
47
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
48
|
+
},
|
|
49
|
+
method,
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
let detail = response.statusText;
|
|
53
|
+
try {
|
|
54
|
+
const payload = (await response.json());
|
|
55
|
+
detail = payload.message ?? detail;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// The status and endpoint are sufficient for a safe diagnostic.
|
|
59
|
+
}
|
|
60
|
+
throw new GitHubApiError(response.status, `GitHub ${method} ${path} failed (${response.status}): ${detail}`);
|
|
61
|
+
}
|
|
62
|
+
if (response.status === 204)
|
|
63
|
+
return undefined;
|
|
64
|
+
return (await response.json());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseRepository(repository) {
|
|
68
|
+
const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(repository.trim());
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new Error('GitHub repository must be explicit and formatted as owner/repository.');
|
|
71
|
+
}
|
|
72
|
+
return { owner: match[1], repo: match[2] };
|
|
73
|
+
}
|
|
74
|
+
function asObjects(value) {
|
|
75
|
+
return Array.isArray(value)
|
|
76
|
+
? value.filter((item) => typeof item === 'object' && item !== null && !Array.isArray(item))
|
|
77
|
+
: [];
|
|
78
|
+
}
|
|
79
|
+
function asStrings(value) {
|
|
80
|
+
return Array.isArray(value)
|
|
81
|
+
? value.filter((item) => typeof item === 'string')
|
|
82
|
+
: [];
|
|
83
|
+
}
|
|
84
|
+
function requiresPrivateEnvironmentCapabilities(repo) {
|
|
85
|
+
if (typeof repo.visibility === 'string') {
|
|
86
|
+
return repo.visibility !== 'public';
|
|
87
|
+
}
|
|
88
|
+
if (typeof repo.private === 'boolean')
|
|
89
|
+
return repo.private;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function namedValues(value, property = 'name') {
|
|
93
|
+
return asObjects(value)
|
|
94
|
+
.map((item) => item[property])
|
|
95
|
+
.filter((item) => typeof item === 'string')
|
|
96
|
+
.sort();
|
|
97
|
+
}
|
|
98
|
+
async function readPagedObjects(api, path, collectionProperty) {
|
|
99
|
+
const values = [];
|
|
100
|
+
for (let page = 1; page <= 100; page += 1) {
|
|
101
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
102
|
+
const payload = await api.request('GET', `${path}${separator}per_page=100&page=${page}`);
|
|
103
|
+
const pageValues = collectionProperty
|
|
104
|
+
? asObjects(typeof payload === 'object' && payload !== null
|
|
105
|
+
? payload[collectionProperty]
|
|
106
|
+
: undefined)
|
|
107
|
+
: asObjects(payload);
|
|
108
|
+
values.push(...pageValues);
|
|
109
|
+
if (pageValues.length < 100)
|
|
110
|
+
return values;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`GitHub pagination exceeded 10,000 records for ${path}.`);
|
|
113
|
+
}
|
|
114
|
+
function rulesetRules(ruleset) {
|
|
115
|
+
return asObjects(ruleset.rules);
|
|
116
|
+
}
|
|
117
|
+
function ruleParameters(rule) {
|
|
118
|
+
return typeof rule.parameters === 'object' && rule.parameters !== null
|
|
119
|
+
? rule.parameters
|
|
120
|
+
: {};
|
|
121
|
+
}
|
|
122
|
+
function activeBranchRuleset(ruleset) {
|
|
123
|
+
return ruleset.target === 'branch' && ruleset.enforcement === 'active';
|
|
124
|
+
}
|
|
125
|
+
function refPatternMatches(pattern, ref) {
|
|
126
|
+
if (pattern === '~ALL')
|
|
127
|
+
return true;
|
|
128
|
+
if (pattern === '~DEFAULT_BRANCH')
|
|
129
|
+
return ref === 'refs/heads/main';
|
|
130
|
+
const expression = pattern
|
|
131
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
132
|
+
.replace(/\*\*/g, '*')
|
|
133
|
+
.replace(/\*/g, '.*')
|
|
134
|
+
.replace(/\?/g, '.');
|
|
135
|
+
return new RegExp(`^${expression}$`).test(ref);
|
|
136
|
+
}
|
|
137
|
+
function rulesetTouchesProtectedBranches(ruleset) {
|
|
138
|
+
const conditions = typeof ruleset.conditions === 'object' && ruleset.conditions !== null
|
|
139
|
+
? ruleset.conditions
|
|
140
|
+
: {};
|
|
141
|
+
const refName = typeof conditions.ref_name === 'object' && conditions.ref_name !== null
|
|
142
|
+
? conditions.ref_name
|
|
143
|
+
: undefined;
|
|
144
|
+
if (!refName)
|
|
145
|
+
return true;
|
|
146
|
+
const include = asStrings(refName.include);
|
|
147
|
+
const exclude = asStrings(refName.exclude);
|
|
148
|
+
return PROTECTED_BRANCHES.some((branch) => {
|
|
149
|
+
const ref = `refs/heads/${branch}`;
|
|
150
|
+
const included = include.length === 0 ||
|
|
151
|
+
include.some((pattern) => refPatternMatches(pattern, ref));
|
|
152
|
+
const excluded = exclude.some((pattern) => refPatternMatches(pattern, ref));
|
|
153
|
+
return included && !excluded;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function conflictsWithMergeCommitContract(ruleset) {
|
|
157
|
+
if (!activeBranchRuleset(ruleset) ||
|
|
158
|
+
!rulesetTouchesProtectedBranches(ruleset)) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const conflicts = [];
|
|
162
|
+
for (const rule of rulesetRules(ruleset)) {
|
|
163
|
+
if (rule.type === 'required_linear_history') {
|
|
164
|
+
conflicts.push('requires linear history');
|
|
165
|
+
}
|
|
166
|
+
if (rule.type === 'pull_request') {
|
|
167
|
+
const methods = asStrings(ruleParameters(rule).allowed_merge_methods);
|
|
168
|
+
if (methods.length > 0 && !methods.includes('merge')) {
|
|
169
|
+
conflicts.push('does not permit merge commits');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return conflicts;
|
|
174
|
+
}
|
|
175
|
+
function reviewerState(environment) {
|
|
176
|
+
const rule = asObjects(environment.protection_rules).find((candidate) => candidate.type === 'required_reviewers');
|
|
177
|
+
if (!rule)
|
|
178
|
+
return { preventSelfReview: false, reviewers: [] };
|
|
179
|
+
const reviewers = asObjects(rule.reviewers).flatMap((entry) => {
|
|
180
|
+
const reviewer = typeof entry.reviewer === 'object' && entry.reviewer !== null
|
|
181
|
+
? entry.reviewer
|
|
182
|
+
: {};
|
|
183
|
+
const id = reviewer.id;
|
|
184
|
+
const candidateType = entry.type ?? reviewer.type;
|
|
185
|
+
if (typeof id !== 'number' ||
|
|
186
|
+
!Number.isSafeInteger(id) ||
|
|
187
|
+
(candidateType !== 'Team' && candidateType !== 'User')) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return [{ id, type: candidateType }];
|
|
191
|
+
});
|
|
192
|
+
reviewers.sort((left, right) => left.type === right.type
|
|
193
|
+
? left.id - right.id
|
|
194
|
+
: left.type.localeCompare(right.type));
|
|
195
|
+
return {
|
|
196
|
+
preventSelfReview: rule.prevent_self_review === true,
|
|
197
|
+
reviewers,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function environmentWaitTimer(environment) {
|
|
201
|
+
const rule = asObjects(environment.protection_rules).find((candidate) => candidate.type === 'wait_timer');
|
|
202
|
+
const waitTimer = rule?.wait_timer;
|
|
203
|
+
return typeof waitTimer === 'number' && Number.isSafeInteger(waitTimer)
|
|
204
|
+
? waitTimer
|
|
205
|
+
: 0;
|
|
206
|
+
}
|
|
207
|
+
function hasValidCatchAllCodeOwner(contents) {
|
|
208
|
+
const catchAllOwners = contents.split(/\r?\n/).flatMap((line) => {
|
|
209
|
+
const withoutComment = line.split('#', 1)[0]?.trim() ?? '';
|
|
210
|
+
if (!withoutComment)
|
|
211
|
+
return [];
|
|
212
|
+
const [pattern, ...owners] = withoutComment.split(/\s+/);
|
|
213
|
+
return pattern === '*' ? [owners] : [];
|
|
214
|
+
});
|
|
215
|
+
const effectiveOwners = catchAllOwners.at(-1);
|
|
216
|
+
return (effectiveOwners !== undefined &&
|
|
217
|
+
effectiveOwners.length > 0 &&
|
|
218
|
+
effectiveOwners.every((owner) => owner.startsWith('@') && owner !== exports.EBK_CODE_OWNER_PLACEHOLDER));
|
|
219
|
+
}
|
|
220
|
+
function desiredRuleset() {
|
|
221
|
+
return {
|
|
222
|
+
bypass_actors: [],
|
|
223
|
+
conditions: {
|
|
224
|
+
ref_name: {
|
|
225
|
+
exclude: [],
|
|
226
|
+
include: PROTECTED_BRANCHES.map((branch) => `refs/heads/${branch}`),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
enforcement: 'active',
|
|
230
|
+
name: exports.EBK_PROTECTED_BRANCH_RULESET_NAME,
|
|
231
|
+
rules: [
|
|
232
|
+
{ type: 'deletion' },
|
|
233
|
+
{ type: 'non_fast_forward' },
|
|
234
|
+
{
|
|
235
|
+
parameters: {
|
|
236
|
+
allowed_merge_methods: ['merge'],
|
|
237
|
+
dismiss_stale_reviews_on_push: true,
|
|
238
|
+
require_code_owner_review: true,
|
|
239
|
+
require_last_push_approval: true,
|
|
240
|
+
required_approving_review_count: 1,
|
|
241
|
+
required_review_thread_resolution: true,
|
|
242
|
+
},
|
|
243
|
+
type: 'pull_request',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
parameters: {
|
|
247
|
+
required_status_checks: exports.EBK_REQUIRED_CHECKS.map((context) => ({
|
|
248
|
+
context,
|
|
249
|
+
})),
|
|
250
|
+
strict_required_status_checks_policy: true,
|
|
251
|
+
},
|
|
252
|
+
type: 'required_status_checks',
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
target: 'branch',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function inspectEnvironment(api, repositoryPath, name) {
|
|
259
|
+
const encoded = encodeURIComponent(name);
|
|
260
|
+
let environment;
|
|
261
|
+
try {
|
|
262
|
+
environment = await api.request('GET', `${repositoryPath}/environments/${encoded}`);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (error instanceof GitHubApiError && error.status === 404) {
|
|
266
|
+
return {
|
|
267
|
+
branchPolicies: [],
|
|
268
|
+
customBranchPolicies: false,
|
|
269
|
+
exists: false,
|
|
270
|
+
preventSelfReview: false,
|
|
271
|
+
reviewers: [],
|
|
272
|
+
secretNames: [],
|
|
273
|
+
variableNames: [],
|
|
274
|
+
waitTimer: 0,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
const customBranchPolicies = typeof environment.deployment_branch_policy === 'object' &&
|
|
280
|
+
environment.deployment_branch_policy !== null &&
|
|
281
|
+
environment.deployment_branch_policy
|
|
282
|
+
.custom_branch_policies === true;
|
|
283
|
+
let branchPolicyValues = [];
|
|
284
|
+
try {
|
|
285
|
+
const branchPolicies = await readPagedObjects(api, `${repositoryPath}/environments/${encoded}/deployment-branch-policies`, 'branch_policies');
|
|
286
|
+
branchPolicyValues = namedValues(branchPolicies);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
if (!(error instanceof GitHubApiError &&
|
|
290
|
+
error.status === 404 &&
|
|
291
|
+
!customBranchPolicies)) {
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const secrets = await readPagedObjects(api, `${repositoryPath}/environments/${encoded}/secrets`, 'secrets');
|
|
296
|
+
const variables = await readPagedObjects(api, `${repositoryPath}/environments/${encoded}/variables`, 'variables');
|
|
297
|
+
const reviewers = reviewerState(environment);
|
|
298
|
+
return {
|
|
299
|
+
branchPolicies: branchPolicyValues,
|
|
300
|
+
customBranchPolicies,
|
|
301
|
+
exists: true,
|
|
302
|
+
preventSelfReview: reviewers.preventSelfReview,
|
|
303
|
+
reviewers: reviewers.reviewers,
|
|
304
|
+
secretNames: namedValues(secrets),
|
|
305
|
+
variableNames: namedValues(variables),
|
|
306
|
+
waitTimer: environmentWaitTimer(environment),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function inspect(api, repositoryPath, productionApprovalMode) {
|
|
310
|
+
const blockers = [];
|
|
311
|
+
const repo = await api.request('GET', repositoryPath);
|
|
312
|
+
let ownerPlanName;
|
|
313
|
+
if (productionApprovalMode === 'environment-reviewer' &&
|
|
314
|
+
requiresPrivateEnvironmentCapabilities(repo)) {
|
|
315
|
+
const owner = typeof repo.owner === 'object' && repo.owner !== null
|
|
316
|
+
? repo.owner
|
|
317
|
+
: {};
|
|
318
|
+
const login = typeof owner.login === 'string' ? owner.login : undefined;
|
|
319
|
+
const ownerType = owner.type;
|
|
320
|
+
if (!login || (ownerType !== 'Organization' && ownerType !== 'User')) {
|
|
321
|
+
blockers.push('Private-repository GitHub plan capability could not be resolved from the repository owner.');
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const ownerPath = `${ownerType === 'Organization' ? '/orgs' : '/users'}/${encodeURIComponent(login)}`;
|
|
325
|
+
try {
|
|
326
|
+
const ownerState = await api.request('GET', ownerPath);
|
|
327
|
+
const plan = typeof ownerState.plan === 'object' && ownerState.plan !== null
|
|
328
|
+
? ownerState.plan
|
|
329
|
+
: {};
|
|
330
|
+
ownerPlanName =
|
|
331
|
+
typeof plan.name === 'string' ? plan.name.toLowerCase() : undefined;
|
|
332
|
+
if (!ownerPlanName) {
|
|
333
|
+
blockers.push('Private-repository GitHub plan capability could not be verified; environment-reviewer mode fails closed.');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
blockers.push(`Private-repository GitHub plan capability could not be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const branch of PROTECTED_BRANCHES) {
|
|
342
|
+
try {
|
|
343
|
+
const codeOwners = await api.request('GET', `${repositoryPath}/contents/.github/CODEOWNERS?ref=${branch}`);
|
|
344
|
+
const content = codeOwners.content;
|
|
345
|
+
const encoding = codeOwners.encoding;
|
|
346
|
+
if (typeof content !== 'string' ||
|
|
347
|
+
encoding !== 'base64' ||
|
|
348
|
+
!hasValidCatchAllCodeOwner(Buffer.from(content, 'base64').toString('utf8'))) {
|
|
349
|
+
blockers.push(`Committed .github/CODEOWNERS on ${branch} must contain a valid effective final non-placeholder catch-all owner before code-owner review can be enforced.`);
|
|
350
|
+
}
|
|
351
|
+
const errors = await api.request('GET', `${repositoryPath}/codeowners/errors?ref=${branch}`);
|
|
352
|
+
const codeOwnerErrors = asObjects(errors.errors);
|
|
353
|
+
if (codeOwnerErrors.length > 0) {
|
|
354
|
+
blockers.push(`GitHub reports ${codeOwnerErrors.length} CODEOWNERS error${codeOwnerErrors.length === 1 ? '' : 's'} on ${branch}.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
blockers.push(`CODEOWNERS enforcement on ${branch} could not be verified: ${error instanceof Error ? error.message : String(error)}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
let rulesets = [];
|
|
362
|
+
try {
|
|
363
|
+
const summaries = await readPagedObjects(api, `${repositoryPath}/rulesets?includes_parents=true`);
|
|
364
|
+
rulesets = await Promise.all(summaries.map(async (summary) => {
|
|
365
|
+
const id = summary.id;
|
|
366
|
+
return typeof id === 'number'
|
|
367
|
+
? api.request('GET', `${repositoryPath}/rulesets/${id}`)
|
|
368
|
+
: summary;
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
blockers.push(`Ruleset enforcement could not be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
373
|
+
}
|
|
374
|
+
for (const branch of PROTECTED_BRANCHES) {
|
|
375
|
+
try {
|
|
376
|
+
const branchState = await api.request('GET', `${repositoryPath}/branches/${branch}`);
|
|
377
|
+
const commit = typeof branchState.commit === 'object' && branchState.commit !== null
|
|
378
|
+
? branchState.commit
|
|
379
|
+
: {};
|
|
380
|
+
const sha = typeof commit.sha === 'string' ? commit.sha : branch;
|
|
381
|
+
const checks = await readPagedObjects(api, `${repositoryPath}/commits/${encodeURIComponent(sha)}/check-runs`, 'check_runs');
|
|
382
|
+
const names = new Set(namedValues(checks));
|
|
383
|
+
for (const required of exports.EBK_REQUIRED_CHECKS) {
|
|
384
|
+
if (!names.has(required)) {
|
|
385
|
+
blockers.push(`${branch} has not emitted the required ${required} check context.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
blockers.push(`${branch} branch/check discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
let repositorySecretNames = [];
|
|
394
|
+
try {
|
|
395
|
+
const secrets = await readPagedObjects(api, `${repositoryPath}/actions/secrets`, 'secrets');
|
|
396
|
+
repositorySecretNames = namedValues(secrets);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
blockers.push(`Repository secret inventory could not be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
400
|
+
}
|
|
401
|
+
let repositoryVariableNames = [];
|
|
402
|
+
try {
|
|
403
|
+
const variables = await readPagedObjects(api, `${repositoryPath}/actions/variables`, 'variables');
|
|
404
|
+
repositoryVariableNames = namedValues(variables);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
blockers.push(`Repository variable inventory could not be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
408
|
+
}
|
|
409
|
+
let orgSecretNames;
|
|
410
|
+
let orgVariableNames;
|
|
411
|
+
const owner = typeof repo.owner === 'object' && repo.owner !== null
|
|
412
|
+
? repo.owner
|
|
413
|
+
: undefined;
|
|
414
|
+
if (owner?.type === 'User') {
|
|
415
|
+
orgSecretNames = [];
|
|
416
|
+
orgVariableNames = [];
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
try {
|
|
420
|
+
const secrets = await readPagedObjects(api, `${repositoryPath}/actions/organization-secrets`, 'secrets');
|
|
421
|
+
orgSecretNames = namedValues(secrets);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
blockers.push(`Organization secret exposure is unverified and requires a credential with inspection access: ${error instanceof Error ? error.message : String(error)}`);
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const variables = await readPagedObjects(api, `${repositoryPath}/actions/organization-variables`, 'variables');
|
|
428
|
+
orgVariableNames = namedValues(variables);
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
blockers.push(`Organization variable exposure is unverified and requires a credential with inspection access: ${error instanceof Error ? error.message : String(error)}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let actionsCanApprovePullRequests;
|
|
435
|
+
let actionsDefaultWorkflowPermissions;
|
|
436
|
+
try {
|
|
437
|
+
const permissions = await api.request('GET', `${repositoryPath}/actions/permissions/workflow`);
|
|
438
|
+
actionsCanApprovePullRequests =
|
|
439
|
+
permissions.can_approve_pull_request_reviews === true;
|
|
440
|
+
actionsDefaultWorkflowPermissions =
|
|
441
|
+
typeof permissions.default_workflow_permissions === 'string'
|
|
442
|
+
? permissions.default_workflow_permissions
|
|
443
|
+
: undefined;
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
blockers.push(`Workflow pull-request permission could not be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
447
|
+
}
|
|
448
|
+
const inspectNamedEnvironment = async (name) => {
|
|
449
|
+
try {
|
|
450
|
+
return await inspectEnvironment(api, repositoryPath, name);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
blockers.push(`${name} environment inventory could not be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
454
|
+
return {
|
|
455
|
+
branchPolicies: [],
|
|
456
|
+
customBranchPolicies: false,
|
|
457
|
+
exists: false,
|
|
458
|
+
preventSelfReview: false,
|
|
459
|
+
reviewers: [],
|
|
460
|
+
secretNames: [],
|
|
461
|
+
variableNames: [],
|
|
462
|
+
waitTimer: 0,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const [production, staging] = await Promise.all([
|
|
467
|
+
inspectNamedEnvironment('production'),
|
|
468
|
+
inspectNamedEnvironment('staging'),
|
|
469
|
+
]);
|
|
470
|
+
return {
|
|
471
|
+
blockers,
|
|
472
|
+
state: {
|
|
473
|
+
actionsCanApprovePullRequests,
|
|
474
|
+
actionsDefaultWorkflowPermissions,
|
|
475
|
+
environments: { production, staging },
|
|
476
|
+
orgSecretNames,
|
|
477
|
+
orgVariableNames,
|
|
478
|
+
ownerPlanName,
|
|
479
|
+
ownedRuleset: rulesets.find((ruleset) => ruleset.name === exports.EBK_PROTECTED_BRANCH_RULESET_NAME &&
|
|
480
|
+
ruleset.source_type !== 'Organization'),
|
|
481
|
+
repo,
|
|
482
|
+
repositorySecretNames,
|
|
483
|
+
repositoryVariableNames,
|
|
484
|
+
rulesets,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function evaluate(repository, state, inspectionBlockers, productionApprovalMode, productionReviewerIds) {
|
|
489
|
+
const blockers = [...inspectionBlockers];
|
|
490
|
+
const changes = [];
|
|
491
|
+
const warnings = [];
|
|
492
|
+
const privateRepository = requiresPrivateEnvironmentCapabilities(state.repo);
|
|
493
|
+
if (productionApprovalMode === 'environment-reviewer' &&
|
|
494
|
+
privateRepository &&
|
|
495
|
+
state.ownerPlanName !== undefined &&
|
|
496
|
+
!PRIVATE_ENVIRONMENT_REVIEWER_PLANS.has(state.ownerPlanName)) {
|
|
497
|
+
blockers.push(`Committed environment-reviewer mode requires GitHub Enterprise support for a private repository; the inspected owner plan is ${state.ownerPlanName}.`);
|
|
498
|
+
}
|
|
499
|
+
if (state.repo.allow_merge_commit !== true) {
|
|
500
|
+
changes.push('Enable repository merge commits.');
|
|
501
|
+
}
|
|
502
|
+
if (state.repo.allow_squash_merge !== false) {
|
|
503
|
+
changes.push('Disable repository squash merging.');
|
|
504
|
+
}
|
|
505
|
+
if (state.repo.allow_rebase_merge !== false) {
|
|
506
|
+
changes.push('Disable repository rebase merging.');
|
|
507
|
+
}
|
|
508
|
+
if (state.actionsCanApprovePullRequests !== true) {
|
|
509
|
+
blockers.push('GitHub Actions is not permitted to create or approve pull requests for managed sync-back.');
|
|
510
|
+
}
|
|
511
|
+
if (state.actionsDefaultWorkflowPermissions !== 'read') {
|
|
512
|
+
blockers.push('GitHub Actions default workflow permissions must be read-only; write access must remain job-scoped.');
|
|
513
|
+
}
|
|
514
|
+
if (state.repositorySecretNames.includes(exports.EBK_DEPLOYMENT_ROLE_SECRET)) {
|
|
515
|
+
blockers.push(`${exports.EBK_DEPLOYMENT_ROLE_SECRET} exists at repository scope and would bypass environment protection.`);
|
|
516
|
+
}
|
|
517
|
+
if (state.orgSecretNames?.includes(exports.EBK_DEPLOYMENT_ROLE_SECRET)) {
|
|
518
|
+
blockers.push(`${exports.EBK_DEPLOYMENT_ROLE_SECRET} is exposed from organization scope and would bypass environment protection.`);
|
|
519
|
+
}
|
|
520
|
+
for (const variable of exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES) {
|
|
521
|
+
if (state.repositoryVariableNames.includes(variable)) {
|
|
522
|
+
blockers.push(`${variable} exists at repository variable scope instead of its protected environment.`);
|
|
523
|
+
}
|
|
524
|
+
if (state.orgVariableNames?.includes(variable)) {
|
|
525
|
+
blockers.push(`${variable} is exposed from organization variable scope instead of its protected environment.`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const ruleset of state.rulesets) {
|
|
529
|
+
const conflicts = conflictsWithMergeCommitContract(ruleset);
|
|
530
|
+
if (conflicts.length > 0) {
|
|
531
|
+
blockers.push(`Active ruleset ${String(ruleset.name ?? ruleset.id ?? 'unknown')} ${conflicts.join(' and ')}.`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!state.ownedRuleset) {
|
|
535
|
+
changes.push(`Create ruleset ${exports.EBK_PROTECTED_BRANCH_RULESET_NAME}.`);
|
|
536
|
+
}
|
|
537
|
+
else if (JSON.stringify(normalizeRuleset(state.ownedRuleset)) !==
|
|
538
|
+
JSON.stringify(normalizeRuleset(desiredRuleset()))) {
|
|
539
|
+
changes.push(`Update ruleset ${exports.EBK_PROTECTED_BRANCH_RULESET_NAME}.`);
|
|
540
|
+
}
|
|
541
|
+
for (const [environment, branch] of Object.entries(ENVIRONMENT_BRANCHES)) {
|
|
542
|
+
const current = state.environments[environment];
|
|
543
|
+
if (!current.exists)
|
|
544
|
+
changes.push(`Create ${environment} environment.`);
|
|
545
|
+
if (!current.customBranchPolicies) {
|
|
546
|
+
changes.push(`Enable custom deployment branch policy for ${environment}.`);
|
|
547
|
+
}
|
|
548
|
+
const extras = current.branchPolicies.filter((item) => item !== branch);
|
|
549
|
+
if (extras.length > 0) {
|
|
550
|
+
blockers.push(`${environment} permits unexpected deployment branches: ${extras.join(', ')}.`);
|
|
551
|
+
}
|
|
552
|
+
if (!current.branchPolicies.includes(branch)) {
|
|
553
|
+
changes.push(`Restrict ${environment} deployment to ${branch}.`);
|
|
554
|
+
}
|
|
555
|
+
if (!current.secretNames.includes(exports.EBK_DEPLOYMENT_ROLE_SECRET)) {
|
|
556
|
+
blockers.push(`${environment} environment is missing ${exports.EBK_DEPLOYMENT_ROLE_SECRET}; bootstrap never creates secret values.`);
|
|
557
|
+
}
|
|
558
|
+
const requiredVariables = environment === 'production'
|
|
559
|
+
? exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES
|
|
560
|
+
: exports.EBK_PROTECTED_ENVIRONMENT_VARIABLES.filter((variable) => variable !== 'EBK_ALLOW_PRODUCTION_REMOVE');
|
|
561
|
+
for (const variable of requiredVariables) {
|
|
562
|
+
if (!current.variableNames.includes(variable)) {
|
|
563
|
+
blockers.push(`${environment} environment is missing required variable ${variable}; bootstrap never creates variable values.`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const production = state.environments.production;
|
|
568
|
+
if (productionApprovalMode === 'promotion-pr') {
|
|
569
|
+
warnings.push('Production authorization is the required non-author CODEOWNER approval on the develop-to-main promotion pull request; its merge starts deployment without an EBK-managed environment approval.');
|
|
570
|
+
warnings.push('The promotion approver may also be the human whose merge triggers deployment; rollback uses a reviewed protected revert and redeploy.');
|
|
571
|
+
warnings.push('A repository write actor who can run Actions may manually dispatch deploy, refresh, or unlock against the approved main tip without another human gate; remove remains separately variable-gated.');
|
|
572
|
+
if (production.reviewers.length > 0 || production.waitTimer > 0) {
|
|
573
|
+
warnings.push('Production retains stronger environment protection outside promotion-pr mode; bootstrap preserves it, so merge may wait for that additional gate.');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const requestedProductionReviewers = existingReviewerPayload(productionReviewerIds);
|
|
578
|
+
const effectiveReviewers = productionReviewerIds.length > 0
|
|
579
|
+
? requestedProductionReviewers
|
|
580
|
+
: production.reviewers;
|
|
581
|
+
if (effectiveReviewers.length === 0) {
|
|
582
|
+
blockers.push('Environment-reviewer mode requires at least one existing or explicitly supplied production reviewer ID.');
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
if (productionReviewerIds.length > 0 &&
|
|
586
|
+
JSON.stringify(requestedProductionReviewers) !==
|
|
587
|
+
JSON.stringify(production.reviewers)) {
|
|
588
|
+
changes.push('Set the explicit production environment reviewers.');
|
|
589
|
+
}
|
|
590
|
+
if (!production.preventSelfReview) {
|
|
591
|
+
changes.push('Enable production environment self-review prevention.');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (state.environments.staging.reviewers.length > 0) {
|
|
596
|
+
warnings.push('Staging has additional reviewer protection; bootstrap will preserve it.');
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
blockers: [...new Set(blockers)].sort(),
|
|
600
|
+
changes: [...new Set(changes)].sort(),
|
|
601
|
+
ready: blockers.length === 0 && changes.length === 0,
|
|
602
|
+
repository,
|
|
603
|
+
productionApprovalMode,
|
|
604
|
+
warnings: [...new Set(warnings)].sort(),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function normalizeRuleset(ruleset) {
|
|
608
|
+
const conditions = typeof ruleset.conditions === 'object' && ruleset.conditions !== null
|
|
609
|
+
? ruleset.conditions
|
|
610
|
+
: {};
|
|
611
|
+
const refName = typeof conditions.ref_name === 'object' && conditions.ref_name !== null
|
|
612
|
+
? conditions.ref_name
|
|
613
|
+
: {};
|
|
614
|
+
const rules = rulesetRules(ruleset)
|
|
615
|
+
.map((rule) => {
|
|
616
|
+
const type = String(rule.type ?? '');
|
|
617
|
+
const parameters = ruleParameters(rule);
|
|
618
|
+
if (type === 'pull_request') {
|
|
619
|
+
return {
|
|
620
|
+
parameters: {
|
|
621
|
+
allowed_merge_methods: asStrings(parameters.allowed_merge_methods).sort(),
|
|
622
|
+
dismiss_stale_reviews_on_push: parameters.dismiss_stale_reviews_on_push === true,
|
|
623
|
+
require_code_owner_review: parameters.require_code_owner_review === true,
|
|
624
|
+
require_last_push_approval: parameters.require_last_push_approval === true,
|
|
625
|
+
required_approving_review_count: parameters.required_approving_review_count,
|
|
626
|
+
required_review_thread_resolution: parameters.required_review_thread_resolution === true,
|
|
627
|
+
},
|
|
628
|
+
type,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (type === 'required_status_checks') {
|
|
632
|
+
return {
|
|
633
|
+
parameters: {
|
|
634
|
+
required_status_checks: asObjects(parameters.required_status_checks)
|
|
635
|
+
.map(({ context }) => ({ context }))
|
|
636
|
+
.sort((left, right) => String(left.context).localeCompare(String(right.context))),
|
|
637
|
+
strict_required_status_checks_policy: parameters.strict_required_status_checks_policy === true,
|
|
638
|
+
},
|
|
639
|
+
type,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return { type };
|
|
643
|
+
})
|
|
644
|
+
.sort((left, right) => String(left.type).localeCompare(String(right.type)));
|
|
645
|
+
return {
|
|
646
|
+
bypass_actors: Array.isArray(ruleset.bypass_actors)
|
|
647
|
+
? ruleset.bypass_actors
|
|
648
|
+
: [],
|
|
649
|
+
conditions: {
|
|
650
|
+
ref_name: {
|
|
651
|
+
exclude: asStrings(refName.exclude).sort(),
|
|
652
|
+
include: asStrings(refName.include).sort(),
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
enforcement: ruleset.enforcement,
|
|
656
|
+
name: ruleset.name,
|
|
657
|
+
rules,
|
|
658
|
+
target: ruleset.target,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function existingReviewerPayload(ids) {
|
|
662
|
+
return ids.map((id) => ({ id, type: 'User' }));
|
|
663
|
+
}
|
|
664
|
+
function rulesetMatchesDesired(ruleset) {
|
|
665
|
+
return (JSON.stringify(normalizeRuleset(ruleset)) ===
|
|
666
|
+
JSON.stringify(normalizeRuleset(desiredRuleset())));
|
|
667
|
+
}
|
|
668
|
+
async function applyChanges(api, repositoryPath, state, productionApprovalMode, productionReviewerIds) {
|
|
669
|
+
if (state.repo.allow_merge_commit !== true ||
|
|
670
|
+
state.repo.allow_rebase_merge !== false ||
|
|
671
|
+
state.repo.allow_squash_merge !== false) {
|
|
672
|
+
await api.request('PATCH', repositoryPath, {
|
|
673
|
+
allow_merge_commit: true,
|
|
674
|
+
allow_rebase_merge: false,
|
|
675
|
+
allow_squash_merge: false,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const desired = desiredRuleset();
|
|
679
|
+
if (state.ownedRuleset &&
|
|
680
|
+
typeof state.ownedRuleset.id === 'number' &&
|
|
681
|
+
!rulesetMatchesDesired(state.ownedRuleset)) {
|
|
682
|
+
await api.request('PUT', `${repositoryPath}/rulesets/${state.ownedRuleset.id}`, desired);
|
|
683
|
+
}
|
|
684
|
+
else if (!state.ownedRuleset) {
|
|
685
|
+
await api.request('POST', `${repositoryPath}/rulesets`, desired);
|
|
686
|
+
}
|
|
687
|
+
for (const [environment, branch] of Object.entries(ENVIRONMENT_BRANCHES)) {
|
|
688
|
+
const current = state.environments[environment];
|
|
689
|
+
const reviewers = environment === 'production' &&
|
|
690
|
+
productionApprovalMode === 'environment-reviewer' &&
|
|
691
|
+
productionReviewerIds.length > 0
|
|
692
|
+
? existingReviewerPayload(productionReviewerIds)
|
|
693
|
+
: current.reviewers;
|
|
694
|
+
const desiredPreventSelfReview = environment === 'production' &&
|
|
695
|
+
productionApprovalMode === 'environment-reviewer'
|
|
696
|
+
? true
|
|
697
|
+
: current.preventSelfReview;
|
|
698
|
+
const reviewersChanged = JSON.stringify(reviewers) !== JSON.stringify(current.reviewers);
|
|
699
|
+
if (!current.exists ||
|
|
700
|
+
!current.customBranchPolicies ||
|
|
701
|
+
reviewersChanged ||
|
|
702
|
+
desiredPreventSelfReview !== current.preventSelfReview) {
|
|
703
|
+
const configureProductionReviewers = environment === 'production' &&
|
|
704
|
+
productionApprovalMode === 'environment-reviewer';
|
|
705
|
+
const preserveEnvironmentProtection = configureProductionReviewers ||
|
|
706
|
+
reviewers.length > 0 ||
|
|
707
|
+
current.waitTimer > 0;
|
|
708
|
+
await api.request('PUT', `${repositoryPath}/environments/${encodeURIComponent(environment)}`, {
|
|
709
|
+
deployment_branch_policy: {
|
|
710
|
+
custom_branch_policies: true,
|
|
711
|
+
protected_branches: false,
|
|
712
|
+
},
|
|
713
|
+
...(preserveEnvironmentProtection
|
|
714
|
+
? {
|
|
715
|
+
prevent_self_review: desiredPreventSelfReview,
|
|
716
|
+
reviewers,
|
|
717
|
+
wait_timer: current.waitTimer,
|
|
718
|
+
}
|
|
719
|
+
: {}),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
if (!current.branchPolicies.includes(branch)) {
|
|
723
|
+
await api.request('POST', `${repositoryPath}/environments/${encodeURIComponent(environment)}/deployment-branch-policies`, { name: branch, type: 'branch' });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function runGitHubBootstrap(options) {
|
|
728
|
+
const repository = options.repository.trim();
|
|
729
|
+
const { owner, repo } = parseRepository(repository);
|
|
730
|
+
if (!options.token.trim()) {
|
|
731
|
+
throw new Error('GH_TOKEN or GITHUB_TOKEN is required for GitHub bootstrap.');
|
|
732
|
+
}
|
|
733
|
+
if (!index_js_1.PRODUCTION_APPROVAL_MODES.includes(options.productionApprovalMode)) {
|
|
734
|
+
throw new Error('Production approval mode must be the explicit committed value promotion-pr or environment-reviewer.');
|
|
735
|
+
}
|
|
736
|
+
const productionReviewerIds = [
|
|
737
|
+
...new Set(options.productionReviewerIds ?? []),
|
|
738
|
+
].sort((left, right) => left - right);
|
|
739
|
+
if (productionReviewerIds.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
|
|
740
|
+
throw new Error('Production reviewer IDs must be positive integers.');
|
|
741
|
+
}
|
|
742
|
+
if (options.productionApprovalMode === 'promotion-pr' &&
|
|
743
|
+
productionReviewerIds.length > 0) {
|
|
744
|
+
throw new Error('Production reviewer IDs are valid only when committed policy selects environment-reviewer mode.');
|
|
745
|
+
}
|
|
746
|
+
const api = new GitHubApi(options.apiBaseUrl?.trim() || 'https://api.github.com', options.token);
|
|
747
|
+
const repositoryPath = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
748
|
+
const initial = await inspect(api, repositoryPath, options.productionApprovalMode);
|
|
749
|
+
const report = evaluate(repository, initial.state, initial.blockers, options.productionApprovalMode, productionReviewerIds);
|
|
750
|
+
if (!options.apply)
|
|
751
|
+
return report;
|
|
752
|
+
if (report.blockers.length > 0) {
|
|
753
|
+
throw new Error(`GitHub bootstrap apply is blocked:\n- ${report.blockers.join('\n- ')}`);
|
|
754
|
+
}
|
|
755
|
+
await applyChanges(api, repositoryPath, initial.state, options.productionApprovalMode, productionReviewerIds);
|
|
756
|
+
const completed = await inspect(api, repositoryPath, options.productionApprovalMode);
|
|
757
|
+
return evaluate(repository, completed.state, completed.blockers, options.productionApprovalMode, productionReviewerIds);
|
|
758
|
+
}
|
|
759
|
+
//# sourceMappingURL=github-bootstrap.js.map
|