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.
- package/build.schema.json +10 -0
- package/dist/cli.js +6 -0
- package/dist/executer/execute-command.js +7 -1
- package/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/utils/append-work-task.js +1 -0
- package/dist/planner/work-secret.js +6 -2
- package/dist/program.js +6 -0
- package/dist/remote/bundle.js +59 -0
- package/dist/remote/collect-bundle.js +41 -0
- package/dist/remote/credentials.js +93 -0
- package/dist/remote/device-login.js +177 -0
- package/dist/remote/host-config.js +170 -0
- package/dist/remote/location.js +46 -0
- package/dist/remote/remote-commands.js +158 -0
- package/dist/remote/remote-work-commands.js +224 -0
- package/dist/remote/server-client.js +88 -0
- package/dist/remote/source-manifest.js +79 -0
- package/dist/remote/text-diff.js +89 -0
- package/dist/schema/build-file-base-task-schema.js +4 -0
- package/dist/schema/build-file-schema.js +3 -0
- package/dist/schema/reference-parser.js +9 -2
- package/dist/schema/runner-schema.js +13 -0
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +104 -0
- package/dist/server/definitions-api.js +163 -0
- package/dist/server/definitions.js +160 -0
- package/dist/server/discovery.js +9 -0
- package/dist/server/executor.js +237 -0
- package/dist/server/git-checkout.js +115 -0
- package/dist/server/http.js +39 -0
- package/dist/server/oauth.js +226 -0
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +138 -0
- package/dist/server/proposals-api.js +145 -0
- package/dist/server/proposals.js +173 -0
- package/dist/server/repo-access.js +127 -0
- package/dist/server/repo.js +94 -0
- package/dist/server/run-environment.js +20 -0
- package/dist/server/run-plan.js +44 -0
- package/dist/server/run-request.js +85 -0
- package/dist/server/run-store.js +61 -0
- package/dist/server/run-worker-command.js +17 -0
- package/dist/server/run-worker-main.js +7 -0
- package/dist/server/run-worker.js +179 -0
- package/dist/server/runs.js +432 -0
- package/dist/server/server-command.js +61 -0
- package/dist/server/server-config.js +394 -0
- package/dist/server/server.js +434 -0
- package/dist/server/sources-api.js +106 -0
- package/dist/server/sources.js +258 -0
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pinLogin = exports.setDefaultLocation = exports.removeServer = exports.addServer = exports.normalizeServerUrl = exports.validateServerName = exports.writeHostConfig = exports.readHostConfig = exports.getHostConfigPath = exports.hasServer = exports.hostConfigSchema = exports.hostServerSchema = exports.RESERVED_SERVER_NAMES = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const yaml_1 = require("yaml");
|
|
7
|
+
const get_cache_directory_1 = require("../optimizer/get-cache-directory");
|
|
8
|
+
// Names with a meaning of their own in `run.on` / `--on`
|
|
9
|
+
exports.RESERVED_SERVER_NAMES = ['local', 'auto'];
|
|
10
|
+
const serverNamePattern = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
11
|
+
const checked = (what) => (value, ctx) => {
|
|
12
|
+
try {
|
|
13
|
+
normalizeServerUrl(value, what);
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: e.message });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// `clientId`, `audience` and `scope` are what a login asks the identity provider
|
|
20
|
+
// for. They are pinned the first time a login happens (or given to `remote add`),
|
|
21
|
+
// so a server can't change them later, see device-login.ts.
|
|
22
|
+
exports.hostServerSchema = zod_1.z
|
|
23
|
+
.object({
|
|
24
|
+
url: zod_1.z.string().superRefine(checked('url')),
|
|
25
|
+
issuer: zod_1.z.string().superRefine(checked('issuer')),
|
|
26
|
+
clientId: zod_1.z.string().min(1).optional(),
|
|
27
|
+
audience: zod_1.z.string().min(1).optional(),
|
|
28
|
+
scope: zod_1.z.string().min(1).optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
// Unknown top-level keys are kept when the file is written back, so a newer
|
|
32
|
+
// hammerkit's settings survive an older one editing the file.
|
|
33
|
+
exports.hostConfigSchema = zod_1.z
|
|
34
|
+
.object({
|
|
35
|
+
servers: zod_1.z.record(exports.hostServerSchema).default({}),
|
|
36
|
+
run: zod_1.z.object({ on: zod_1.z.string().optional() }).passthrough().default({}),
|
|
37
|
+
})
|
|
38
|
+
.passthrough();
|
|
39
|
+
// Names like "constructor" must not find inherited members.
|
|
40
|
+
function hasServer(servers, name) {
|
|
41
|
+
return Object.prototype.hasOwnProperty.call(servers, name);
|
|
42
|
+
}
|
|
43
|
+
exports.hasServer = hasServer;
|
|
44
|
+
function getHostConfigPath(environment) {
|
|
45
|
+
return environment.processEnvs.HAMMERKIT_CONFIG ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'config.yaml');
|
|
46
|
+
}
|
|
47
|
+
exports.getHostConfigPath = getHostConfigPath;
|
|
48
|
+
async function readHostConfig(environment) {
|
|
49
|
+
const path = getHostConfigPath(environment);
|
|
50
|
+
if (!(await environment.file.exists(path))) {
|
|
51
|
+
return exports.hostConfigSchema.parse({});
|
|
52
|
+
}
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = (0, yaml_1.parse)(await environment.file.read(path));
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
throw new Error(`unable to read ${path}: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
const result = exports.hostConfigSchema.safeParse(raw ?? {});
|
|
61
|
+
if (!result.success) {
|
|
62
|
+
const issue = result.error.issues[0];
|
|
63
|
+
throw new Error(`invalid ${path}: ${issue.path.join('.') || 'config'}: ${issue.message}`);
|
|
64
|
+
}
|
|
65
|
+
return result.data;
|
|
66
|
+
}
|
|
67
|
+
exports.readHostConfig = readHostConfig;
|
|
68
|
+
async function writeHostConfig(environment, config) {
|
|
69
|
+
const path = getHostConfigPath(environment);
|
|
70
|
+
await environment.file.createDirectory((0, path_1.join)(path, '..'));
|
|
71
|
+
await environment.file.writeFile(path, (0, yaml_1.stringify)(config));
|
|
72
|
+
}
|
|
73
|
+
exports.writeHostConfig = writeHostConfig;
|
|
74
|
+
function validateServerName(name) {
|
|
75
|
+
if (exports.RESERVED_SERVER_NAMES.includes(name.toLowerCase())) {
|
|
76
|
+
throw new Error(`"${name}" is reserved and cannot be used as a server name`);
|
|
77
|
+
}
|
|
78
|
+
if (!serverNamePattern.test(name)) {
|
|
79
|
+
throw new Error(`invalid server name "${name}": use letters, digits, ".", "_" and "-", starting with a letter or digit`);
|
|
80
|
+
}
|
|
81
|
+
return name;
|
|
82
|
+
}
|
|
83
|
+
exports.validateServerName = validateServerName;
|
|
84
|
+
function isLoopback(hostname) {
|
|
85
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
|
86
|
+
}
|
|
87
|
+
// A server receives the source of a run and the caller's OAuth token, so it must
|
|
88
|
+
// be reached over https; plain http is only for a server on this machine. The
|
|
89
|
+
// result has no trailing slash, so one server has one spelling.
|
|
90
|
+
// An issuer is compared as a string with the `iss` of tokens, so it keeps the
|
|
91
|
+
// spelling it was given (some providers end it with a slash, some don't).
|
|
92
|
+
function normalizeServerUrl(value, what = 'url') {
|
|
93
|
+
let url;
|
|
94
|
+
try {
|
|
95
|
+
url = new URL(value);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error(`invalid ${what} "${value}"`);
|
|
99
|
+
}
|
|
100
|
+
if (url.username || url.password) {
|
|
101
|
+
throw new Error(`the ${what} must not contain credentials`);
|
|
102
|
+
}
|
|
103
|
+
if (url.search || url.hash) {
|
|
104
|
+
throw new Error(`the ${what} must not contain a query or fragment`);
|
|
105
|
+
}
|
|
106
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback(url.hostname))) {
|
|
107
|
+
throw new Error(`the ${what} must use https (http is only allowed for localhost), got "${value}"`);
|
|
108
|
+
}
|
|
109
|
+
return what === 'issuer' ? value : url.toString().replace(/\/+$/, '');
|
|
110
|
+
}
|
|
111
|
+
exports.normalizeServerUrl = normalizeServerUrl;
|
|
112
|
+
const PINNED = ['clientId', 'audience', 'scope'];
|
|
113
|
+
async function addServer(environment, name, url, options) {
|
|
114
|
+
validateServerName(name);
|
|
115
|
+
const server = {
|
|
116
|
+
url: normalizeServerUrl(url),
|
|
117
|
+
issuer: normalizeServerUrl(options.issuer, 'issuer'),
|
|
118
|
+
};
|
|
119
|
+
for (const key of PINNED) {
|
|
120
|
+
if (options[key]) {
|
|
121
|
+
server[key] = options[key];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const config = await readHostConfig(environment);
|
|
125
|
+
const existing = hasServer(config.servers, name) ? config.servers[name] : undefined;
|
|
126
|
+
const changed = (a, b) => a.url !== b.url || a.issuer !== b.issuer || PINNED.some((key) => a[key] !== b[key]);
|
|
127
|
+
if (existing && changed(existing, server) && !options.force) {
|
|
128
|
+
throw new Error(`server "${name}" is already registered as ${existing.url} (issuer ${existing.issuer}); ` +
|
|
129
|
+
`use --force to point it somewhere else`);
|
|
130
|
+
}
|
|
131
|
+
config.servers[name] = server;
|
|
132
|
+
await writeHostConfig(environment, config);
|
|
133
|
+
return server;
|
|
134
|
+
}
|
|
135
|
+
exports.addServer = addServer;
|
|
136
|
+
async function removeServer(environment, name) {
|
|
137
|
+
const config = await readHostConfig(environment);
|
|
138
|
+
if (!hasServer(config.servers, name)) {
|
|
139
|
+
throw new Error(`server "${name}" is not registered`);
|
|
140
|
+
}
|
|
141
|
+
delete config.servers[name];
|
|
142
|
+
const resetDefault = config.run.on === name;
|
|
143
|
+
if (resetDefault) {
|
|
144
|
+
delete config.run.on;
|
|
145
|
+
}
|
|
146
|
+
await writeHostConfig(environment, config);
|
|
147
|
+
return { resetDefault };
|
|
148
|
+
}
|
|
149
|
+
exports.removeServer = removeServer;
|
|
150
|
+
async function setDefaultLocation(environment, on) {
|
|
151
|
+
const config = await readHostConfig(environment);
|
|
152
|
+
if (!exports.RESERVED_SERVER_NAMES.includes(on) && !hasServer(config.servers, on)) {
|
|
153
|
+
throw new Error(`server "${on}" is not registered; add it with: hammerkit remote add ${on} <url> --issuer <url>`);
|
|
154
|
+
}
|
|
155
|
+
config.run.on = on;
|
|
156
|
+
await writeHostConfig(environment, config);
|
|
157
|
+
}
|
|
158
|
+
exports.setDefaultLocation = setDefaultLocation;
|
|
159
|
+
// Remember what the first login asked the identity provider for; later logins
|
|
160
|
+
// have to ask for the same.
|
|
161
|
+
async function pinLogin(environment, name, pins) {
|
|
162
|
+
const config = await readHostConfig(environment);
|
|
163
|
+
if (!hasServer(config.servers, name)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
Object.assign(config.servers[name], pins);
|
|
167
|
+
await writeHostConfig(environment, config);
|
|
168
|
+
}
|
|
169
|
+
exports.pinLogin = pinLogin;
|
|
170
|
+
//# sourceMappingURL=host-config.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRunLocation = void 0;
|
|
4
|
+
const host_config_1 = require("./host-config");
|
|
5
|
+
// Where a run happens, highest precedence first: --on / --local, HAMMERKIT_ON,
|
|
6
|
+
// the runner of the tasks, `run.on` of the host config, local. `auto` (as
|
|
7
|
+
// --on, HAMMERKIT_ON or run.on) means the runner of the tasks, else local.
|
|
8
|
+
// A name that is not registered is an error: a run never falls back to the
|
|
9
|
+
// local machine silently.
|
|
10
|
+
async function resolveRunLocation(workTree, environment, options = {}) {
|
|
11
|
+
if (options.on && options.local) {
|
|
12
|
+
throw new Error('--on and --local contradict each other');
|
|
13
|
+
}
|
|
14
|
+
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
15
|
+
const explicit = options.local ? 'local' : options.on ?? (environment.processEnvs.HAMMERKIT_ON || undefined);
|
|
16
|
+
if (explicit && explicit !== 'auto') {
|
|
17
|
+
return explicit === 'local' ? { type: 'local' } : lookup(config.servers, explicit);
|
|
18
|
+
}
|
|
19
|
+
const runners = getTaskRunners(workTree);
|
|
20
|
+
if (runners.length > 1) {
|
|
21
|
+
throw new Error(`a run goes to one server, but its tasks name ${runners.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
const requested = runners[0] ?? (explicit ? undefined : config.run.on);
|
|
24
|
+
if (!requested || requested === 'local' || requested === 'auto') {
|
|
25
|
+
return { type: 'local' };
|
|
26
|
+
}
|
|
27
|
+
return lookup(config.servers, requested);
|
|
28
|
+
}
|
|
29
|
+
exports.resolveRunLocation = resolveRunLocation;
|
|
30
|
+
function lookup(servers, name) {
|
|
31
|
+
const server = (0, host_config_1.hasServer)(servers, name) ? servers[name] : undefined;
|
|
32
|
+
if (!server) {
|
|
33
|
+
throw new Error(`server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`);
|
|
34
|
+
}
|
|
35
|
+
return { type: 'server', name, server };
|
|
36
|
+
}
|
|
37
|
+
function getTaskRunners(workTree) {
|
|
38
|
+
const runners = new Set();
|
|
39
|
+
for (const task of Object.values(workTree.tasks)) {
|
|
40
|
+
if (task.data.runner) {
|
|
41
|
+
runners.add(task.data.runner);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...runners].sort();
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=location.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerRemoteCommands = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const colors_1 = __importDefault(require("colors"));
|
|
9
|
+
const log_1 = require("../log");
|
|
10
|
+
const host_config_1 = require("./host-config");
|
|
11
|
+
const device_login_1 = require("./device-login");
|
|
12
|
+
const credentials_1 = require("./credentials");
|
|
13
|
+
const server_client_1 = require("./server-client");
|
|
14
|
+
const remote_work_commands_1 = require("./remote-work-commands");
|
|
15
|
+
// Host-level commands: they only touch the machine's config file, so they work
|
|
16
|
+
// with or without a build file in the working directory.
|
|
17
|
+
function registerRemoteCommands(program, environment, buildFile) {
|
|
18
|
+
const remote = program.command('remote').description('register the servers runs can be sent to');
|
|
19
|
+
const guarded = (failure, fn) => {
|
|
20
|
+
return async (...args) => {
|
|
21
|
+
try {
|
|
22
|
+
await fn(...args);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
if (e instanceof commander_1.CommanderError) {
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
program.error(`${failure}: ${(0, log_1.getErrorMessage)(e)}`, { exitCode: 1 });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
remote
|
|
33
|
+
.command('add <name> <url>')
|
|
34
|
+
.description('register a server under a name that build files and --on can refer to')
|
|
35
|
+
.addOption(new commander_1.Option('--issuer <url>', 'OAuth issuer the server accepts tokens from').makeOptionMandatory())
|
|
36
|
+
.addOption(new commander_1.Option('--client-id <id>', 'OAuth client to sign in with (pinned; default: what the server offers)'))
|
|
37
|
+
.addOption(new commander_1.Option('--audience <audience>', 'audience of the token (pinned; default: what the server offers)'))
|
|
38
|
+
.addOption(new commander_1.Option('--scope <scope>', 'scopes to request (pinned; default: what the server offers)'))
|
|
39
|
+
.addOption(new commander_1.Option('--force', 'point an already registered name at a different server').default(false))
|
|
40
|
+
.action(guarded('Remote add was not successful', async (name, url, options) => {
|
|
41
|
+
const server = await (0, host_config_1.addServer)(environment, name, url, {
|
|
42
|
+
issuer: options.issuer,
|
|
43
|
+
clientId: options.clientId,
|
|
44
|
+
audience: options.audience,
|
|
45
|
+
scope: options.scope,
|
|
46
|
+
force: options.force,
|
|
47
|
+
});
|
|
48
|
+
environment.stdout.write(`added ${name}: ${server.url} (issuer ${server.issuer})\n`);
|
|
49
|
+
}));
|
|
50
|
+
remote
|
|
51
|
+
.command('list')
|
|
52
|
+
.description('list the registered servers')
|
|
53
|
+
.action(guarded('Remote list was not successful', async () => {
|
|
54
|
+
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
55
|
+
const names = Object.keys(config.servers).sort();
|
|
56
|
+
if (names.length === 0) {
|
|
57
|
+
environment.stdout.write(`no servers registered (config: ${(0, host_config_1.getHostConfigPath)(environment)})\n`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
const server = config.servers[name];
|
|
62
|
+
const marker = config.run.on === name ? colors_1.default.green(' (default)') : '';
|
|
63
|
+
environment.stdout.write(`${colors_1.default.bold(name)}${marker} ${server.url} issuer ${server.issuer}\n`);
|
|
64
|
+
}
|
|
65
|
+
if (config.run.on === 'auto' || config.run.on === 'local') {
|
|
66
|
+
environment.stdout.write(`default location: ${config.run.on}\n`);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
remote
|
|
70
|
+
.command('remove <name>')
|
|
71
|
+
.description('forget a registered server')
|
|
72
|
+
.action(guarded('Remote remove was not successful', async (name) => {
|
|
73
|
+
const { resetDefault } = await (0, host_config_1.removeServer)(environment, name);
|
|
74
|
+
environment.stdout.write(`removed ${name}\n`);
|
|
75
|
+
if (resetDefault) {
|
|
76
|
+
environment.stdout.write('it was the default location, runs are local again\n');
|
|
77
|
+
}
|
|
78
|
+
}));
|
|
79
|
+
remote
|
|
80
|
+
.command('use <location>')
|
|
81
|
+
.description('set where runs happen by default: local, auto or a registered server')
|
|
82
|
+
.action(guarded('Remote use was not successful', async (location) => {
|
|
83
|
+
await (0, host_config_1.setDefaultLocation)(environment, location);
|
|
84
|
+
environment.stdout.write(`default location: ${location}\n`);
|
|
85
|
+
}));
|
|
86
|
+
const getServer = async (name) => {
|
|
87
|
+
const config = await (0, host_config_1.readHostConfig)(environment);
|
|
88
|
+
const chosen = name ?? config.run.on;
|
|
89
|
+
if (!chosen || !(0, host_config_1.hasServer)(config.servers, chosen)) {
|
|
90
|
+
throw new Error(name
|
|
91
|
+
? `server ${name} is not registered, add it with: hammerkit remote add ${name} <url> --issuer <url>`
|
|
92
|
+
: 'name a server, or set one as the default with: hammerkit remote use <name>');
|
|
93
|
+
}
|
|
94
|
+
return { name: chosen, server: config.servers[chosen] };
|
|
95
|
+
};
|
|
96
|
+
remote
|
|
97
|
+
.command('login <name>')
|
|
98
|
+
.description('sign in to a registered server with your identity provider (device code, works headless)')
|
|
99
|
+
.action(guarded('Remote login was not successful', async (name) => {
|
|
100
|
+
const { server } = await getServer(name);
|
|
101
|
+
const { pinned, ...result } = await (0, device_login_1.deviceLogin)(server, {
|
|
102
|
+
prompt: (message) => environment.stdout.write(`${message}\n`),
|
|
103
|
+
signal: environment.abortCtrl.signal,
|
|
104
|
+
});
|
|
105
|
+
await (0, host_config_1.pinLogin)(environment, name, pinned);
|
|
106
|
+
await (0, credentials_1.saveToken)(environment, name, { url: server.url, issuer: server.issuer, ...result });
|
|
107
|
+
try {
|
|
108
|
+
const me = await (0, server_client_1.getMe)(server, result.accessToken);
|
|
109
|
+
environment.stdout.write(`signed in to ${name} as ${me.sub}\n`);
|
|
110
|
+
environment.stdout.write(me.accounts.length > 0
|
|
111
|
+
? `service accounts: ${me.accounts.map((a) => `${(0, server_client_1.printable)(a.name)} (${a.level})`).join(', ')}\n`
|
|
112
|
+
: 'you have no service accounts on this server\n');
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// a token the server does not accept is of no use
|
|
116
|
+
await (0, credentials_1.removeToken)(environment, name);
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
remote
|
|
121
|
+
.command('logout <name>')
|
|
122
|
+
.description('forget the sign-in to a server')
|
|
123
|
+
.action(guarded('Remote logout was not successful', async (name) => {
|
|
124
|
+
const removed = await (0, credentials_1.removeToken)(environment, name);
|
|
125
|
+
environment.stdout.write(removed ? `signed out of ${name}\n` : `not signed in to ${name}\n`);
|
|
126
|
+
}));
|
|
127
|
+
remote
|
|
128
|
+
.command('accounts [name]')
|
|
129
|
+
.description('show who you are on a server and which service accounts you may use')
|
|
130
|
+
.action(guarded('Remote accounts was not successful', async (name) => {
|
|
131
|
+
const chosen = await getServer(name);
|
|
132
|
+
const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
|
|
133
|
+
const me = await (0, server_client_1.getMe)(chosen.server, token);
|
|
134
|
+
const { body } = await (0, server_client_1.callServer)(chosen.server, token, 'GET', '/v1/accounts');
|
|
135
|
+
environment.stdout.write(`${chosen.name}: ${(0, server_client_1.printable)(me.sub)} (${(0, server_client_1.printable)(me.iss)})\n`);
|
|
136
|
+
if (body.accounts.length === 0) {
|
|
137
|
+
environment.stdout.write('no service accounts\n');
|
|
138
|
+
}
|
|
139
|
+
for (const account of body.accounts) {
|
|
140
|
+
const level = account.level === 'use' ? 'use' : 'request, somebody who uses it approves';
|
|
141
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(account.name))} ${level}${account.description ? ` ${(0, server_client_1.printable)(account.description)}` : ''} from ${account.sources.map(server_client_1.printable).join(', ')}\n`);
|
|
142
|
+
}
|
|
143
|
+
if (me.builds) {
|
|
144
|
+
const may = {
|
|
145
|
+
run: 'run published build files',
|
|
146
|
+
propose: 'propose build files, which somebody who publishes approves',
|
|
147
|
+
publish: 'publish build files',
|
|
148
|
+
}[me.builds];
|
|
149
|
+
environment.stdout.write(`you may ${may}\n`);
|
|
150
|
+
}
|
|
151
|
+
if (me.repos.length > 0) {
|
|
152
|
+
environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
(0, remote_work_commands_1.registerWorkCommands)(remote, { environment, buildFile, getServer, guarded });
|
|
156
|
+
}
|
|
157
|
+
exports.registerRemoteCommands = registerRemoteCommands;
|
|
158
|
+
//# sourceMappingURL=remote-commands.js.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerWorkCommands = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const colors_1 = __importDefault(require("colors"));
|
|
9
|
+
const credentials_1 = require("./credentials");
|
|
10
|
+
const collect_bundle_1 = require("./collect-bundle");
|
|
11
|
+
const bundle_1 = require("./bundle");
|
|
12
|
+
const server_client_1 = require("./server-client");
|
|
13
|
+
const text_diff_1 = require("./text-diff");
|
|
14
|
+
const PROPOSAL_NAME = /^([a-z0-9][a-z0-9._-]{0,63})@(sha256:[0-9a-f]{64})$/i;
|
|
15
|
+
// A proposed version is named like `payments@sha256:...`
|
|
16
|
+
function parseProposal(value) {
|
|
17
|
+
const match = PROPOSAL_NAME.exec(value);
|
|
18
|
+
return match && bundle_1.HASH_PATTERN.test(match[2]) ? { name: match[1], hash: match[2] } : undefined;
|
|
19
|
+
}
|
|
20
|
+
const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
|
|
21
|
+
// What a person who approves is shown of a run: everything the decision rests on.
|
|
22
|
+
function describe(run) {
|
|
23
|
+
const source = run.source.type === 'ref' ? `ref ${run.source.ref}` : `snapshot ${run.source.snapshot}`;
|
|
24
|
+
const options = Object.entries(run.options)
|
|
25
|
+
.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
|
|
26
|
+
.join(' ');
|
|
27
|
+
return [
|
|
28
|
+
`${colors_1.default.bold((0, server_client_1.printable)(run.id))} requested by ${(0, server_client_1.printable)(run.requestedBy.sub)} (${(0, server_client_1.printable)(run.requestedBy.iss)})`,
|
|
29
|
+
` ${(0, server_client_1.printable)(run.definition.name)} ${(0, server_client_1.printable)(run.definition.hash)}`,
|
|
30
|
+
` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
|
|
31
|
+
` accounts: ${run.accounts.length > 0 ? run.accounts.map(server_client_1.printable).join(', ') : 'none'}, source ${(0, server_client_1.printable)(source)}${options ? `, ${(0, server_client_1.printable)(options)}` : ''}`,
|
|
32
|
+
...(run.approvals ?? []).map((approval) => ` ${(0, server_client_1.printable)(approval.account)}: ${approval.approvedBy
|
|
33
|
+
? `approved by ${(0, server_client_1.printable)(approval.approvedBy.sub)}`
|
|
34
|
+
: 'waits for somebody who uses it to approve'}`),
|
|
35
|
+
...flagLines(run),
|
|
36
|
+
` decide before ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`,
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
// What to look at first, one line.
|
|
40
|
+
function flagLines(run) {
|
|
41
|
+
const flagged = Object.entries(run.plan ?? {}).filter(([, entry]) => entry.flags.length > 0);
|
|
42
|
+
return flagged.length > 0
|
|
43
|
+
? [
|
|
44
|
+
` look at: ${flagged
|
|
45
|
+
.map(([name, entry]) => `${(0, server_client_1.printable)(name)} (${entry.flags.map(server_client_1.printable).join(', ')})`)
|
|
46
|
+
.join(', ')}`,
|
|
47
|
+
]
|
|
48
|
+
: [];
|
|
49
|
+
}
|
|
50
|
+
// The tasks and services the run executes and what each does, never a secret's value.
|
|
51
|
+
function describePlan(plan) {
|
|
52
|
+
return Object.entries(plan).flatMap(([name, entry]) => [
|
|
53
|
+
` ${colors_1.default.bold((0, server_client_1.printable)(name))} ${entry.kind}, ${(0, server_client_1.printable)(entry.runtime)}${entry.image ? ` ${(0, server_client_1.printable)(entry.image)}` : ''}${entry.flags.length > 0 ? ` [${entry.flags.map(server_client_1.printable).join(', ')}]` : ''}`,
|
|
54
|
+
...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printable)(cmd)}`),
|
|
55
|
+
...(entry.deps.length > 0 ? [` after: ${entry.deps.map(server_client_1.printable).join(', ')}`] : []),
|
|
56
|
+
...(entry.needs.length > 0 ? [` needs: ${entry.needs.map(server_client_1.printable).join(', ')}`] : []),
|
|
57
|
+
...entry.secrets.map((secret) => ` secret ${(0, server_client_1.printable)(secret.from)}${secret.account ? ` as ${(0, server_client_1.printable)(secret.account)}` : ''} -> ${(0, server_client_1.printable)(secret.target)}`),
|
|
58
|
+
...(entry.mounts.length > 0 ? [` mounts: ${entry.mounts.map(server_client_1.printable).join(', ')}`] : []),
|
|
59
|
+
...(entry.ports.length > 0 ? [` ports: ${entry.ports.map(server_client_1.printable).join(', ')}`] : []),
|
|
60
|
+
...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
function registerWorkCommands(remote, context) {
|
|
64
|
+
const { environment, buildFile, getServer, guarded } = context;
|
|
65
|
+
const connect = async (name) => {
|
|
66
|
+
const chosen = await getServer(name);
|
|
67
|
+
const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
|
|
68
|
+
return { ...chosen, token };
|
|
69
|
+
};
|
|
70
|
+
remote
|
|
71
|
+
.command('publish <definition> [server]')
|
|
72
|
+
.description('publish the build file and the files it includes to a server, under a name runs can refer to')
|
|
73
|
+
.action(guarded('Remote publish was not successful', async (definition, name) => {
|
|
74
|
+
const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
|
|
75
|
+
const { name: serverName, server, token } = await connect(name);
|
|
76
|
+
const answer = await (0, server_client_1.callServer)(server, token, 'PUT', `/v1/definitions/${encodeURIComponent(definition)}`, bundle);
|
|
77
|
+
const version = answer.body;
|
|
78
|
+
const files = Object.keys(bundle.files).length;
|
|
79
|
+
const proposed = answer.status === 202;
|
|
80
|
+
const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
|
|
81
|
+
environment.stdout.write(`${verb} ${definition} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
|
|
82
|
+
const names = [...(version.tasks ?? []), ...(version.services ?? [])];
|
|
83
|
+
environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
84
|
+
if (proposed) {
|
|
85
|
+
environment.stdout.write(`it can not run before somebody who may publish ${(0, server_client_1.printable)(definition)} approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
remote
|
|
89
|
+
.command('approvals [server]')
|
|
90
|
+
.description('list the runs on a server that wait for your approval')
|
|
91
|
+
.action(guarded('Remote approvals was not successful', async (name) => {
|
|
92
|
+
const { name: serverName, server, token } = await connect(name);
|
|
93
|
+
const { body } = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/approvals');
|
|
94
|
+
if (body.approvals.length === 0) {
|
|
95
|
+
environment.stdout.write(`${serverName}: no runs wait for your approval\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
environment.stdout.write(`${serverName}: ${body.approvals.length} waiting for your approval\n`);
|
|
99
|
+
for (const run of body.approvals) {
|
|
100
|
+
for (const line of describe(run)) {
|
|
101
|
+
environment.stdout.write(`${line}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}));
|
|
105
|
+
remote
|
|
106
|
+
.command('show <run> [server]')
|
|
107
|
+
.description('show a run that waits for your approval in full: the commands, secrets and accounts of its tasks')
|
|
108
|
+
.action(guarded('Remote show was not successful', async (run, name) => {
|
|
109
|
+
if (!RUN_ID.test(run)) {
|
|
110
|
+
throw new Error(`${(0, server_client_1.printable)(run)} is not a run id, list them with: hammerkit remote approvals`);
|
|
111
|
+
}
|
|
112
|
+
const { server, token } = await connect(name);
|
|
113
|
+
const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${run}`)).body;
|
|
114
|
+
for (const line of describe(view)) {
|
|
115
|
+
environment.stdout.write(`${line}\n`);
|
|
116
|
+
}
|
|
117
|
+
environment.stdout.write(view.plan ? `${describePlan(view.plan).join('\n')}\n` : 'the server did not record what its tasks do\n');
|
|
118
|
+
}));
|
|
119
|
+
remote
|
|
120
|
+
.command('proposals [server]')
|
|
121
|
+
.description('list the proposed versions of build files you made or may approve')
|
|
122
|
+
.action(guarded('Remote proposals was not successful', async (name) => {
|
|
123
|
+
const { name: serverName, server, token } = await connect(name);
|
|
124
|
+
const { body } = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/proposals');
|
|
125
|
+
if (body.proposals.length === 0) {
|
|
126
|
+
environment.stdout.write(`${serverName}: no proposed versions\n`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
environment.stdout.write(`${serverName}: ${body.proposals.length} proposed\n`);
|
|
130
|
+
for (const proposal of body.proposals) {
|
|
131
|
+
const who = proposal.mine ? 'you' : (0, server_client_1.printable)(proposal.proposedBy.sub);
|
|
132
|
+
const state = proposal.canDecide ? colors_1.default.bold('waits for your approval') : (0, server_client_1.printable)(proposal.state);
|
|
133
|
+
environment.stdout.write(`${colors_1.default.bold(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)}`)} proposed by ${who}, ${state}\n`);
|
|
134
|
+
const names = [...proposal.tasks, ...proposal.services];
|
|
135
|
+
environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
136
|
+
if (proposal.accounts.length > 0) {
|
|
137
|
+
environment.stdout.write(` accounts: ${proposal.accounts.map(server_client_1.printable).join(', ')}${proposal.missing.length > 0
|
|
138
|
+
? `, the proposer does not use ${proposal.missing.map(server_client_1.printable).join(', ')}`
|
|
139
|
+
: ''}\n`);
|
|
140
|
+
}
|
|
141
|
+
for (const line of flagLines(proposal)) {
|
|
142
|
+
environment.stdout.write(`${line}\n`);
|
|
143
|
+
}
|
|
144
|
+
if (proposal.stale) {
|
|
145
|
+
environment.stdout.write(` stale: ${(0, server_client_1.printable)(proposal.name)} has a newer version, reject it and propose again on top of it\n`);
|
|
146
|
+
}
|
|
147
|
+
if (proposal.state === 'waiting') {
|
|
148
|
+
environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
|
|
149
|
+
}
|
|
150
|
+
else if (proposal.reason) {
|
|
151
|
+
environment.stdout.write(` ${(0, server_client_1.printable)(proposal.state)}: ${(0, server_client_1.printable)(proposal.reason)}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
remote
|
|
156
|
+
.command('diff <proposal> [server]')
|
|
157
|
+
.description('show what a proposed version (name@hash) changes against the version it follows')
|
|
158
|
+
.action(guarded('Remote diff was not successful', async (target, name) => {
|
|
159
|
+
const proposal = parseProposal(target);
|
|
160
|
+
if (!proposal) {
|
|
161
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a proposal, list them with: hammerkit remote proposals`);
|
|
162
|
+
}
|
|
163
|
+
const { name: serverName, server, token } = await connect(name);
|
|
164
|
+
const path = `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}`;
|
|
165
|
+
const view = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
166
|
+
const proposed = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
167
|
+
const base = view.base
|
|
168
|
+
? (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/definitions/${encodeURIComponent(proposal.name)}/${view.base}/files`)).body
|
|
169
|
+
: undefined;
|
|
170
|
+
environment.stdout.write(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)} on ${serverName}, proposed by ${(0, server_client_1.printable)(view.proposedBy.sub)} (${(0, server_client_1.printable)(view.proposedBy.iss)})\n`);
|
|
171
|
+
environment.stdout.write(base ? `compared with ${(0, server_client_1.printable)(view.base)}\n` : 'no version is published yet, everything is new\n');
|
|
172
|
+
if (view.stale) {
|
|
173
|
+
environment.stdout.write(`warning: ${(0, server_client_1.printable)(proposal.name)} has a newer version (${(0, server_client_1.printable)(view.latest)}), this can only be rejected\n`);
|
|
174
|
+
}
|
|
175
|
+
const lines = (0, text_diff_1.diffBundles)(base, proposed);
|
|
176
|
+
environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
|
|
177
|
+
}));
|
|
178
|
+
for (const action of ['yank', 'unyank']) {
|
|
179
|
+
remote
|
|
180
|
+
.command(`${action} <version> [server]`)
|
|
181
|
+
.description(action === 'yank'
|
|
182
|
+
? 'pull a published version (name@hash) back: no run starts from it and it is not latest'
|
|
183
|
+
: 'let a yanked version (name@hash) go on')
|
|
184
|
+
.addOption(new commander_1.Option('--reason <text>', 'why, shown to whoever tries to run it').argParser((value) => value))
|
|
185
|
+
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
186
|
+
const version = parseProposal(target);
|
|
187
|
+
if (!version) {
|
|
188
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it like payments@sha256:...`);
|
|
189
|
+
}
|
|
190
|
+
const { name: serverName, server, token } = await connect(name);
|
|
191
|
+
await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${encodeURIComponent(version.name)}/${version.hash}/${action}`, options.reason ? { reason: options.reason } : undefined);
|
|
192
|
+
environment.stdout.write(`${action === 'yank' ? 'yanked' : 'let go on'} ${(0, server_client_1.printable)(target)} on ${serverName}\n`);
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
for (const action of ['approve', 'reject']) {
|
|
196
|
+
remote
|
|
197
|
+
.command(`${action} <target> [server]`)
|
|
198
|
+
.description(action === 'approve'
|
|
199
|
+
? 'let a run (run_...) or a proposed version (name@hash) that waits for your approval go ahead'
|
|
200
|
+
: 'end a run (run_...) or drop a proposed version (name@hash) that waits for your approval')
|
|
201
|
+
.addOption(new commander_1.Option('--reason <text>', 'why, shown to who asked').argParser((value) => value))
|
|
202
|
+
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
203
|
+
const proposal = parseProposal(target);
|
|
204
|
+
if (!proposal && !RUN_ID.test(target)) {
|
|
205
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a proposed version, list them with: hammerkit remote approvals or remote proposals`);
|
|
206
|
+
}
|
|
207
|
+
const { name: serverName, server, token } = await connect(name);
|
|
208
|
+
const path = proposal
|
|
209
|
+
? `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}/${action}`
|
|
210
|
+
: `/v1/approvals/${target}/${action}`;
|
|
211
|
+
const { body } = await (0, server_client_1.callServer)(server, token, 'POST', path, options.reason ? { reason: options.reason } : undefined);
|
|
212
|
+
const rejected = body.state === 'rejected';
|
|
213
|
+
environment.stdout.write(proposal
|
|
214
|
+
? rejected
|
|
215
|
+
? `rejected ${target} on ${serverName}\n`
|
|
216
|
+
: `approved ${target} on ${serverName}, it is published\n`
|
|
217
|
+
: rejected
|
|
218
|
+
? `rejected ${target} on ${serverName}\n`
|
|
219
|
+
: `approved ${target} on ${serverName}, it is queued\n`);
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
exports.registerWorkCommands = registerWorkCommands;
|
|
224
|
+
//# sourceMappingURL=remote-work-commands.js.map
|