reposets 0.4.2 → 1.0.0
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/README.md +87 -75
- package/bin/reposets.js +68 -17
- package/cli/commands/credentials.js +170 -49
- package/cli/commands/doctor.js +364 -91
- package/cli/commands/drift.js +48 -0
- package/cli/commands/history.js +203 -0
- package/cli/commands/init.js +110 -104
- package/cli/commands/list.js +60 -39
- package/cli/commands/nuke.js +127 -0
- package/cli/commands/sync.js +219 -59
- package/cli/commands/validate.js +57 -41
- package/cli/flags.js +36 -0
- package/cli/logger.js +48 -0
- package/index.d.ts +471 -1499
- package/index.js +3 -18
- package/lib/config-refs.js +76 -0
- package/lib/credential-labels.js +0 -0
- package/lib/fingerprint.js +52 -0
- package/lib/org-only.js +61 -0
- package/lib/schema-issues.js +50 -0
- package/package.json +11 -10
- package/schemas/annotations.js +81 -0
- package/schemas/common.js +83 -48
- package/schemas/config.js +214 -212
- package/schemas/credentials.js +190 -54
- package/schemas/environment.js +27 -21
- package/schemas/ruleset.js +235 -139
- package/services/ConfigFiles.js +126 -104
- package/services/CredentialResolver.js +97 -33
- package/services/OnePasswordClient.js +88 -16
- package/services/SyncLogger.js +107 -76
- package/store/AppliedState.js +0 -0
- package/store/RepoCache.js +86 -0
- package/store/SyncJournal.js +92 -0
- package/store/migrations.js +86 -0
- package/sync/SyncEngine.js +156 -0
- package/sync/decide.js +56 -0
- package/sync/phase.js +55 -0
- package/sync/phases/cleanup.js +220 -0
- package/sync/phases/code-scanning.js +187 -0
- package/sync/phases/environments.js +106 -0
- package/sync/phases/index.js +39 -0
- package/sync/phases/resource.js +149 -0
- package/sync/phases/rulesets.js +186 -0
- package/sync/phases/secrets.js +138 -0
- package/sync/phases/security.js +129 -0
- package/sync/phases/settings.js +274 -0
- package/sync/phases/variables.js +132 -0
- package/tsdoc-metadata.json +1 -1
- package/bin/reposets.d.ts +0 -1
- package/errors.js +0 -12
- package/lib/crypto.js +0 -27
- package/services/GitHubClient.js +0 -875
- package/services/SyncEngine.js +0 -580
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { SyncLogger } from "../../services/SyncLogger.js";
|
|
2
|
+
import { AppliedState, lookup } from "../../store/AppliedState.js";
|
|
3
|
+
import { RepoCache } from "../../store/RepoCache.js";
|
|
4
|
+
import { read } from "./resource.js";
|
|
5
|
+
import { fingerprint } from "../../lib/fingerprint.js";
|
|
6
|
+
import { decide, isDrift, needsApply } from "../decide.js";
|
|
7
|
+
import { Effect, Option } from "effect";
|
|
8
|
+
import { GitHubRepository, Ruleset } from "@effected/github";
|
|
9
|
+
|
|
10
|
+
//#region src/sync/phases/settings.ts
|
|
11
|
+
/**
|
|
12
|
+
* Stands in for live state this phase cannot read.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* `decide` needs three fingerprints, and GitHub exposes no read of these
|
|
16
|
+
* resources comparable to what is written — the PATCH surface and the GET
|
|
17
|
+
* surface differ, and some fields are not on the GET at all. So live is taken
|
|
18
|
+
* to be the last applied value, and this sentinel covers the case where there
|
|
19
|
+
* is no last applied value.
|
|
20
|
+
*
|
|
21
|
+
* It must never equal a real fingerprint. Using the desired fingerprint here
|
|
22
|
+
* instead would make `FirstSync` compute `needsApply: desired !== live` as
|
|
23
|
+
* `false` — and the very first sync of a repository would write nothing at all.
|
|
24
|
+
*
|
|
25
|
+
* The consequence, stated plainly: **this phase reports config change, not
|
|
26
|
+
* drift.** A baseline that differs reads as `ConfigChanged`, never as `Drift`,
|
|
27
|
+
* because there is no independent observation to attribute the difference to.
|
|
28
|
+
*/
|
|
29
|
+
const UNREADABLE = "\0unreadable";
|
|
30
|
+
const KIND = "settings";
|
|
31
|
+
/**
|
|
32
|
+
* A repository has exactly one settings resource, so its drift key is fixed.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* The alternative — one resource per field — would report twenty drift lines
|
|
36
|
+
* for one person toggling twenty checkboxes in the UI. Settings move together
|
|
37
|
+
* and are reported together.
|
|
38
|
+
*/
|
|
39
|
+
const NAME = "settings";
|
|
40
|
+
/**
|
|
41
|
+
* `security_and_analysis` fields GitHub only accepts on organization-owned
|
|
42
|
+
* repositories.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Sending one to a personal repository is rejected outright, taking the whole
|
|
46
|
+
* PATCH with it — so they are stripped and reported rather than attempted.
|
|
47
|
+
*/
|
|
48
|
+
const ORG_ONLY_SAA_FIELDS = /* @__PURE__ */ new Set([
|
|
49
|
+
"secret_scanning_delegated_alert_dismissal",
|
|
50
|
+
"secret_scanning_delegated_bypass",
|
|
51
|
+
"delegated_bypass_reviewers"
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Settings fields only valid on organization-owned repositories.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* Checkable from `ownerType` alone, which the engine already resolved — so
|
|
58
|
+
* these cost nothing to gate.
|
|
59
|
+
*/
|
|
60
|
+
const ORG_ONLY_SETTINGS = /* @__PURE__ */ new Set();
|
|
61
|
+
/**
|
|
62
|
+
* Fields needing an organization owner **and** a private repository.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* `allow_forking` lived in {@link ORG_ONLY_SETTINGS} and was gated on owner type
|
|
66
|
+
* alone, which is half of GitHub's rule: it answers
|
|
67
|
+
*
|
|
68
|
+
* > Allow forks setting can only be changed on org-owned private repositories
|
|
69
|
+
*
|
|
70
|
+
* with a 422 that takes the **whole PATCH** with it — so one unsettable field
|
|
71
|
+
* costs the repository every other setting in the same request.
|
|
72
|
+
*
|
|
73
|
+
* Visibility is not in the config and not in `ownerType`, so gating these means
|
|
74
|
+
* asking GitHub. The read happens only when one of these fields actually
|
|
75
|
+
* survives the merge, so a config that never mentions them never pays for it.
|
|
76
|
+
*/
|
|
77
|
+
const ORG_PRIVATE_ONLY_SETTINGS = /* @__PURE__ */ new Set(["allow_forking"]);
|
|
78
|
+
/**
|
|
79
|
+
* Merge every settings group the repository's group references.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* Last write wins, and `security_and_analysis` is merged separately: it is a
|
|
83
|
+
* nested block, so `Object.assign` across groups would replace it wholesale
|
|
84
|
+
* rather than merging its fields.
|
|
85
|
+
*/
|
|
86
|
+
const mergeSettings = (ctx) => {
|
|
87
|
+
const refs = ctx.config.groups[ctx.group]?.settings ?? [];
|
|
88
|
+
const settings = {};
|
|
89
|
+
const saa = {};
|
|
90
|
+
let sawSaa = false;
|
|
91
|
+
for (const ref of refs) {
|
|
92
|
+
const group = ctx.config.settings[ref];
|
|
93
|
+
if (group === void 0) continue;
|
|
94
|
+
const { security_and_analysis, ...rest } = group;
|
|
95
|
+
Object.assign(settings, rest);
|
|
96
|
+
if (security_and_analysis !== void 0) {
|
|
97
|
+
sawSaa = true;
|
|
98
|
+
for (const [key, value] of Object.entries(security_and_analysis)) if (value !== void 0) saa[key] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const skipped = [];
|
|
102
|
+
if (ctx.ownerType === "User") {
|
|
103
|
+
for (const key of ORG_PRIVATE_ONLY_SETTINGS) if (key in settings) {
|
|
104
|
+
delete settings[key];
|
|
105
|
+
skipped.push({
|
|
106
|
+
where: "setting",
|
|
107
|
+
key,
|
|
108
|
+
reason: "org-only, owner is a personal account"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
for (const key of ORG_ONLY_SETTINGS) if (key in settings) {
|
|
112
|
+
delete settings[key];
|
|
113
|
+
skipped.push({
|
|
114
|
+
where: "setting",
|
|
115
|
+
key,
|
|
116
|
+
reason: "org-only, owner is a personal account"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const key of ORG_ONLY_SAA_FIELDS) if (key in saa) {
|
|
120
|
+
delete saa[key];
|
|
121
|
+
skipped.push({
|
|
122
|
+
where: "security_and_analysis",
|
|
123
|
+
key,
|
|
124
|
+
reason: "org-only, owner is a personal account"
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
settings,
|
|
130
|
+
securityAndAnalysis: sawSaa && Object.keys(saa).length > 0 ? saa : void 0,
|
|
131
|
+
skipped,
|
|
132
|
+
conditional: [...ORG_PRIVATE_ONLY_SETTINGS].filter((key) => key in settings)
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Repository settings, over REST and the GraphQL mutation.
|
|
137
|
+
*
|
|
138
|
+
* @remarks
|
|
139
|
+
* The whole settings block is one drift resource — see {@link NAME}.
|
|
140
|
+
*
|
|
141
|
+
* Delegated-bypass reviewers are config-level `{ team }` or `{ role }` names
|
|
142
|
+
* that GitHub wants as numeric `reviewer_id`. Resolution happens here, before
|
|
143
|
+
* the fingerprint is taken, so the fingerprint covers what was actually sent: a
|
|
144
|
+
* team renamed under the same id must not read as drift, and a team pointing at
|
|
145
|
+
* a different id must.
|
|
146
|
+
*
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
const settingsPhase = Effect.gen(function* () {
|
|
150
|
+
const settingsClient = yield* GitHubRepository;
|
|
151
|
+
const rulesets = yield* Ruleset;
|
|
152
|
+
const cache = yield* RepoCache;
|
|
153
|
+
const applied = yield* AppliedState;
|
|
154
|
+
const logger = yield* SyncLogger;
|
|
155
|
+
/** Turn `{ team }` / `{ role }` reviewer entries into `{ reviewer_id, reviewer_type }`. */
|
|
156
|
+
const resolveReviewers = (ctx, reviewers) => Effect.gen(function* () {
|
|
157
|
+
const resolved = [];
|
|
158
|
+
const errors = [];
|
|
159
|
+
for (const reviewer of reviewers) {
|
|
160
|
+
const entry = {};
|
|
161
|
+
if (typeof reviewer.team === "string") {
|
|
162
|
+
const slug = reviewer.team;
|
|
163
|
+
const id = yield* cache.teamId(ctx.owner, slug, rulesets.teamId(slug)).pipe(Effect.map(Option.some), Effect.orElseSucceed(() => Option.none()));
|
|
164
|
+
if (Option.isNone(id)) {
|
|
165
|
+
errors.push({
|
|
166
|
+
context: `resolve team '${slug}'`,
|
|
167
|
+
message: "could not resolve team slug"
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
entry.reviewer_id = id.value;
|
|
172
|
+
entry.reviewer_type = "TEAM";
|
|
173
|
+
} else if (typeof reviewer.role === "string") {
|
|
174
|
+
const name = reviewer.role;
|
|
175
|
+
const id = yield* rulesets.roleId(name).pipe(Effect.map(Option.some), Effect.orElseSucceed(() => Option.none()));
|
|
176
|
+
if (Option.isNone(id)) {
|
|
177
|
+
errors.push({
|
|
178
|
+
context: `resolve role '${name}'`,
|
|
179
|
+
message: "could not resolve organization role"
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
entry.reviewer_id = id.value;
|
|
184
|
+
entry.reviewer_type = "ROLE";
|
|
185
|
+
} else continue;
|
|
186
|
+
if (reviewer.mode !== void 0) entry.mode = reviewer.mode;
|
|
187
|
+
resolved.push(entry);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
resolved,
|
|
191
|
+
errors
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
const run = (ctx) => Effect.gen(function* () {
|
|
195
|
+
const merged = mergeSettings(ctx);
|
|
196
|
+
const errors = [];
|
|
197
|
+
const desired = { ...merged.settings };
|
|
198
|
+
const skipped = [...merged.skipped];
|
|
199
|
+
if (merged.conditional.length > 0) {
|
|
200
|
+
const repository = yield* read(cache.repoPrivate(ctx.owner, ctx.repo, settingsClient.settings));
|
|
201
|
+
if ("failed" in repository) errors.push({
|
|
202
|
+
context: "settings",
|
|
203
|
+
message: `could not read repository visibility: ${repository.failed}`
|
|
204
|
+
});
|
|
205
|
+
else if (!repository.value) for (const key of merged.conditional) {
|
|
206
|
+
delete desired[key];
|
|
207
|
+
skipped.push({
|
|
208
|
+
where: "setting",
|
|
209
|
+
key,
|
|
210
|
+
reason: "org-owned private repositories only; this one is public"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const { where, key, reason } of skipped) yield* logger.syncOperation("skip", where, key, `(${reason})`);
|
|
215
|
+
if (merged.securityAndAnalysis !== void 0) {
|
|
216
|
+
const saa = { ...merged.securityAndAnalysis };
|
|
217
|
+
const reviewers = saa.delegated_bypass_reviewers;
|
|
218
|
+
if (Array.isArray(reviewers)) {
|
|
219
|
+
const outcome = yield* resolveReviewers(ctx, reviewers);
|
|
220
|
+
errors.push(...outcome.errors);
|
|
221
|
+
saa.delegated_bypass_reviewers = outcome.resolved;
|
|
222
|
+
}
|
|
223
|
+
if (Object.keys(saa).length > 0) desired.security_and_analysis = saa;
|
|
224
|
+
}
|
|
225
|
+
if (Object.keys(desired).length === 0) return {
|
|
226
|
+
changes: [],
|
|
227
|
+
errors
|
|
228
|
+
};
|
|
229
|
+
const baselines = yield* applied.getMany(ctx.slug).pipe(Effect.orElseSucceed(() => /* @__PURE__ */ new Map()));
|
|
230
|
+
const baseline = Option.map(lookup(baselines, KIND, NAME), (record) => record.fingerprint);
|
|
231
|
+
const desiredPrint = fingerprint(desired);
|
|
232
|
+
const decision = decide(desiredPrint, Option.getOrElse(baseline, () => UNREADABLE), baseline);
|
|
233
|
+
if (isDrift(decision)) yield* logger.driftDetected(KIND, NAME, decision);
|
|
234
|
+
if (!needsApply(decision)) return {
|
|
235
|
+
changes: [],
|
|
236
|
+
errors
|
|
237
|
+
};
|
|
238
|
+
let sent;
|
|
239
|
+
if (!ctx.dryRun) {
|
|
240
|
+
const outcome = yield* settingsClient.applySettings(desired).pipe(Effect.map((applied) => ({ applied })), Effect.catch((error) => Effect.succeed({ failed: error.message ?? String(error) })));
|
|
241
|
+
if ("failed" in outcome) return {
|
|
242
|
+
changes: [],
|
|
243
|
+
errors: [...errors, {
|
|
244
|
+
context: KIND,
|
|
245
|
+
message: outcome.failed
|
|
246
|
+
}]
|
|
247
|
+
};
|
|
248
|
+
sent = [...outcome.applied.rest, ...outcome.applied.graphql];
|
|
249
|
+
yield* applied.record({
|
|
250
|
+
repo: ctx.slug,
|
|
251
|
+
kind: KIND,
|
|
252
|
+
name: NAME
|
|
253
|
+
}, desiredPrint, ctx.runId).pipe(Effect.ignore);
|
|
254
|
+
}
|
|
255
|
+
yield* logger.settingsApplied(sent ?? Object.keys(desired));
|
|
256
|
+
return {
|
|
257
|
+
changes: [{
|
|
258
|
+
repo: ctx.slug,
|
|
259
|
+
kind: KIND,
|
|
260
|
+
name: NAME,
|
|
261
|
+
action: isDrift(decision) ? "drift-overwritten" : "updated"
|
|
262
|
+
}],
|
|
263
|
+
errors
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
name: "settings",
|
|
268
|
+
appliesTo: (ctx) => (ctx.config.groups[ctx.group]?.settings?.length ?? 0) > 0,
|
|
269
|
+
run
|
|
270
|
+
};
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
export { settingsPhase };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SyncLogger } from "../../services/SyncLogger.js";
|
|
2
|
+
import { AppliedState, lookup } from "../../store/AppliedState.js";
|
|
3
|
+
import { capture, resolveGroups } from "./resource.js";
|
|
4
|
+
import { fingerprint } from "../../lib/fingerprint.js";
|
|
5
|
+
import { decide, isDrift, needsApply } from "../decide.js";
|
|
6
|
+
import { Effect, Option, Redacted } from "effect";
|
|
7
|
+
import { RepositoryVariable } from "@effected/github";
|
|
8
|
+
|
|
9
|
+
//#region src/sync/phases/variables.ts
|
|
10
|
+
const KIND = "variable";
|
|
11
|
+
/**
|
|
12
|
+
* Stands in for a variable that is not on the repository at all.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Absence is a live state like any other, and it needs a fingerprint distinct
|
|
16
|
+
* from every real value so `decide` can compare against it. Without it, a
|
|
17
|
+
* variable someone deleted out of band would compare equal to whatever we last
|
|
18
|
+
* applied and never be restored.
|
|
19
|
+
*/
|
|
20
|
+
const ABSENT = "\0absent";
|
|
21
|
+
/** An environment-scoped variable is named `env/name` so the two never collide. */
|
|
22
|
+
const resourceName = (name, environment) => environment === void 0 ? name : `${environment}/${name}`;
|
|
23
|
+
/**
|
|
24
|
+
* Repository and environment variables.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Unlike secrets, variables are readable — `listVariables` returns what is
|
|
28
|
+
* actually on the repository — so this phase does **real** three-way drift
|
|
29
|
+
* detection rather than the config-change-only comparison `settings` is limited
|
|
30
|
+
* to. Someone editing a variable in the UI is reported as drift and overwritten;
|
|
31
|
+
* someone deleting one is reported and restored.
|
|
32
|
+
*
|
|
33
|
+
* The listing carries values, not just names — variables are not secret, so
|
|
34
|
+
* GitHub returns them — which makes this a full three-way comparison at no
|
|
35
|
+
* extra request: an *edited* variable is as detectable as a deleted one. That
|
|
36
|
+
* is the real difference from `secrets`, where no projection change could help
|
|
37
|
+
* because GitHub never returns a secret value at any endpoint.
|
|
38
|
+
*
|
|
39
|
+
* Values arrive {@link Redacted.Redacted} from the shared resolver even though
|
|
40
|
+
* variables are not secret, so `Redacted.value` appears exactly once per write
|
|
41
|
+
* and never near a log line.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
const variablesPhase = Effect.gen(function* () {
|
|
46
|
+
const variables = yield* RepositoryVariable;
|
|
47
|
+
const applied = yield* AppliedState;
|
|
48
|
+
const logger = yield* SyncLogger;
|
|
49
|
+
const run = (ctx) => Effect.gen(function* () {
|
|
50
|
+
const scopes = ctx.config.groups[ctx.group]?.variables;
|
|
51
|
+
if (scopes === void 0) return {
|
|
52
|
+
changes: [],
|
|
53
|
+
errors: []
|
|
54
|
+
};
|
|
55
|
+
const changes = [];
|
|
56
|
+
const errors = [];
|
|
57
|
+
const actions = resolveGroups(scopes.actions ?? [], ctx.config.variables, ctx);
|
|
58
|
+
errors.push(...actions.errors);
|
|
59
|
+
/** Every environment's resolved entries, by environment name. */
|
|
60
|
+
const byEnvironment = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const [environment, refs] of Object.entries(scopes.environments ?? {})) {
|
|
62
|
+
const resolved = resolveGroups(refs, ctx.config.variables, ctx);
|
|
63
|
+
errors.push(...resolved.errors);
|
|
64
|
+
byEnvironment.set(environment, resolved.entries);
|
|
65
|
+
}
|
|
66
|
+
if (actions.entries.size === 0 && byEnvironment.size === 0) return {
|
|
67
|
+
changes,
|
|
68
|
+
errors
|
|
69
|
+
};
|
|
70
|
+
const baselines = yield* applied.getMany(ctx.slug).pipe(Effect.orElseSucceed(() => /* @__PURE__ */ new Map()));
|
|
71
|
+
/** What is currently on the repository, name to value, for one scope. */
|
|
72
|
+
const liveValues = (environment) => (environment === void 0 ? variables.list() : variables.listForEnvironment(environment)).pipe(Effect.map((list) => Option.some(new Map(list.map((entry) => [entry.name, entry.value])))), Effect.orElseSucceed(() => Option.none()));
|
|
73
|
+
const syncScope = (entries, environment) => Effect.gen(function* () {
|
|
74
|
+
if (entries.size === 0) return;
|
|
75
|
+
const live = yield* liveValues(environment);
|
|
76
|
+
if (Option.isNone(live)) {
|
|
77
|
+
errors.push({
|
|
78
|
+
context: environment === void 0 ? "list variables" : `list variables (env: ${environment})`,
|
|
79
|
+
message: "could not read current variables"
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const [name, value] of entries) {
|
|
84
|
+
const resource = resourceName(name, environment);
|
|
85
|
+
const desiredPrint = fingerprint(Redacted.value(value));
|
|
86
|
+
const baseline = Option.map(lookup(baselines, KIND, resource), (record) => record.fingerprint);
|
|
87
|
+
const liveValue = live.value.get(name);
|
|
88
|
+
const livePrint = liveValue === void 0 ? fingerprint(ABSENT) : fingerprint(liveValue);
|
|
89
|
+
const decision = decide(desiredPrint, livePrint, baseline);
|
|
90
|
+
if (isDrift(decision)) yield* logger.driftDetected(KIND, resource, decision);
|
|
91
|
+
if (!needsApply(decision)) continue;
|
|
92
|
+
if (!ctx.dryRun) {
|
|
93
|
+
const failure = yield* capture(`variable ${resource}`, environment === void 0 ? variables.set(name, Redacted.value(value)) : variables.setForEnvironment(environment, name, Redacted.value(value)));
|
|
94
|
+
if (failure !== void 0) {
|
|
95
|
+
errors.push(failure);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
yield* applied.record({
|
|
99
|
+
repo: ctx.slug,
|
|
100
|
+
kind: KIND,
|
|
101
|
+
name: resource
|
|
102
|
+
}, desiredPrint, ctx.runId).pipe(Effect.ignore);
|
|
103
|
+
}
|
|
104
|
+
yield* logger.syncOperation("sync", KIND, name, environment === void 0 ? void 0 : `(env: ${environment})`);
|
|
105
|
+
changes.push({
|
|
106
|
+
repo: ctx.slug,
|
|
107
|
+
kind: KIND,
|
|
108
|
+
name: resource,
|
|
109
|
+
action: isDrift(decision) ? "drift-overwritten" : "updated"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
yield* syncScope(actions.entries);
|
|
114
|
+
for (const [environment, entries] of byEnvironment) yield* syncScope(entries, environment);
|
|
115
|
+
return {
|
|
116
|
+
changes,
|
|
117
|
+
errors
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
name: "variables",
|
|
122
|
+
appliesTo: (ctx) => {
|
|
123
|
+
const scopes = ctx.config.groups[ctx.group]?.variables;
|
|
124
|
+
if (scopes === void 0) return false;
|
|
125
|
+
return (scopes.actions?.length ?? 0) > 0 || Object.keys(scopes.environments ?? {}).length > 0;
|
|
126
|
+
},
|
|
127
|
+
run
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
export { variablesPhase };
|
package/tsdoc-metadata.json
CHANGED
package/bin/reposets.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/errors.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Data } from "effect";
|
|
2
|
-
|
|
3
|
-
//#region src/errors.ts
|
|
4
|
-
/* v8 ignore start -- TaggedError class declarations, covered via errors.test.ts */
|
|
5
|
-
var ResolveError = class extends Data.TaggedError("ResolveError") {};
|
|
6
|
-
var OnePasswordError = class extends Data.TaggedError("OnePasswordError") {};
|
|
7
|
-
var GitHubApiError = class extends Data.TaggedError("GitHubApiError") {};
|
|
8
|
-
var SyncError = class extends Data.TaggedError("SyncError") {};
|
|
9
|
-
/* v8 ignore stop */
|
|
10
|
-
|
|
11
|
-
//#endregion
|
|
12
|
-
export { GitHubApiError, OnePasswordError, ResolveError, SyncError };
|
package/lib/crypto.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { blake2b } from "blakejs";
|
|
2
|
-
import nacl from "tweetnacl";
|
|
3
|
-
|
|
4
|
-
//#region src/lib/crypto.ts
|
|
5
|
-
/**
|
|
6
|
-
* Encrypt a secret using libsodium's sealed box algorithm.
|
|
7
|
-
* Implementation based on tweetsodium using tweetnacl + blakejs.
|
|
8
|
-
*
|
|
9
|
-
* The sealed box format is: ephemeral_public_key (32 bytes) || ciphertext
|
|
10
|
-
*/
|
|
11
|
-
function encryptSecret(publicKey, secretValue) {
|
|
12
|
-
const messageBytes = Buffer.from(secretValue);
|
|
13
|
-
const publicKeyBytes = Buffer.from(publicKey, "base64");
|
|
14
|
-
const ephemeralKeyPair = nacl.box.keyPair();
|
|
15
|
-
const nonceInput = new Uint8Array(64);
|
|
16
|
-
nonceInput.set(ephemeralKeyPair.publicKey);
|
|
17
|
-
nonceInput.set(publicKeyBytes, 32);
|
|
18
|
-
const nonce = blake2b(nonceInput, void 0, 24);
|
|
19
|
-
const ciphertext = nacl.box(messageBytes, nonce, publicKeyBytes, ephemeralKeyPair.secretKey);
|
|
20
|
-
const sealed = new Uint8Array(ephemeralKeyPair.publicKey.length + ciphertext.length);
|
|
21
|
-
sealed.set(ephemeralKeyPair.publicKey);
|
|
22
|
-
sealed.set(ciphertext, ephemeralKeyPair.publicKey.length);
|
|
23
|
-
return Buffer.from(sealed).toString("base64");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
//#endregion
|
|
27
|
-
export { encryptSecret };
|