hammerkit 1.23.0 → 1.25.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.
Files changed (52) hide show
  1. package/build.schema.json +10 -0
  2. package/dist/cli.js +6 -0
  3. package/dist/executer/execute-command.js +7 -1
  4. package/dist/optimizer/get-cache-directory.js +5 -0
  5. package/dist/planner/utils/append-work-task.js +1 -0
  6. package/dist/planner/work-secret.js +6 -2
  7. package/dist/program.js +6 -0
  8. package/dist/remote/bundle.js +59 -0
  9. package/dist/remote/collect-bundle.js +41 -0
  10. package/dist/remote/credentials.js +93 -0
  11. package/dist/remote/device-login.js +177 -0
  12. package/dist/remote/host-config.js +170 -0
  13. package/dist/remote/location.js +46 -0
  14. package/dist/remote/remote-commands.js +158 -0
  15. package/dist/remote/remote-work-commands.js +224 -0
  16. package/dist/remote/server-client.js +88 -0
  17. package/dist/remote/source-manifest.js +79 -0
  18. package/dist/remote/text-diff.js +89 -0
  19. package/dist/schema/build-file-base-task-schema.js +4 -0
  20. package/dist/schema/build-file-schema.js +3 -0
  21. package/dist/schema/reference-parser.js +9 -2
  22. package/dist/schema/runner-schema.js +13 -0
  23. package/dist/secrets/provider-values.js +4 -0
  24. package/dist/server/accounts-api.js +42 -0
  25. package/dist/server/approvals-api.js +104 -0
  26. package/dist/server/definitions-api.js +163 -0
  27. package/dist/server/definitions.js +160 -0
  28. package/dist/server/discovery.js +9 -0
  29. package/dist/server/executor.js +237 -0
  30. package/dist/server/git-checkout.js +115 -0
  31. package/dist/server/http.js +39 -0
  32. package/dist/server/oauth.js +226 -0
  33. package/dist/server/plan.js +296 -0
  34. package/dist/server/policy.js +138 -0
  35. package/dist/server/proposals-api.js +145 -0
  36. package/dist/server/proposals.js +173 -0
  37. package/dist/server/repo-access.js +127 -0
  38. package/dist/server/repo.js +94 -0
  39. package/dist/server/run-environment.js +20 -0
  40. package/dist/server/run-plan.js +44 -0
  41. package/dist/server/run-request.js +85 -0
  42. package/dist/server/run-store.js +61 -0
  43. package/dist/server/run-worker-command.js +17 -0
  44. package/dist/server/run-worker-main.js +7 -0
  45. package/dist/server/run-worker.js +179 -0
  46. package/dist/server/runs.js +432 -0
  47. package/dist/server/server-command.js +61 -0
  48. package/dist/server/server-config.js +394 -0
  49. package/dist/server/server.js +434 -0
  50. package/dist/server/sources-api.js +106 -0
  51. package/dist/server/sources.js +258 -0
  52. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleProposals = exports.mayApprove = void 0;
4
+ const zod_1 = require("zod");
5
+ const bundle_1 = require("../remote/bundle");
6
+ const definitions_1 = require("./definitions");
7
+ const http_1 = require("./http");
8
+ const oauth_1 = require("./oauth");
9
+ const policy_1 = require("./policy");
10
+ const proposals_1 = require("./proposals");
11
+ const server_config_1 = require("./server-config");
12
+ const decisionSchema = zod_1.z.object({ reason: zod_1.z.string().max(500).optional() }).strict();
13
+ const same = (a, b) => a.iss === b.iss && a.sub === b.sub;
14
+ // Whoever approves publishes it, and uses the accounts its proposer did not: a
15
+ // version that names an account is approved by somebody the account trusts.
16
+ const mayApprove = (grant, proposal) => (0, policy_1.mayPublish)(grant) && proposal.missing.every((account) => (0, policy_1.canUse)(grant, account));
17
+ exports.mayApprove = mayApprove;
18
+ // What the caller is shown of a proposal: what it is, who proposed it and the
19
+ // version it would replace, so the files can be compared.
20
+ function toView(store, proposal, principal, grant) {
21
+ const latest = store.find(proposal.name, 'latest')?.hash ?? null;
22
+ const stale = proposal.state === 'waiting' && latest !== proposal.base;
23
+ return {
24
+ ...proposal,
25
+ // the version it follows is fixed when it is proposed; if another one was
26
+ // published since, it can only be rejected
27
+ latest,
28
+ stale,
29
+ mine: same(proposal.proposedBy, principal),
30
+ // approving needs the permission to publish, the accounts the proposer lacked, and
31
+ // somebody else's proposal
32
+ canDecide: proposal.state === 'waiting' && !stale && (0, exports.mayApprove)(grant, proposal) && !same(proposal.proposedBy, principal),
33
+ };
34
+ }
35
+ /**
36
+ * New versions of build files that wait for an approval. Whoever may only
37
+ * propose a version (`builds: propose`), or publishes one that names accounts they
38
+ * do not use, puts one here; someone who may publish, and uses those accounts,
39
+ * approves it, and it is published then.
40
+ *
41
+ * GET /v1/proposals the ones the caller proposed or may decide
42
+ * GET /v1/proposals/<name>/<hash> one of them
43
+ * GET /v1/proposals/<name>/<hash>/files its files, for who decides
44
+ * POST /v1/proposals/<name>/<hash>/approve publish it (body: optional {reason})
45
+ * POST /v1/proposals/<name>/<hash>/reject drop it
46
+ */
47
+ async function handleProposals(config, store, proposals, log, principal, req, res, url) {
48
+ const match = /^\/v1\/proposals(?:\/([^/]+)\/([^/]+)(?:\/(files|approve|reject))?)?$/.exec(url.pathname);
49
+ if (!match) {
50
+ return false;
51
+ }
52
+ const [, name, hash, action] = match;
53
+ const grant = (0, policy_1.getGrant)(config.entitlements, principal);
54
+ // what the caller neither proposed nor may decide does not exist for them
55
+ const visible = (proposal) => same(proposal.proposedBy, principal) || (0, policy_1.mayPublish)(grant);
56
+ if (name === undefined) {
57
+ if (req.method !== 'GET') {
58
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
59
+ return true;
60
+ }
61
+ (0, http_1.send)(res, 200, {
62
+ proposals: proposals
63
+ .all()
64
+ .filter(visible)
65
+ .map((proposal) => toView(store, proposal, principal, grant)),
66
+ });
67
+ return true;
68
+ }
69
+ if (!server_config_1.definitionName.safeParse(name).success || !bundle_1.HASH_PATTERN.test(hash)) {
70
+ (0, http_1.send)(res, 404, { error: 'not found' });
71
+ return true;
72
+ }
73
+ const proposal = proposals.get(name, hash);
74
+ if (!action) {
75
+ if (req.method !== 'GET') {
76
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
77
+ return true;
78
+ }
79
+ proposal && visible(proposal)
80
+ ? (0, http_1.send)(res, 200, toView(store, proposal, principal, grant))
81
+ : (0, http_1.send)(res, 404, { error: 'not found' });
82
+ return true;
83
+ }
84
+ if (action === 'files') {
85
+ if (req.method !== 'GET') {
86
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
87
+ return true;
88
+ }
89
+ // the files are for who decides, and for who wrote them
90
+ const bundle = proposal && visible(proposal) ? proposals.bundle(name, hash) : undefined;
91
+ bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
92
+ return true;
93
+ }
94
+ if (req.method !== 'POST') {
95
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
96
+ return true;
97
+ }
98
+ const body = decisionSchema.safeParse(req.headers['content-type'] ? await (0, http_1.readJson)(req) : {});
99
+ if (!body.success) {
100
+ const issue = body.error.issues[0];
101
+ (0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
102
+ return true;
103
+ }
104
+ const outcome = action === 'approve' ? 'approved' : 'rejected';
105
+ try {
106
+ const result = await proposals.decide(principal, name, hash, outcome, (proposed) => (0, exports.mayApprove)(grant, proposed), async (bundle) => {
107
+ const proposed = proposals.get(name, hash);
108
+ // looked at again here, just before it is published
109
+ const latest = store.find(name, 'latest')?.hash ?? null;
110
+ if (proposed && latest !== proposed.base) {
111
+ throw new proposals_1.StaleProposalError(proposed.base, latest);
112
+ }
113
+ await store.publish(name, bundle, proposed?.proposedBy ?? principal, principal);
114
+ }, body.data.reason);
115
+ log(`${action} proposal=${name}@${hash} iss=${principal.iss} sub=${principal.sub} result=${result.status}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
116
+ switch (result.status) {
117
+ case 'ok':
118
+ (0, http_1.send)(res, 200, toView(store, result.proposal, principal, grant));
119
+ break;
120
+ case 'own-proposal':
121
+ (0, http_1.send)(res, 403, { error: 'a version is approved by somebody else than who proposed it' });
122
+ break;
123
+ case 'not-waiting':
124
+ (0, http_1.send)(res, 409, { error: `the proposal is ${result.state}, it does not wait for an approval` });
125
+ break;
126
+ default:
127
+ (0, http_1.send)(res, 404, { error: 'not found' });
128
+ }
129
+ }
130
+ catch (e) {
131
+ if (e instanceof proposals_1.StaleProposalError) {
132
+ log(`${action} proposal=${name}@${hash} iss=${principal.iss} sub=${principal.sub} result=stale`);
133
+ (0, http_1.send)(res, 409, { error: e.message });
134
+ return true;
135
+ }
136
+ if (e instanceof definitions_1.DefinitionError) {
137
+ (0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
138
+ return true;
139
+ }
140
+ throw e;
141
+ }
142
+ return true;
143
+ }
144
+ exports.handleProposals = handleProposals;
145
+ //# sourceMappingURL=proposals-api.js.map
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProposalRegistry = exports.StaleProposalError = exports.CooldownError = exports.ProposalError = void 0;
4
+ const units_1 = require("../utils/units");
5
+ class ProposalError extends Error {
6
+ constructor() {
7
+ super(...arguments);
8
+ // what the caller is told
9
+ this.status = 503;
10
+ }
11
+ }
12
+ exports.ProposalError = ProposalError;
13
+ // The caller proposes too soon after a rejection
14
+ class CooldownError extends ProposalError {
15
+ constructor() {
16
+ super(...arguments);
17
+ this.status = 429;
18
+ }
19
+ }
20
+ exports.CooldownError = CooldownError;
21
+ // The build file moved on since the proposal was made
22
+ class StaleProposalError extends Error {
23
+ constructor(base, latest) {
24
+ super(`the build file changed since this was proposed (it follows ${base ?? 'nothing'}, the latest is ${latest ?? 'nothing'}), propose it again on top of the latest`);
25
+ this.base = base;
26
+ this.latest = latest;
27
+ }
28
+ }
29
+ exports.StaleProposalError = StaleProposalError;
30
+ // The proposals of the server, in memory: a restart forgets what waits, and
31
+ // whoever proposed it proposes it again. Nothing in here is runnable.
32
+ class ProposalRegistry {
33
+ constructor(options = {}) {
34
+ this.entries = new Map();
35
+ this.closed = false;
36
+ this.maxWaiting = options.maxWaiting ?? 100;
37
+ this.timeout = options.timeout ?? 60 * 60 * 1000;
38
+ this.keep = options.keep ?? 100;
39
+ this.cooldown = options.cooldown ?? 0;
40
+ this.now = options.now ?? Date.now;
41
+ }
42
+ // Proposing what already waits is not an error and changes nothing.
43
+ propose(version, bundle, proposer, missing = [], base = null) {
44
+ if (this.closed) {
45
+ throw new ProposalError('the server is shutting down');
46
+ }
47
+ const key = this.key(version.name, version.hash);
48
+ const existing = this.entries.get(key);
49
+ if (existing && existing.proposal.state === 'waiting') {
50
+ return { proposal: { ...existing.proposal }, created: false };
51
+ }
52
+ this.checkCooldown(version, proposer);
53
+ if (this.waiting().length >= this.maxWaiting) {
54
+ throw new ProposalError('too many build file versions are waiting for an approval');
55
+ }
56
+ const proposal = {
57
+ name: version.name,
58
+ hash: version.hash,
59
+ entry: version.entry,
60
+ tasks: version.tasks,
61
+ services: version.services,
62
+ accounts: version.accounts ?? [],
63
+ missing,
64
+ plan: version.plan ?? {},
65
+ base,
66
+ proposedBy: { iss: proposer.iss, sub: proposer.sub },
67
+ createdAt: new Date(this.now()).toISOString(),
68
+ expiresAt: new Date(this.now() + this.timeout).toISOString(),
69
+ state: 'waiting',
70
+ };
71
+ const entry = { proposal, bundle };
72
+ entry.timer = setTimeout(() => this.settle(entry, 'expired'), this.timeout);
73
+ entry.timer.unref();
74
+ this.entries.delete(key);
75
+ this.entries.set(key, entry);
76
+ this.evict();
77
+ return { proposal: { ...proposal }, created: true };
78
+ }
79
+ waiting() {
80
+ return [...this.entries.values()].filter((e) => e.proposal.state === 'waiting').map((e) => ({ ...e.proposal }));
81
+ }
82
+ // Everything kept, newest first.
83
+ all() {
84
+ return [...this.entries.values()].map((e) => ({ ...e.proposal })).reverse();
85
+ }
86
+ get(name, hash) {
87
+ const entry = this.entries.get(this.key(name, hash));
88
+ return entry ? { ...entry.proposal } : undefined;
89
+ }
90
+ // The files of one that still waits, for whoever decides on it.
91
+ bundle(name, hash) {
92
+ return this.entries.get(this.key(name, hash))?.bundle;
93
+ }
94
+ // An approval publishes the files first (`publish`), and only then is the
95
+ // proposal approved, so it never claims a version that is not there. Nobody
96
+ // decides on their own proposal.
97
+ async decide(decider, name, hash, outcome, mayDecide, publish, reason) {
98
+ const entry = this.entries.get(this.key(name, hash));
99
+ if (!entry || !mayDecide(entry.proposal)) {
100
+ return { status: 'not-found' };
101
+ }
102
+ if (entry.proposal.proposedBy.iss === decider.iss && entry.proposal.proposedBy.sub === decider.sub) {
103
+ return { status: 'own-proposal' };
104
+ }
105
+ if (entry.proposal.state !== 'waiting' || !entry.bundle) {
106
+ return { status: 'not-waiting', state: entry.proposal.state };
107
+ }
108
+ if (outcome === 'approved') {
109
+ await publish(entry.bundle);
110
+ if (entry.proposal.state !== 'waiting') {
111
+ return { status: 'not-waiting', state: entry.proposal.state };
112
+ }
113
+ }
114
+ this.settle(entry, outcome, { iss: decider.iss, sub: decider.sub }, reason);
115
+ return { status: 'ok', proposal: { ...entry.proposal } };
116
+ }
117
+ close() {
118
+ this.closed = true;
119
+ for (const entry of this.entries.values()) {
120
+ if (entry.timer) {
121
+ clearTimeout(entry.timer);
122
+ entry.timer = undefined;
123
+ }
124
+ }
125
+ }
126
+ settle(entry, state, by, reason) {
127
+ if (entry.proposal.state !== 'waiting') {
128
+ return;
129
+ }
130
+ if (entry.timer) {
131
+ clearTimeout(entry.timer);
132
+ entry.timer = undefined;
133
+ }
134
+ entry.proposal.state = state;
135
+ entry.proposal.decidedAt = new Date(this.now()).toISOString();
136
+ if (by) {
137
+ entry.proposal.decidedBy = by;
138
+ }
139
+ if (reason) {
140
+ entry.proposal.reason = reason;
141
+ }
142
+ entry.bundle = undefined;
143
+ }
144
+ // A rejection is a no; a proposal that only ran out of time is not.
145
+ checkCooldown(version, proposer) {
146
+ if (this.cooldown <= 0) {
147
+ return;
148
+ }
149
+ for (const { proposal } of this.entries.values()) {
150
+ if (proposal.state !== 'rejected' || proposal.name !== version.name) {
151
+ continue;
152
+ }
153
+ const left = Date.parse(proposal.decidedAt ?? '') + this.cooldown - this.now();
154
+ const sameVersion = proposal.hash === version.hash;
155
+ const sameProposer = proposal.proposedBy.iss === proposer.iss && proposal.proposedBy.sub === proposer.sub;
156
+ if (left > 0 && (sameVersion || sameProposer)) {
157
+ throw new CooldownError(`${sameVersion ? 'this version was' : 'a version of this build file by you was'} rejected, ` +
158
+ `propose again in ${(0, units_1.formatDuration)(Math.ceil(left / 1000) * 1000)}`);
159
+ }
160
+ }
161
+ }
162
+ evict() {
163
+ const decided = [...this.entries.entries()].filter(([, e]) => e.proposal.state !== 'waiting');
164
+ for (const [key] of decided.slice(0, Math.max(0, decided.length - this.keep))) {
165
+ this.entries.delete(key);
166
+ }
167
+ }
168
+ key(name, hash) {
169
+ return `${name}@${hash}`;
170
+ }
171
+ }
172
+ exports.ProposalRegistry = ProposalRegistry;
173
+ //# sourceMappingURL=proposals.js.map
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RepoAccess = void 0;
4
+ const redact_1 = require("../utils/redact");
5
+ const provider_values_1 = require("../secrets/provider-values");
6
+ const git_checkout_1 = require("./git-checkout");
7
+ const repo_1 = require("./repo");
8
+ // A token is read again after this long, so a short-lived one (a GitHub App
9
+ // installation token minted by the provider command) is not used up and a revoked one
10
+ // stops working soon.
11
+ const TOKEN_TTL_MS = 60 * 1000;
12
+ // A run waits for the repository at submit, not forever
13
+ const RESOLVE_TIMEOUT_MS = 30 * 1000;
14
+ /**
15
+ * The repositories a server can run code from. What may be read is the operator's
16
+ * `sources` list; who may name which of them is the entitlement's `repos`. The read
17
+ * credential is a secret of a provider, read as a service account that nobody can use
18
+ * in a task. It is handed to git for one fetch through the environment of that git
19
+ * process (never an argument, never `.git/config`, never the environment of a task)
20
+ * and masked in whatever the run logs.
21
+ */
22
+ class RepoAccess {
23
+ constructor(config,
24
+ // the base environment of the server's runs, with `hosted` set
25
+ environment, options = {}) {
26
+ this.config = config;
27
+ this.environment = environment;
28
+ this.options = options;
29
+ this.tokens = new Map();
30
+ this.now = options.now ?? Date.now;
31
+ }
32
+ // The first source that covers the repository: patterns are listed from the most
33
+ // specific to the least.
34
+ source(repo) {
35
+ return this.config.sources.find((source) => (0, repo_1.matchesAnyRepo)([source.match], repo));
36
+ }
37
+ readable(repo) {
38
+ return this.source(repo) !== undefined;
39
+ }
40
+ url(repo) {
41
+ return this.options.url ? this.options.url(repo) : `https://${repo}.git`;
42
+ }
43
+ // The commit a ref points to now, or an error that says what is wrong with the ref.
44
+ async resolve(repo, ref, signal) {
45
+ const timeout = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
46
+ const abort = signal ? AbortSignal.any([signal, timeout]) : timeout;
47
+ if (this.options.resolve) {
48
+ return this.options.resolve(repo, ref, abort);
49
+ }
50
+ const credentials = await this.credentials(repo);
51
+ try {
52
+ return await (0, git_checkout_1.resolveRef)(this.url(repo), ref, credentials.env, abort);
53
+ }
54
+ catch (e) {
55
+ throw this.scrub(e, credentials.values);
56
+ }
57
+ }
58
+ /**
59
+ * The environment a git process needs to read the repository: the allowlisted
60
+ * variables of the server, and the token as a header of requests to that host. The
61
+ * values are what the run has to mask.
62
+ */
63
+ async credentials(repo) {
64
+ const env = {};
65
+ for (const [key, value] of Object.entries(this.environment.processEnvs)) {
66
+ if (value !== undefined) {
67
+ env[key] = value;
68
+ }
69
+ }
70
+ const secret = this.source(repo)?.secret;
71
+ if (!secret) {
72
+ return { env, values: [] };
73
+ }
74
+ const token = await this.token(secret);
75
+ const host = repo.split('/')[0];
76
+ const header = `Authorization: Basic ${Buffer.from(`${secret.username}:${token}`).toString('base64')}`;
77
+ env.GIT_CONFIG_COUNT = '1';
78
+ env.GIT_CONFIG_KEY_0 = `http.https://${host}/.extraHeader`;
79
+ env.GIT_CONFIG_VALUE_0 = header;
80
+ return { env, values: [token, header] };
81
+ }
82
+ async token(secret) {
83
+ const key = `${secret.account}\0${secret.from}`;
84
+ const known = this.tokens.get(key);
85
+ if (known && this.now() - known.at < TOKEN_TTL_MS) {
86
+ return known.value;
87
+ }
88
+ const catalog = this.environment.hosted?.catalog;
89
+ const kind = secret.from.substring(0, secret.from.indexOf(':'));
90
+ const ref = secret.from.substring(secret.from.indexOf(':') + 1);
91
+ const provider = catalog.providers[kind];
92
+ const account = catalog.accounts[secret.account];
93
+ const credentials = account?.providers[kind];
94
+ if (!provider || !credentials) {
95
+ throw new Error(`the source credential ${secret.from} as ${secret.account} is not defined`);
96
+ }
97
+ // a fresh environment per read: values are kept here and for a short time only
98
+ const registry = (0, redact_1.createSecretRegistry)();
99
+ const environment = {
100
+ ...this.environment,
101
+ secrets: registry,
102
+ hosted: { catalog, accounts: [secret.account] },
103
+ };
104
+ let value;
105
+ try {
106
+ value = await (0, provider_values_1.fetchProviderValue)({ definition: provider, account: { name: secret.account, env: credentials.env } }, ref, environment);
107
+ }
108
+ catch (e) {
109
+ throw new Error(`could not read the source credential ${secret.from}: ${e instanceof Error ? e.message : e}`);
110
+ }
111
+ if (value.length === 0) {
112
+ throw new Error(`the source credential ${secret.from} is empty`);
113
+ }
114
+ this.tokens.set(key, { value, at: this.now() });
115
+ return value;
116
+ }
117
+ // An error as the caller may read it: without the token.
118
+ scrub(error, values) {
119
+ const registry = (0, redact_1.createSecretRegistry)();
120
+ values.forEach((value) => registry.register(value));
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ const scrubbed = new (error instanceof Error ? error.constructor : Error)(registry.redact(message));
123
+ return scrubbed;
124
+ }
125
+ }
126
+ exports.RepoAccess = RepoAccess;
127
+ //# sourceMappingURL=repo-access.js.map
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ // Repositories are named `host/path`, such as `github.com/corp/payments` or
3
+ // `gitlab.corp.com/platform/team/tool`, so one pattern can cover a GitHub
4
+ // organisation or a GitLab group with its subgroups. Names are compared in
5
+ // lower case: both hosts treat them that way.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.matchesRef = exports.matchesAnyRepo = exports.matchesRepo = exports.normalizeRepoPattern = exports.isRepoPattern = exports.normalizeRepo = void 0;
8
+ const SEGMENT = /^[a-z0-9._-]+$/;
9
+ const PATTERN_SEGMENT = /^[a-z0-9._*-]+$/;
10
+ // `host/path` of what a caller or an operator wrote, or null when it is not a
11
+ // plain repository address: no credentials, port, query, option-like names or
12
+ // `..`. A scheme (`https://`) and a trailing `.git` are accepted and dropped.
13
+ function normalizeRepo(input) {
14
+ let value = input.trim().toLowerCase();
15
+ value = value.replace(/^https:\/\//, '');
16
+ value = value.replace(/\.git$/, '').replace(/\/+$/, '');
17
+ const parts = value.split('/');
18
+ if (parts.length < 2 || parts.length > 12) {
19
+ return null;
20
+ }
21
+ const [host, ...path] = parts;
22
+ if (!/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/.test(host) || host.includes('..')) {
23
+ return null;
24
+ }
25
+ if (path.some((part) => !SEGMENT.test(part) || part === '.' || part === '..' || part.startsWith('-'))) {
26
+ return null;
27
+ }
28
+ return [host, ...path].join('/');
29
+ }
30
+ exports.normalizeRepo = normalizeRepo;
31
+ // A pattern is a repository where a segment may hold `*` (any characters but `/`),
32
+ // and the last one may be `**`: everything below, subgroups included.
33
+ function isRepoPattern(input) {
34
+ const value = input
35
+ .trim()
36
+ .toLowerCase()
37
+ .replace(/^https:\/\//, '')
38
+ .replace(/\/+$/, '');
39
+ const parts = value.split('/');
40
+ if (parts.length < 2 || !/^[a-z0-9*]([a-z0-9.*-]*[a-z0-9*])?$/.test(parts[0])) {
41
+ return false;
42
+ }
43
+ return parts.slice(1).every((part, index, rest) => {
44
+ if (part === '**') {
45
+ return index === rest.length - 1;
46
+ }
47
+ return PATTERN_SEGMENT.test(part) && part !== '.' && part !== '..' && !part.startsWith('-') && !part.includes('**');
48
+ });
49
+ }
50
+ exports.isRepoPattern = isRepoPattern;
51
+ function normalizeRepoPattern(input) {
52
+ return input
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/^https:\/\//, '')
56
+ .replace(/\/+$/, '');
57
+ }
58
+ exports.normalizeRepoPattern = normalizeRepoPattern;
59
+ function matchesRepo(pattern, repo) {
60
+ const parts = normalizeRepoPattern(pattern).split('/');
61
+ const target = repo.split('/');
62
+ const last = parts[parts.length - 1];
63
+ if (last === '**') {
64
+ const fixed = parts.slice(0, -1);
65
+ return target.length > fixed.length && fixed.every((part, index) => segmentMatches(part, target[index]));
66
+ }
67
+ return parts.length === target.length && parts.every((part, index) => segmentMatches(part, target[index]));
68
+ }
69
+ exports.matchesRepo = matchesRepo;
70
+ function segmentMatches(pattern, segment) {
71
+ const regex = pattern
72
+ .split('*')
73
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
74
+ .join('[^/]*');
75
+ return new RegExp(`^${regex}$`).test(segment);
76
+ }
77
+ function matchesAnyRepo(patterns, repo) {
78
+ return patterns.some((pattern) => matchesRepo(pattern, repo));
79
+ }
80
+ exports.matchesAnyRepo = matchesAnyRepo;
81
+ // A ref is matched as written: `main`, `release/*` (one level) or `release/**`.
82
+ function matchesRef(patterns, ref) {
83
+ return patterns.some((pattern) => {
84
+ const parts = pattern.split('/');
85
+ const target = ref.split('/');
86
+ if (parts[parts.length - 1] === '**') {
87
+ const fixed = parts.slice(0, -1);
88
+ return target.length > fixed.length && fixed.every((part, index) => segmentMatches(part, target[index]));
89
+ }
90
+ return parts.length === target.length && parts.every((part, index) => segmentMatches(part, target[index]));
91
+ });
92
+ }
93
+ exports.matchesRef = matchesRef;
94
+ //# sourceMappingURL=repo.js.map
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runEnvironment = void 0;
4
+ // What a run sees of the server's environment: the variables the operator listed
5
+ // in `runs.env.allow`, and the fixed ones of `runs.env.set`. The credentials of a
6
+ // run are not among them, they exist only as the secrets of an account.
7
+ function runEnvironment(config, processEnvs) {
8
+ const passed = {};
9
+ for (const name of config.runs.env.allow) {
10
+ // Windows names are not case sensitive
11
+ const windows = process.platform === 'win32';
12
+ const key = Object.keys(processEnvs).find((k) => k === name || (windows && k.toLowerCase() === name.toLowerCase()));
13
+ if (key !== undefined && processEnvs[key] !== undefined) {
14
+ passed[name] = processEnvs[key];
15
+ }
16
+ }
17
+ return { ...passed, ...config.runs.env.set };
18
+ }
19
+ exports.runEnvironment = runEnvironment;
20
+ //# sourceMappingURL=run-environment.js.map
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planRun = exports.getRunScopes = void 0;
4
+ const parse_label_arguments_1 = require("../parser/parse-label-arguments");
5
+ const plan_1 = require("./plan");
6
+ function labels(values) {
7
+ return values && values.length > 0 ? (0, parse_label_arguments_1.parseLabelArguments)(values) : undefined;
8
+ }
9
+ // The tasks in the order they were named, one scope each; by labels when none is.
10
+ function getRunScopes(tasks, options) {
11
+ return tasks.length > 0
12
+ ? tasks.map((task) => ({
13
+ name: task,
14
+ scope: { taskName: task, environmentName: options.env ?? null },
15
+ }))
16
+ : [
17
+ {
18
+ name: 'labels',
19
+ scope: {
20
+ filterLabels: labels(options.filter),
21
+ excludeLabels: labels(options.exclude),
22
+ environmentName: options.env ?? null,
23
+ },
24
+ },
25
+ ];
26
+ }
27
+ exports.getRunScopes = getRunScopes;
28
+ /**
29
+ * What a run will do, read with the engine from the files of the version it is
30
+ * pinned to and with the environment a run has: the same function answers at
31
+ * submit, and again in the executor, so what was approved is what runs.
32
+ */
33
+ async function planRun(bundle, run, environment, policy) {
34
+ const plan = {};
35
+ for (const { scope } of getRunScopes(run.tasks, run.options)) {
36
+ Object.assign(plan, (await (0, plan_1.planBundle)(bundle, environment, { scope, policy })).plan);
37
+ }
38
+ return {
39
+ plan,
40
+ accounts: [...new Set(Object.values(plan).flatMap((entry) => entry.accounts))].sort(),
41
+ };
42
+ }
43
+ exports.planRun = planRun;
44
+ //# sourceMappingURL=run-plan.js.map