@trebired/git-host 2.1.0 → 3.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/CHANGELOG.md +25 -0
- package/README.md +327 -6
- package/bin/git-host-actions-runner-darwin-arm64 +0 -0
- package/bin/git-host-actions-runner-darwin-x64 +0 -0
- package/bin/git-host-actions-runner-linux-arm64-gnu +0 -0
- package/bin/git-host-actions-runner-linux-x64-gnu +0 -0
- package/dist/api/forge/actions.d.ts +6 -3
- package/dist/api/forge/actions.d.ts.map +1 -1
- package/dist/api/forge/actions.js +46 -10
- package/dist/api/forge/actions.js.map +1 -1
- package/dist/api/forge/route.d.ts +10 -0
- package/dist/api/forge/route.d.ts.map +1 -1
- package/dist/api/forge/route.js +6 -0
- package/dist/api/forge/route.js.map +1 -1
- package/dist/api/forge/socket.d.ts.map +1 -1
- package/dist/api/forge/socket.js +4 -148
- package/dist/api/forge/socket.js.map +1 -1
- package/dist/api/forge/socket_subscription.d.ts +10 -0
- package/dist/api/forge/socket_subscription.d.ts.map +1 -0
- package/dist/api/forge/socket_subscription.js +202 -0
- package/dist/api/forge/socket_subscription.js.map +1 -0
- package/dist/api/handler/action.d.ts.map +1 -1
- package/dist/api/handler/action.js +24 -18
- package/dist/api/handler/action.js.map +1 -1
- package/dist/api/socket/linguist_connection.d.ts +9 -0
- package/dist/api/socket/linguist_connection.d.ts.map +1 -0
- package/dist/api/socket/linguist_connection.js +167 -0
- package/dist/api/socket/linguist_connection.js.map +1 -0
- package/dist/api/socket/server.d.ts.map +1 -1
- package/dist/api/socket/server.js +4 -119
- package/dist/api/socket/server.js.map +1 -1
- package/dist/core/actions/artifacts.d.ts +21 -0
- package/dist/core/actions/artifacts.d.ts.map +1 -0
- package/dist/core/actions/artifacts.js +121 -0
- package/dist/core/actions/artifacts.js.map +1 -0
- package/dist/core/actions/environment.d.ts +9 -0
- package/dist/core/actions/environment.d.ts.map +1 -0
- package/dist/core/actions/environment.js +64 -0
- package/dist/core/actions/environment.js.map +1 -0
- package/dist/core/actions/expressions.d.ts +15 -0
- package/dist/core/actions/expressions.d.ts.map +1 -0
- package/dist/core/actions/expressions.js +279 -0
- package/dist/core/actions/expressions.js.map +1 -0
- package/dist/core/actions/go_runner.d.ts +6 -0
- package/dist/core/actions/go_runner.d.ts.map +1 -0
- package/dist/core/actions/go_runner.js +106 -0
- package/dist/core/actions/go_runner.js.map +1 -0
- package/dist/core/actions/local_runner.d.ts +33 -0
- package/dist/core/actions/local_runner.d.ts.map +1 -0
- package/dist/core/actions/local_runner.js +173 -0
- package/dist/core/actions/local_runner.js.map +1 -0
- package/dist/core/actions/node_runner.d.ts +4 -0
- package/dist/core/actions/node_runner.d.ts.map +1 -0
- package/dist/core/actions/node_runner.js +176 -0
- package/dist/core/actions/node_runner.js.map +1 -0
- package/dist/core/actions/normalize.d.ts +7 -0
- package/dist/core/actions/normalize.d.ts.map +1 -0
- package/dist/core/actions/normalize.js +401 -0
- package/dist/core/actions/normalize.js.map +1 -0
- package/dist/core/actions/planner.d.ts +13 -0
- package/dist/core/actions/planner.d.ts.map +1 -0
- package/dist/core/actions/planner.js +106 -0
- package/dist/core/actions/planner.js.map +1 -0
- package/dist/core/actions/redaction.d.ts +16 -0
- package/dist/core/actions/redaction.d.ts.map +1 -0
- package/dist/core/actions/redaction.js +43 -0
- package/dist/core/actions/redaction.js.map +1 -0
- package/dist/core/actions/release_assets.d.ts +31 -0
- package/dist/core/actions/release_assets.d.ts.map +1 -0
- package/dist/core/actions/release_assets.js +114 -0
- package/dist/core/actions/release_assets.js.map +1 -0
- package/dist/core/actions/runner/launch.d.ts +5 -0
- package/dist/core/actions/runner/launch.d.ts.map +1 -0
- package/dist/core/actions/runner/launch.js +100 -0
- package/dist/core/actions/runner/launch.js.map +1 -0
- package/dist/core/actions/runner/protocol.d.ts +7 -0
- package/dist/core/actions/runner/protocol.d.ts.map +1 -0
- package/dist/core/actions/runner/protocol.js +66 -0
- package/dist/core/actions/runner/protocol.js.map +1 -0
- package/dist/core/actions/runner.d.ts +6 -0
- package/dist/core/actions/runner.d.ts.map +1 -0
- package/dist/core/actions/runner.js +13 -0
- package/dist/core/actions/runner.js.map +1 -0
- package/dist/core/actions/sandbox.d.ts +4 -0
- package/dist/core/actions/sandbox.d.ts.map +1 -0
- package/dist/core/actions/sandbox.js +46 -0
- package/dist/core/actions/sandbox.js.map +1 -0
- package/dist/core/{actions_runner.d.ts → actions/types.d.ts} +38 -6
- package/dist/core/actions/types.d.ts.map +1 -0
- package/dist/core/actions/types.js +2 -0
- package/dist/core/actions/types.js.map +1 -0
- package/dist/core/{actions_workflows.d.ts → actions/workflows.d.ts} +4 -4
- package/dist/core/actions/workflows.d.ts.map +1 -0
- package/dist/core/{actions_workflows.js → actions/workflows.js} +40 -100
- package/dist/core/actions/workflows.js.map +1 -0
- package/dist/core/actions/workspace.d.ts +7 -0
- package/dist/core/actions/workspace.d.ts.map +1 -0
- package/dist/core/actions/workspace.js +47 -0
- package/dist/core/actions/workspace.js.map +1 -0
- package/dist/core/activity.d.ts.map +1 -1
- package/dist/core/activity.js +57 -39
- package/dist/core/activity.js.map +1 -1
- package/dist/core/archive_cache_filesystem.d.ts.map +1 -1
- package/dist/core/archive_cache_filesystem.js +39 -33
- package/dist/core/archive_cache_filesystem.js.map +1 -1
- package/dist/core/create/git/forge.d.ts.map +1 -1
- package/dist/core/create/git/forge.js +12 -2
- package/dist/core/create/git/forge.js.map +1 -1
- package/dist/core/create_git_host/remote_methods.js.map +1 -1
- package/dist/core/forge_actions.d.ts +10 -2
- package/dist/core/forge_actions.d.ts.map +1 -1
- package/dist/core/forge_actions.js +1025 -160
- package/dist/core/forge_actions.js.map +1 -1
- package/dist/core/git_forge/storage_memory.d.ts.map +1 -1
- package/dist/core/git_forge/storage_memory.js +72 -2
- package/dist/core/git_forge/storage_memory.js.map +1 -1
- package/dist/core/repository/parsers.d.ts.map +1 -1
- package/dist/core/repository/parsers.js +45 -32
- package/dist/core/repository/parsers.js.map +1 -1
- package/dist/core/run_git/process.d.ts.map +1 -1
- package/dist/core/run_git/process.js +19 -18
- package/dist/core/run_git/process.js.map +1 -1
- package/dist/core/working_tree/read.d.ts.map +1 -1
- package/dist/core/working_tree/read.js +26 -32
- package/dist/core/working_tree/read.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/forge/activity.d.ts +52 -0
- package/dist/types/forge/activity.d.ts.map +1 -0
- package/dist/types/forge/activity.js +2 -0
- package/dist/types/forge/activity.js.map +1 -0
- package/dist/types/forge/api.d.ts +116 -0
- package/dist/types/forge/api.d.ts.map +1 -0
- package/dist/types/forge/api.js +2 -0
- package/dist/types/forge/api.js.map +1 -0
- package/dist/types/forge/releases.d.ts +98 -0
- package/dist/types/forge/releases.d.ts.map +1 -0
- package/dist/types/forge/releases.js +2 -0
- package/dist/types/forge/releases.js.map +1 -0
- package/dist/types/forge/social_forks.d.ts +51 -0
- package/dist/types/forge/social_forks.d.ts.map +1 -0
- package/dist/types/forge/social_forks.js +2 -0
- package/dist/types/forge/social_forks.js.map +1 -0
- package/dist/types/forge/workflows.d.ts +361 -0
- package/dist/types/forge/workflows.d.ts.map +1 -0
- package/dist/types/forge/workflows.js +2 -0
- package/dist/types/forge/workflows.js.map +1 -0
- package/dist/types/forge.d.ts +5 -475
- package/dist/types/forge.d.ts.map +1 -1
- package/dist/types/host/archive.d.ts.map +1 -1
- package/dist/types/host/options.d.ts.map +1 -1
- package/dist/types/host/service.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +38 -9
- package/dist/core/actions_runner.d.ts.map +0 -1
- package/dist/core/actions_runner.js +0 -414
- package/dist/core/actions_runner.js.map +0 -1
- package/dist/core/actions_workflows.d.ts.map +0 -1
- package/dist/core/actions_workflows.js.map +0 -1
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { GitHostError } from "../errors.js";
|
|
3
4
|
import { text } from "../utils/text.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { uploadArtifact, downloadArtifact } from "./actions/artifacts.js";
|
|
6
|
+
import { buildStepBaseEnv, runnerNeedsPrivilegeWarning } from "./actions/environment.js";
|
|
7
|
+
import { resolveWorkflowBoolean, resolveWorkflowString } from "./actions/expressions.js";
|
|
8
|
+
import { materializeJobWorkspace, runShellCommand, setupRuntime } from "./actions/local_runner.js";
|
|
9
|
+
import { normalizeEnv } from "./actions/normalize.js";
|
|
10
|
+
import { assertAcyclicWorkflow, planWorkflowJobs, resolveRefName } from "./actions/planner.js";
|
|
11
|
+
import { createRunRedactor } from "./actions/redaction.js";
|
|
12
|
+
import { publishReleaseAsset } from "./actions/release_assets.js";
|
|
13
|
+
import { resolveActionsWorkspaceRoot, resolveReleaseAssetsRoot } from "./actions/workspace.js";
|
|
14
|
+
import { listRepositoryWorkflows, matchesWorkflowTrigger, readRepositoryWorkflow, resolveRepositoryWorkflowRoot } from "./actions/workflows.js";
|
|
6
15
|
import { runGit } from "./run_git.js";
|
|
7
16
|
const ACTIVITY_LISTENER_SYMBOL = Symbol.for("@trebired/git-host/actions-activity-listeners");
|
|
8
17
|
const ACTIVITY_WRAPPED_SYMBOL = Symbol.for("@trebired/git-host/actions-activity-wrapped");
|
|
9
18
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 1000;
|
|
19
|
+
const DEFAULT_LOCAL_RUNNER_LABELS = [
|
|
20
|
+
"bun",
|
|
21
|
+
"linux",
|
|
22
|
+
"local",
|
|
23
|
+
"node",
|
|
24
|
+
"ubuntu",
|
|
25
|
+
"ubuntu-22.04",
|
|
26
|
+
"ubuntu-latest",
|
|
27
|
+
];
|
|
10
28
|
const TERMINAL_RUN_STATUSES = new Set(["cancelled", "failed", "skipped", "success"]);
|
|
29
|
+
const TERMINAL_JOB_STATUSES = new Set(["cancelled", "failed", "skipped", "success"]);
|
|
11
30
|
function nowIso() {
|
|
12
31
|
return new Date().toISOString();
|
|
13
32
|
}
|
|
14
|
-
function toDateSortValue(value) {
|
|
15
|
-
return text(value);
|
|
16
|
-
}
|
|
17
|
-
function matchesWorkflowTrigger(workflow, triggerKind, context) {
|
|
18
|
-
if (!workflow.enabled || text(workflow.trigger) !== text(triggerKind))
|
|
19
|
-
return false;
|
|
20
|
-
const branches = Array.isArray(workflow.source?.branches) ? workflow.source?.branches.map((entry) => text(entry)).filter(Boolean) : [];
|
|
21
|
-
const tags = Array.isArray(workflow.source?.tags) ? workflow.source?.tags.map((entry) => text(entry)).filter(Boolean) : [];
|
|
22
|
-
const branch = text(context.branch);
|
|
23
|
-
const tag = text(context.tag_name, text(context.tag));
|
|
24
|
-
if (branches.length && !branches.includes(branch))
|
|
25
|
-
return false;
|
|
26
|
-
if (tags.length && !tags.includes(tag))
|
|
27
|
-
return false;
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
33
|
function isTerminalRunStatus(status) {
|
|
31
34
|
return TERMINAL_RUN_STATUSES.has(status);
|
|
32
35
|
}
|
|
36
|
+
function isTerminalJobStatus(status) {
|
|
37
|
+
return TERMINAL_JOB_STATUSES.has(status);
|
|
38
|
+
}
|
|
33
39
|
function ensureActionsStorage(storage) {
|
|
34
40
|
if (!storage) {
|
|
35
41
|
throw new GitHostError("forge_actions_not_configured", "Actions storage is required to use repository workflows.");
|
|
@@ -38,22 +44,179 @@ function ensureActionsStorage(storage) {
|
|
|
38
44
|
}
|
|
39
45
|
function normalizeRunner(options) {
|
|
40
46
|
const runner = options?.runner || {};
|
|
47
|
+
const labels = Array.isArray(options?.localRunnerLabels) && options?.localRunnerLabels.length
|
|
48
|
+
? options.localRunnerLabels.map((entry) => text(entry)).filter(Boolean)
|
|
49
|
+
: DEFAULT_LOCAL_RUNNER_LABELS;
|
|
41
50
|
return {
|
|
42
|
-
capabilities: Array.isArray(runner.capabilities)
|
|
51
|
+
capabilities: Array.isArray(runner.capabilities)
|
|
52
|
+
? runner.capabilities.map((entry) => text(entry)).filter(Boolean)
|
|
53
|
+
: ["artifacts", "env", "expressions", "matrix", "needs", "secrets", "socket-events", "snapshot", "uses"],
|
|
43
54
|
host: text(runner.host, "local-host"),
|
|
44
|
-
id: text(runner.id, "
|
|
45
|
-
kind: text(runner.kind, "
|
|
55
|
+
id: text(runner.id, "local-runner"),
|
|
56
|
+
kind: text(runner.kind, "local"),
|
|
57
|
+
labels,
|
|
46
58
|
platform_version: text(runner.platform_version, "@trebired/git-host"),
|
|
47
59
|
};
|
|
48
60
|
}
|
|
61
|
+
function aggregateJobStatus(statuses) {
|
|
62
|
+
if (statuses.some((status) => status === "failed"))
|
|
63
|
+
return "failed";
|
|
64
|
+
if (statuses.some((status) => status === "cancelled"))
|
|
65
|
+
return "cancelled";
|
|
66
|
+
if (statuses.every((status) => status === "skipped"))
|
|
67
|
+
return "skipped";
|
|
68
|
+
if (statuses.every((status) => status === "success"))
|
|
69
|
+
return "success";
|
|
70
|
+
return "queued";
|
|
71
|
+
}
|
|
72
|
+
function normalizeTriggerContext(input) {
|
|
73
|
+
return input && typeof input === "object" ? { ...input } : {};
|
|
74
|
+
}
|
|
75
|
+
function resolveGithubRef(run) {
|
|
76
|
+
if (text(run.ref).startsWith("refs/"))
|
|
77
|
+
return text(run.ref);
|
|
78
|
+
if (run.branch)
|
|
79
|
+
return `refs/heads/${run.branch}`;
|
|
80
|
+
return text(run.ref, "HEAD");
|
|
81
|
+
}
|
|
82
|
+
function buildExpressionContext(input) {
|
|
83
|
+
const githubRef = resolveGithubRef(input.run);
|
|
84
|
+
const eventName = input.run.trigger_kind === "manual" ? "workflow_dispatch" : input.run.trigger_kind;
|
|
85
|
+
return {
|
|
86
|
+
env: {
|
|
87
|
+
...(input.execution.env || {}),
|
|
88
|
+
...(input.extraEnv || {}),
|
|
89
|
+
},
|
|
90
|
+
github: {
|
|
91
|
+
actor: text(input.execution.actor?.id, input.run.created_by),
|
|
92
|
+
event: {
|
|
93
|
+
...input.triggerContext,
|
|
94
|
+
inputs: input.execution.inputs,
|
|
95
|
+
},
|
|
96
|
+
event_name: eventName,
|
|
97
|
+
ref: githubRef,
|
|
98
|
+
ref_name: resolveRefName(githubRef),
|
|
99
|
+
repository: input.run.repository_id,
|
|
100
|
+
run_id: input.run.id,
|
|
101
|
+
sha: input.run.commit_hash,
|
|
102
|
+
workflow: input.workflow.name,
|
|
103
|
+
workflow_ref: input.run.workflow_id,
|
|
104
|
+
},
|
|
105
|
+
job: {
|
|
106
|
+
status: "queued",
|
|
107
|
+
},
|
|
108
|
+
...(input.matrix ? { matrix: input.matrix } : {}),
|
|
109
|
+
...(input.needs ? { needs: input.needs } : {}),
|
|
110
|
+
secrets: input.execution.secrets,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function resolveEnvLayer(layer, context) {
|
|
114
|
+
if (!layer)
|
|
115
|
+
return {};
|
|
116
|
+
const next = {};
|
|
117
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
118
|
+
const envContext = {
|
|
119
|
+
...context,
|
|
120
|
+
env: {
|
|
121
|
+
...(context.env || {}),
|
|
122
|
+
...next,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
next[key] = resolveWorkflowString(value, envContext);
|
|
126
|
+
}
|
|
127
|
+
return next;
|
|
128
|
+
}
|
|
129
|
+
function mergeRuntimeEnv(input) {
|
|
130
|
+
let env = {
|
|
131
|
+
...buildStepBaseEnv(input.actions?.environment),
|
|
132
|
+
...(input.actions?.env || {}),
|
|
133
|
+
...(input.execution.env || {}),
|
|
134
|
+
};
|
|
135
|
+
let context = buildExpressionContext({
|
|
136
|
+
execution: input.execution,
|
|
137
|
+
extraEnv: env,
|
|
138
|
+
...(input.matrix ? { matrix: input.matrix } : {}),
|
|
139
|
+
run: input.run,
|
|
140
|
+
triggerContext: input.triggerContext,
|
|
141
|
+
workflow: input.workflow,
|
|
142
|
+
});
|
|
143
|
+
env = {
|
|
144
|
+
...env,
|
|
145
|
+
...resolveEnvLayer(input.workflow.env, context),
|
|
146
|
+
};
|
|
147
|
+
context = {
|
|
148
|
+
...context,
|
|
149
|
+
env,
|
|
150
|
+
};
|
|
151
|
+
env = {
|
|
152
|
+
...env,
|
|
153
|
+
...resolveEnvLayer(input.jobEnv, context),
|
|
154
|
+
};
|
|
155
|
+
context = {
|
|
156
|
+
...context,
|
|
157
|
+
env,
|
|
158
|
+
};
|
|
159
|
+
env = {
|
|
160
|
+
...env,
|
|
161
|
+
...resolveEnvLayer(input.stepEnv, context),
|
|
162
|
+
...(input.execution.secrets || {}),
|
|
163
|
+
};
|
|
164
|
+
return env;
|
|
165
|
+
}
|
|
166
|
+
function validateDispatchInputs(workflow, provided) {
|
|
167
|
+
const inputs = workflow.on?.workflow_dispatch?.inputs || [];
|
|
168
|
+
const next = {};
|
|
169
|
+
for (const input of inputs) {
|
|
170
|
+
const value = provided?.[input.name];
|
|
171
|
+
if (value === undefined || value === null || value === "") {
|
|
172
|
+
if (input.default !== undefined) {
|
|
173
|
+
next[input.name] = input.default;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (input.required) {
|
|
177
|
+
throw new GitHostError("forge_invalid_workflow_definition", `Manual workflow input "${input.name}" is required.`, {
|
|
178
|
+
input: input.name,
|
|
179
|
+
workflowId: workflow.id,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (input.type === "boolean") {
|
|
185
|
+
if (typeof value === "boolean") {
|
|
186
|
+
next[input.name] = value;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (value === "true" || value === "false") {
|
|
190
|
+
next[input.name] = value === "true";
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
throw new GitHostError("forge_invalid_workflow_definition", `Manual workflow input "${input.name}" must be a boolean.`, {
|
|
194
|
+
input: input.name,
|
|
195
|
+
value,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
next[input.name] = String(value);
|
|
199
|
+
}
|
|
200
|
+
return next;
|
|
201
|
+
}
|
|
49
202
|
function createGitForgeActionsRuntime(options) {
|
|
50
203
|
const storage = ensureActionsStorage(options.storage);
|
|
51
204
|
const runListeners = new Map();
|
|
52
205
|
const runSequences = new Map();
|
|
206
|
+
const runExecutionContexts = new Map();
|
|
53
207
|
const queuedRuns = [];
|
|
54
208
|
const activeRuns = new Map();
|
|
55
209
|
const runner = normalizeRunner(options.actions);
|
|
56
210
|
let processing = false;
|
|
211
|
+
if (options.actions?.environment?.inheritProcessEnv) {
|
|
212
|
+
console.warn("[git-host] actions.environment.inheritProcessEnv is enabled: the entire host process environment is exposed to every workflow step and is not redacted. Only enable this for fully trusted workflows.");
|
|
213
|
+
}
|
|
214
|
+
if (runnerNeedsPrivilegeWarning({
|
|
215
|
+
localRunner: options.actions?.localRunner,
|
|
216
|
+
uid: typeof process.getuid === "function" ? process.getuid() : null,
|
|
217
|
+
})) {
|
|
218
|
+
console.warn("[git-host] the local Actions runner is executing as root with no actions.localRunner.uid drop or beforeSpawn sandbox: workflow steps run as root on the host. Configure a uid/gid drop or a sandbox (see createBubblewrapSandbox), or only run trusted workflows.");
|
|
219
|
+
}
|
|
57
220
|
async function nextRunSequence(runId) {
|
|
58
221
|
const current = runSequences.get(runId);
|
|
59
222
|
if (Number.isFinite(current)) {
|
|
@@ -133,7 +296,17 @@ function createGitForgeActionsRuntime(options) {
|
|
|
133
296
|
}
|
|
134
297
|
return run;
|
|
135
298
|
}
|
|
136
|
-
async function
|
|
299
|
+
async function updateJob(runId, jobRunId, input) {
|
|
300
|
+
const job = await storage.updateWorkflowRunJob(runId, jobRunId, input);
|
|
301
|
+
if (!job) {
|
|
302
|
+
throw new GitHostError("forge_resource_not_found", `Workflow job run "${jobRunId}" was not found.`, {
|
|
303
|
+
jobRunId,
|
|
304
|
+
runId,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return job;
|
|
308
|
+
}
|
|
309
|
+
async function updateStep(runId, stepId, input) {
|
|
137
310
|
const step = await storage.updateWorkflowRunStep(runId, stepId, input);
|
|
138
311
|
if (!step) {
|
|
139
312
|
throw new GitHostError("forge_resource_not_found", `Workflow run step "${stepId}" was not found.`, {
|
|
@@ -143,27 +316,76 @@ function createGitForgeActionsRuntime(options) {
|
|
|
143
316
|
}
|
|
144
317
|
return step;
|
|
145
318
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
319
|
+
async function readWorkflowAtRef(repositoryId, repositoryPath, ref, workflowId) {
|
|
320
|
+
const workflowRoot = await resolveRepositoryWorkflowRoot(options.actions, repositoryId);
|
|
321
|
+
return await readRepositoryWorkflow({
|
|
322
|
+
ref,
|
|
323
|
+
repositoryId,
|
|
324
|
+
repositoryPath,
|
|
325
|
+
workflowId,
|
|
326
|
+
workflowRoot,
|
|
151
327
|
});
|
|
152
328
|
}
|
|
153
|
-
async function
|
|
154
|
-
const
|
|
329
|
+
async function resolveExecutionContext(repositoryId, workflow, input) {
|
|
330
|
+
const resolved = options.actions?.resolveExecutionContext
|
|
331
|
+
? await options.actions.resolveExecutionContext({
|
|
332
|
+
actor: input.actor,
|
|
333
|
+
repositoryId,
|
|
334
|
+
runInput: input,
|
|
335
|
+
triggerContext: input.triggerContext,
|
|
336
|
+
triggerKind: input.triggerKind,
|
|
337
|
+
workflow,
|
|
338
|
+
})
|
|
339
|
+
: null;
|
|
340
|
+
const manualExecution = input.executionContext || {};
|
|
341
|
+
return {
|
|
342
|
+
...(resolved?.actor || manualExecution.actor ? { actor: { ...(resolved?.actor || {}), ...(manualExecution.actor || {}) } } : {}),
|
|
343
|
+
env: normalizeEnv({
|
|
344
|
+
...(resolved?.env || {}),
|
|
345
|
+
...(manualExecution.env || {}),
|
|
346
|
+
...(input.env || {}),
|
|
347
|
+
}) || {},
|
|
348
|
+
inputs: validateDispatchInputs(workflow, input.inputs),
|
|
349
|
+
...(resolved?.metadata || manualExecution.metadata ? { metadata: { ...(resolved?.metadata || {}), ...(manualExecution.metadata || {}) } } : {}),
|
|
350
|
+
secrets: normalizeEnv({
|
|
351
|
+
...(resolved?.secrets || {}),
|
|
352
|
+
...(manualExecution.secrets || {}),
|
|
353
|
+
...(input.secrets || {}),
|
|
354
|
+
}) || {},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function markQueuedStepsForJob(runId, jobRunId, status) {
|
|
358
|
+
const steps = await storage.listWorkflowRunSteps(runId, {
|
|
359
|
+
jobRunId,
|
|
360
|
+
});
|
|
155
361
|
const now = nowIso();
|
|
156
362
|
await Promise.all(steps
|
|
157
|
-
.filter((step) => step.
|
|
363
|
+
.filter((step) => step.status === "queued")
|
|
158
364
|
.map((step) => storage.updateWorkflowRunStep(runId, step.id, {
|
|
159
365
|
finished_at: now,
|
|
160
366
|
status,
|
|
161
367
|
})));
|
|
162
368
|
}
|
|
369
|
+
async function markQueuedJobsAndSteps(runId, status) {
|
|
370
|
+
const jobs = await storage.listWorkflowRunJobs(runId);
|
|
371
|
+
const now = nowIso();
|
|
372
|
+
await Promise.all(jobs.map(async (job) => {
|
|
373
|
+
if (job.status === "queued") {
|
|
374
|
+
await storage.updateWorkflowRunJob(runId, job.id, {
|
|
375
|
+
finished_at: now,
|
|
376
|
+
status,
|
|
377
|
+
summary: status === "cancelled" ? "Cancelled before starting." : "Skipped before starting.",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
await markQueuedStepsForJob(runId, job.id, status === "cancelled" ? "cancelled" : "skipped");
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
163
383
|
async function finalizeRun(run, input) {
|
|
164
384
|
const finished = await updateRun(run.repository_id, run.id, {
|
|
165
|
-
|
|
166
|
-
|
|
385
|
+
current_job: null,
|
|
386
|
+
current_job_id: null,
|
|
387
|
+
current_step: null,
|
|
388
|
+
current_step_index: null,
|
|
167
389
|
finished_at: nowIso(),
|
|
168
390
|
status: input.status,
|
|
169
391
|
summary: input.summary,
|
|
@@ -175,59 +397,141 @@ function createGitForgeActionsRuntime(options) {
|
|
|
175
397
|
});
|
|
176
398
|
return finished;
|
|
177
399
|
}
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
400
|
+
async function resolveConcurrencyGroup(repositoryId, workflow, input, runTarget, execution) {
|
|
401
|
+
if (!workflow.concurrency?.group)
|
|
402
|
+
return null;
|
|
403
|
+
const temporaryRun = {
|
|
404
|
+
branch: runTarget.branch,
|
|
405
|
+
commit_hash: runTarget.commitHash,
|
|
406
|
+
created_at: nowIso(),
|
|
407
|
+
created_by: text(input.actor.id, "system"),
|
|
408
|
+
current_step: null,
|
|
409
|
+
current_step_index: null,
|
|
410
|
+
finished_at: null,
|
|
411
|
+
id: "pending",
|
|
412
|
+
ref: runTarget.ref,
|
|
413
|
+
repository_id: repositoryId,
|
|
414
|
+
runner: null,
|
|
415
|
+
started_at: null,
|
|
416
|
+
status: "queued",
|
|
417
|
+
summary: "",
|
|
418
|
+
trigger_context: input.triggerContext,
|
|
419
|
+
trigger_kind: input.triggerKind,
|
|
420
|
+
workflow_id: workflow.id,
|
|
421
|
+
};
|
|
422
|
+
const context = buildExpressionContext({
|
|
423
|
+
execution,
|
|
424
|
+
run: temporaryRun,
|
|
425
|
+
triggerContext: input.triggerContext,
|
|
426
|
+
workflow,
|
|
427
|
+
});
|
|
428
|
+
return resolveWorkflowString(workflow.concurrency.group, context);
|
|
429
|
+
}
|
|
430
|
+
async function cancelConflictingRuns(repositoryId, runId, concurrencyGroup, actor) {
|
|
431
|
+
const active = await storage.listWorkflowRuns(repositoryId, {
|
|
432
|
+
status: ["queued", "running", "starting"],
|
|
186
433
|
});
|
|
434
|
+
for (const entry of active) {
|
|
435
|
+
if (entry.id === runId)
|
|
436
|
+
continue;
|
|
437
|
+
if (text(entry.concurrency_group) !== text(concurrencyGroup))
|
|
438
|
+
continue;
|
|
439
|
+
await runtime.cancelWorkflowRun(repositoryId, entry.id, actor);
|
|
440
|
+
}
|
|
187
441
|
}
|
|
188
442
|
async function queueWorkflowRun(repositoryId, workflow, input) {
|
|
443
|
+
assertAcyclicWorkflow(workflow);
|
|
444
|
+
const triggerContext = normalizeTriggerContext(input.triggerContext);
|
|
189
445
|
const target = await resolveRunTarget(repositoryId, input);
|
|
190
446
|
const createdAt = nowIso();
|
|
191
447
|
const createdBy = text(input.actor.id, text(input.actor.name, "system"));
|
|
448
|
+
const execution = await resolveExecutionContext(repositoryId, workflow, {
|
|
449
|
+
...input,
|
|
450
|
+
triggerContext,
|
|
451
|
+
});
|
|
452
|
+
const concurrencyGroup = await resolveConcurrencyGroup(repositoryId, workflow, {
|
|
453
|
+
...input,
|
|
454
|
+
triggerContext,
|
|
455
|
+
}, target, execution);
|
|
192
456
|
const run = await storage.createWorkflowRun({
|
|
193
457
|
branch: target.branch,
|
|
194
458
|
commit_hash: target.commitHash,
|
|
459
|
+
...(workflow.concurrency ? { concurrency_cancel_in_progress: workflow.concurrency.cancel_in_progress === true } : {}),
|
|
460
|
+
...(concurrencyGroup ? { concurrency_group: concurrencyGroup } : {}),
|
|
195
461
|
created_at: createdAt,
|
|
196
462
|
created_by: createdBy,
|
|
463
|
+
current_job: null,
|
|
464
|
+
current_job_id: null,
|
|
197
465
|
current_step: null,
|
|
198
466
|
current_step_index: null,
|
|
467
|
+
execution_context: {
|
|
468
|
+
...(execution.actor ? { actor: execution.actor } : {}),
|
|
469
|
+
env: execution.env,
|
|
470
|
+
inputs: execution.inputs,
|
|
471
|
+
...(execution.metadata ? { metadata: execution.metadata } : {}),
|
|
472
|
+
secret_names: Object.keys(execution.secrets),
|
|
473
|
+
},
|
|
199
474
|
finished_at: null,
|
|
200
475
|
id: randomUUID(),
|
|
201
476
|
ref: target.ref,
|
|
202
|
-
release_id:
|
|
477
|
+
release_id: text(triggerContext.release_id) || null,
|
|
203
478
|
repository_id: repositoryId,
|
|
204
479
|
runner: null,
|
|
205
480
|
started_at: null,
|
|
206
481
|
status: "queued",
|
|
207
482
|
summary: "Workflow run queued.",
|
|
208
483
|
trigger_context: {
|
|
209
|
-
...
|
|
484
|
+
...triggerContext,
|
|
485
|
+
event_name: input.triggerKind === "manual" ? "workflow_dispatch" : input.triggerKind,
|
|
486
|
+
inputs: execution.inputs,
|
|
210
487
|
workflow_definition_path: workflow.definition_path,
|
|
211
488
|
},
|
|
212
489
|
trigger_kind: input.triggerKind,
|
|
213
490
|
workflow_id: workflow.id,
|
|
214
491
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
492
|
+
const plannedJobs = planWorkflowJobs(workflow);
|
|
493
|
+
for (const plannedJob of plannedJobs) {
|
|
494
|
+
const jobRun = await storage.createWorkflowRunJob({
|
|
495
|
+
current_step: null,
|
|
496
|
+
current_step_index: null,
|
|
219
497
|
finished_at: null,
|
|
220
498
|
id: randomUUID(),
|
|
221
|
-
index,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
name:
|
|
225
|
-
|
|
499
|
+
index: plannedJob.index,
|
|
500
|
+
job_id: plannedJob.job.id,
|
|
501
|
+
...(plannedJob.matrix ? { matrix: plannedJob.matrix } : {}),
|
|
502
|
+
name: plannedJob.name,
|
|
503
|
+
needs: plannedJob.job.needs,
|
|
226
504
|
run_id: run.id,
|
|
505
|
+
runner: null,
|
|
506
|
+
runs_on: plannedJob.job.runs_on,
|
|
227
507
|
started_at: null,
|
|
228
508
|
status: "queued",
|
|
509
|
+
summary: "Job queued.",
|
|
229
510
|
});
|
|
230
|
-
|
|
511
|
+
for (const [index, step] of plannedJob.job.steps.entries()) {
|
|
512
|
+
await storage.createWorkflowRunStep({
|
|
513
|
+
command: text(step.run, step.uses),
|
|
514
|
+
exit_code: null,
|
|
515
|
+
finished_at: null,
|
|
516
|
+
id: randomUUID(),
|
|
517
|
+
index,
|
|
518
|
+
job_run_id: jobRun.id,
|
|
519
|
+
kind: step.kind || (step.uses ? "uses" : "shell"),
|
|
520
|
+
metadata: {
|
|
521
|
+
env: step.env,
|
|
522
|
+
if: step.if,
|
|
523
|
+
shell: step.shell,
|
|
524
|
+
with: step.with,
|
|
525
|
+
},
|
|
526
|
+
name: text(step.name, `Step ${index + 1}`),
|
|
527
|
+
output_preview: "",
|
|
528
|
+
run_id: run.id,
|
|
529
|
+
started_at: null,
|
|
530
|
+
status: "queued",
|
|
531
|
+
uses: text(step.uses) || null,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
231
535
|
await emitRunEvent(run, {
|
|
232
536
|
metadata: {
|
|
233
537
|
branch: run.branch,
|
|
@@ -238,6 +542,10 @@ function createGitForgeActionsRuntime(options) {
|
|
|
238
542
|
summary: run.summary,
|
|
239
543
|
type: "run.accepted",
|
|
240
544
|
});
|
|
545
|
+
runExecutionContexts.set(run.id, execution);
|
|
546
|
+
if (run.concurrency_group && workflow.concurrency?.cancel_in_progress) {
|
|
547
|
+
await cancelConflictingRuns(repositoryId, run.id, run.concurrency_group, input.actor);
|
|
548
|
+
}
|
|
241
549
|
queuedRuns.push({
|
|
242
550
|
repositoryId,
|
|
243
551
|
runId: run.id,
|
|
@@ -245,95 +553,595 @@ function createGitForgeActionsRuntime(options) {
|
|
|
245
553
|
scheduleQueueProcessing();
|
|
246
554
|
return run;
|
|
247
555
|
}
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
556
|
+
function scheduleQueueProcessing() {
|
|
557
|
+
if (processing)
|
|
558
|
+
return;
|
|
559
|
+
queueMicrotask(() => {
|
|
560
|
+
void processQueue();
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function supportedRunnerLabels() {
|
|
564
|
+
return new Set(runner.labels || []);
|
|
565
|
+
}
|
|
566
|
+
function assertJobRunnerSupported(jobRun) {
|
|
567
|
+
const labels = supportedRunnerLabels();
|
|
568
|
+
const unsupported = jobRun.runs_on.filter((entry) => !labels.has(text(entry)));
|
|
569
|
+
if (unsupported.length) {
|
|
570
|
+
throw new GitHostError("forge_actions_runner_failed", `Job "${jobRun.name}" requested unsupported runner labels: ${unsupported.join(", ")}.`, {
|
|
571
|
+
jobRunId: jobRun.id,
|
|
572
|
+
labels: unsupported,
|
|
253
573
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function emitJobStarted(run, jobRun) {
|
|
577
|
+
await emitRunEvent(run, {
|
|
578
|
+
job_id: jobRun.job_id,
|
|
579
|
+
job_name: jobRun.name,
|
|
580
|
+
job_run_id: jobRun.id,
|
|
581
|
+
status: "running",
|
|
582
|
+
summary: `Running job ${jobRun.name}.`,
|
|
583
|
+
type: "job.started",
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
async function emitJobFinished(run, jobRun) {
|
|
587
|
+
await emitRunEvent(run, {
|
|
588
|
+
job_id: jobRun.job_id,
|
|
589
|
+
job_name: jobRun.name,
|
|
590
|
+
job_run_id: jobRun.id,
|
|
591
|
+
status: jobRun.status,
|
|
592
|
+
summary: jobRun.summary,
|
|
593
|
+
type: "job.finished",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async function finishStep(run, jobRun, stepRun, input) {
|
|
597
|
+
const finished = await updateStep(run.id, stepRun.id, {
|
|
598
|
+
exit_code: input.exitCode === undefined ? stepRun.exit_code : input.exitCode,
|
|
599
|
+
finished_at: nowIso(),
|
|
600
|
+
output_preview: text(input.outputPreview, stepRun.output_preview),
|
|
601
|
+
status: input.status,
|
|
602
|
+
});
|
|
603
|
+
await emitRunEvent(run, {
|
|
604
|
+
command: finished.command,
|
|
605
|
+
job_id: jobRun.job_id,
|
|
606
|
+
job_name: jobRun.name,
|
|
607
|
+
job_run_id: jobRun.id,
|
|
608
|
+
metadata: {
|
|
609
|
+
exit_code: finished.exit_code,
|
|
610
|
+
},
|
|
611
|
+
status: finished.status,
|
|
612
|
+
step_id: finished.id,
|
|
613
|
+
step_index: finished.index,
|
|
614
|
+
step_name: finished.name,
|
|
615
|
+
summary: input.summary,
|
|
616
|
+
type: "step.finished",
|
|
617
|
+
});
|
|
618
|
+
return finished;
|
|
619
|
+
}
|
|
620
|
+
async function skipJob(run, jobRun, summary) {
|
|
621
|
+
await markQueuedStepsForJob(run.id, jobRun.id, "skipped");
|
|
622
|
+
const finished = await updateJob(run.id, jobRun.id, {
|
|
623
|
+
finished_at: nowIso(),
|
|
624
|
+
status: "skipped",
|
|
625
|
+
summary,
|
|
626
|
+
});
|
|
627
|
+
await emitJobFinished(run, finished);
|
|
628
|
+
return finished;
|
|
629
|
+
}
|
|
630
|
+
async function cancelJob(run, jobRun, summary) {
|
|
631
|
+
await markQueuedStepsForJob(run.id, jobRun.id, "cancelled");
|
|
632
|
+
const finished = await updateJob(run.id, jobRun.id, {
|
|
633
|
+
finished_at: nowIso(),
|
|
634
|
+
status: "cancelled",
|
|
635
|
+
summary,
|
|
636
|
+
});
|
|
637
|
+
await emitJobFinished(run, finished);
|
|
638
|
+
return finished;
|
|
639
|
+
}
|
|
640
|
+
async function executePublishReleaseAssetStep(input) {
|
|
641
|
+
const assetName = text(input.stepWith.name);
|
|
642
|
+
const pathSpec = text(input.stepWith.path);
|
|
643
|
+
const format = text(input.stepWith.format, "tar.gz") === "zip" ? "zip" : "tar.gz";
|
|
644
|
+
if (!assetName || !pathSpec) {
|
|
645
|
+
throw new GitHostError("forge_actions_runner_failed", "actions/publish-release-asset requires with.name and with.path.", {
|
|
646
|
+
uses: input.stepRun.uses,
|
|
258
647
|
});
|
|
259
|
-
return updatedRun;
|
|
260
648
|
}
|
|
261
|
-
const
|
|
262
|
-
if (!
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
649
|
+
const tagName = text(input.stepWith.tag, resolveRefName(resolveGithubRef(input.run)));
|
|
650
|
+
if (!tagName) {
|
|
651
|
+
throw new GitHostError("forge_actions_runner_failed", "actions/publish-release-asset could not resolve a tag name; pass with.tag explicitly.", {
|
|
652
|
+
uses: input.stepRun.uses,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
const { asset, release } = await publishReleaseAsset({
|
|
656
|
+
assetName,
|
|
657
|
+
format,
|
|
658
|
+
releaseAssetsRoot: resolveReleaseAssetsRoot(options.actions),
|
|
659
|
+
releaseId: text(input.run.release_id) || undefined,
|
|
660
|
+
releases: options.releases,
|
|
661
|
+
repositoryId: input.run.repository_id,
|
|
662
|
+
sourcePath: path.resolve(input.workspacePath, pathSpec),
|
|
663
|
+
tagName,
|
|
664
|
+
});
|
|
665
|
+
await emitRunEvent(input.run, {
|
|
666
|
+
job_id: input.jobRun.job_id,
|
|
667
|
+
job_name: input.jobRun.name,
|
|
668
|
+
job_run_id: input.jobRun.id,
|
|
669
|
+
metadata: {
|
|
670
|
+
asset_id: asset.id,
|
|
671
|
+
asset_name: asset.name,
|
|
672
|
+
release_id: release.id,
|
|
673
|
+
size: asset.size,
|
|
674
|
+
tag_name: release.tag_name,
|
|
675
|
+
},
|
|
676
|
+
status: "success",
|
|
677
|
+
step_id: input.stepRun.id,
|
|
678
|
+
step_index: input.stepRun.index,
|
|
679
|
+
step_name: input.stepRun.name,
|
|
680
|
+
summary: `Published release asset ${asset.name}.`,
|
|
681
|
+
type: "release_asset.published",
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
outputPreview: `Published release asset ${asset.name} to release ${release.tag_name}.\n`,
|
|
685
|
+
summary: `Published release asset ${asset.name}.`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async function executeUsesStep(input) {
|
|
689
|
+
const uses = text(input.stepRun.uses);
|
|
690
|
+
if (uses === "actions/checkout" || uses === "actions/checkout@v4") {
|
|
691
|
+
const targetRef = text(input.stepWith.ref);
|
|
692
|
+
const targetPath = text(input.stepWith.path, ".");
|
|
693
|
+
if (targetRef && targetRef !== input.run.ref && targetRef !== resolveGithubRef(input.run)) {
|
|
694
|
+
throw new GitHostError("forge_actions_runner_failed", "actions/checkout only supports the workflow snapshot ref in v1.", {
|
|
695
|
+
requestedRef: targetRef,
|
|
696
|
+
runRef: input.run.ref,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
if (targetPath && targetPath !== ".") {
|
|
700
|
+
throw new GitHostError("forge_actions_runner_failed", "actions/checkout path overrides are not supported in v1.", {
|
|
701
|
+
path: targetPath,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
outputPreview: "Checked out workflow snapshot.\n",
|
|
706
|
+
summary: "Checked out workflow snapshot.",
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (uses === "actions/setup-node" || uses === "actions/setup-node@v4") {
|
|
710
|
+
const version = await setupRuntime("node", input.workspacePath);
|
|
711
|
+
return {
|
|
712
|
+
outputPreview: text(version.stdout, version.stderr),
|
|
713
|
+
summary: "Node runtime is available.",
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (uses === "oven-sh/setup-bun" || uses === "oven-sh/setup-bun@v2") {
|
|
717
|
+
const version = await setupRuntime("bun", input.workspacePath);
|
|
718
|
+
return {
|
|
719
|
+
outputPreview: text(version.stdout, version.stderr),
|
|
720
|
+
summary: "Bun runtime is available.",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (uses === "actions/upload-artifact" || uses === "actions/upload-artifact@v4") {
|
|
724
|
+
const artifactName = text(input.stepWith.name);
|
|
725
|
+
const pathSpec = text(input.stepWith.path);
|
|
726
|
+
if (!artifactName || !pathSpec) {
|
|
727
|
+
throw new GitHostError("forge_actions_runner_failed", "actions/upload-artifact requires with.name and with.path.", {
|
|
728
|
+
uses,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const stored = uploadArtifact({
|
|
732
|
+
artifactName,
|
|
733
|
+
artifactsRoot: input.artifactsRoot,
|
|
734
|
+
pathSpec,
|
|
735
|
+
workspacePath: input.workspacePath,
|
|
736
|
+
});
|
|
737
|
+
const artifact = await storage.createWorkflowRunArtifact({
|
|
738
|
+
created_at: nowIso(),
|
|
739
|
+
file_count: stored.fileCount,
|
|
740
|
+
id: randomUUID(),
|
|
741
|
+
job_run_id: input.jobRun.id,
|
|
742
|
+
name: artifactName,
|
|
743
|
+
path: stored.path,
|
|
744
|
+
repository_id: input.run.repository_id,
|
|
745
|
+
run_id: input.run.id,
|
|
746
|
+
size: stored.size,
|
|
747
|
+
step_id: input.stepRun.id,
|
|
748
|
+
});
|
|
749
|
+
await emitRunEvent(input.run, {
|
|
750
|
+
artifact_id: artifact.id,
|
|
751
|
+
artifact_name: artifact.name,
|
|
752
|
+
job_id: input.jobRun.job_id,
|
|
753
|
+
job_name: input.jobRun.name,
|
|
754
|
+
job_run_id: input.jobRun.id,
|
|
755
|
+
metadata: {
|
|
756
|
+
file_count: artifact.file_count,
|
|
757
|
+
size: artifact.size,
|
|
758
|
+
},
|
|
759
|
+
status: "success",
|
|
760
|
+
step_id: input.stepRun.id,
|
|
761
|
+
step_index: input.stepRun.index,
|
|
762
|
+
step_name: input.stepRun.name,
|
|
763
|
+
summary: `Uploaded artifact ${artifact.name}.`,
|
|
764
|
+
type: "artifact.uploaded",
|
|
765
|
+
});
|
|
766
|
+
return {
|
|
767
|
+
outputPreview: `Uploaded artifact ${artifactName} (${stored.fileCount} files).\n`,
|
|
768
|
+
summary: `Uploaded artifact ${artifactName}.`,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
if (uses === "actions/download-artifact" || uses === "actions/download-artifact@v4") {
|
|
772
|
+
const artifactName = text(input.stepWith.name);
|
|
773
|
+
if (!artifactName) {
|
|
774
|
+
throw new GitHostError("forge_actions_runner_failed", "actions/download-artifact requires with.name.", {
|
|
775
|
+
uses,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const artifacts = await storage.listWorkflowRunArtifacts(input.run.id, {
|
|
779
|
+
name: artifactName,
|
|
780
|
+
});
|
|
781
|
+
const artifact = artifacts[0];
|
|
782
|
+
if (!artifact) {
|
|
783
|
+
throw new GitHostError("forge_actions_runner_failed", `Artifact "${artifactName}" was not found for this run.`, {
|
|
784
|
+
artifactName,
|
|
785
|
+
runId: input.run.id,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
downloadArtifact({
|
|
789
|
+
artifact,
|
|
790
|
+
artifactsRoot: input.artifactsRoot,
|
|
791
|
+
destinationPath: text(input.stepWith.path, "."),
|
|
792
|
+
workspacePath: input.workspacePath,
|
|
793
|
+
});
|
|
794
|
+
await emitRunEvent(input.run, {
|
|
795
|
+
artifact_id: artifact.id,
|
|
796
|
+
artifact_name: artifact.name,
|
|
797
|
+
job_id: input.jobRun.job_id,
|
|
798
|
+
job_name: input.jobRun.name,
|
|
799
|
+
job_run_id: input.jobRun.id,
|
|
800
|
+
status: "success",
|
|
801
|
+
step_id: input.stepRun.id,
|
|
802
|
+
step_index: input.stepRun.index,
|
|
803
|
+
step_name: input.stepRun.name,
|
|
804
|
+
summary: `Downloaded artifact ${artifact.name}.`,
|
|
805
|
+
type: "artifact.downloaded",
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
outputPreview: `Downloaded artifact ${artifactName}.\n`,
|
|
809
|
+
summary: `Downloaded artifact ${artifactName}.`,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (uses === "actions/publish-release-asset" || uses === "actions/publish-release-asset@v1") {
|
|
813
|
+
return await executePublishReleaseAssetStep(input);
|
|
814
|
+
}
|
|
815
|
+
throw new GitHostError("forge_actions_runner_failed", `Unsupported action "${uses}".`, {
|
|
816
|
+
uses,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
async function executeJob(input) {
|
|
820
|
+
const workflowJob = input.workflow.jobs.find((entry) => entry.id === input.jobRun.job_id);
|
|
821
|
+
if (!workflowJob) {
|
|
822
|
+
throw new GitHostError("forge_invalid_workflow_definition", `Workflow job "${input.jobRun.job_id}" no longer exists.`, {
|
|
823
|
+
jobId: input.jobRun.job_id,
|
|
824
|
+
workflowId: input.workflow.id,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
const baseContext = buildExpressionContext({
|
|
828
|
+
execution: input.execution,
|
|
829
|
+
...(input.jobRun.matrix ? { matrix: input.jobRun.matrix } : {}),
|
|
830
|
+
needs: input.needs,
|
|
831
|
+
run: input.run,
|
|
832
|
+
triggerContext: normalizeTriggerContext(input.run.trigger_context),
|
|
833
|
+
workflow: input.workflow,
|
|
834
|
+
});
|
|
835
|
+
const dependencyStatus = Object.values(input.needs).map((entry) => entry.result);
|
|
836
|
+
if (!workflowJob.if && dependencyStatus.some((status) => status !== "success")) {
|
|
837
|
+
return await skipJob(input.run, input.jobRun, "Skipped because a dependency did not complete successfully.");
|
|
838
|
+
}
|
|
839
|
+
if (workflowJob.if && !resolveWorkflowBoolean(workflowJob.if, baseContext, true)) {
|
|
840
|
+
return await skipJob(input.run, input.jobRun, `Skipped by if condition for job ${input.jobRun.name}.`);
|
|
841
|
+
}
|
|
842
|
+
if (input.activeState.cancelRequested) {
|
|
843
|
+
return await cancelJob(input.run, input.jobRun, "Cancelled before job start.");
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
assertJobRunnerSupported(input.jobRun);
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
await markQueuedStepsForJob(input.run.id, input.jobRun.id, "skipped");
|
|
850
|
+
const failedJob = await updateJob(input.run.id, input.jobRun.id, {
|
|
851
|
+
finished_at: nowIso(),
|
|
852
|
+
status: "failed",
|
|
853
|
+
summary: error instanceof Error ? error.message : `Job ${input.jobRun.name} failed before start.`,
|
|
854
|
+
});
|
|
855
|
+
await emitJobFinished(input.run, failedJob);
|
|
856
|
+
return failedJob;
|
|
857
|
+
}
|
|
858
|
+
const workspaceRoot = resolveActionsWorkspaceRoot(options.actions, input.run.repository_id, input.run.id);
|
|
859
|
+
const workspacePath = path.join(workspaceRoot, "jobs", input.jobRun.id, "workspace");
|
|
860
|
+
await materializeJobWorkspace({
|
|
861
|
+
commitHash: input.run.commit_hash,
|
|
862
|
+
repositoryPath: input.repositoryPath,
|
|
863
|
+
workspacePath,
|
|
864
|
+
});
|
|
865
|
+
let jobRun = await updateJob(input.run.id, input.jobRun.id, {
|
|
866
|
+
runner,
|
|
867
|
+
started_at: nowIso(),
|
|
868
|
+
status: "running",
|
|
869
|
+
summary: `Running job ${input.jobRun.name}.`,
|
|
870
|
+
});
|
|
871
|
+
await updateRun(input.run.repository_id, input.run.id, {
|
|
872
|
+
current_job: jobRun.name,
|
|
873
|
+
current_job_id: jobRun.id,
|
|
874
|
+
status: "running",
|
|
875
|
+
summary: `Running job ${jobRun.name}.`,
|
|
876
|
+
});
|
|
877
|
+
await emitJobStarted(input.run, jobRun);
|
|
878
|
+
const stepRuns = await storage.listWorkflowRunSteps(input.run.id, {
|
|
879
|
+
jobRunId: jobRun.id,
|
|
880
|
+
});
|
|
881
|
+
for (const stepRun of stepRuns) {
|
|
882
|
+
if (input.activeState.cancelRequested) {
|
|
883
|
+
await finishStep(input.run, jobRun, stepRun, {
|
|
884
|
+
status: "cancelled",
|
|
885
|
+
summary: `Cancelled before step ${stepRun.name}.`,
|
|
886
|
+
});
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const stepContext = {
|
|
890
|
+
...baseContext,
|
|
891
|
+
env: mergeRuntimeEnv({
|
|
892
|
+
actions: options.actions,
|
|
893
|
+
execution: input.execution,
|
|
894
|
+
jobEnv: workflowJob.env,
|
|
895
|
+
...(input.jobRun.matrix ? { matrix: input.jobRun.matrix } : {}),
|
|
896
|
+
run: input.run,
|
|
897
|
+
stepEnv: normalizeEnv(stepRun.metadata?.env),
|
|
898
|
+
triggerContext: normalizeTriggerContext(input.run.trigger_context),
|
|
899
|
+
workflow: input.workflow,
|
|
900
|
+
}),
|
|
901
|
+
job: {
|
|
902
|
+
status: jobRun.status,
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
const stepIf = text(stepRun.metadata?.if);
|
|
906
|
+
if (stepIf && !resolveWorkflowBoolean(stepIf, stepContext, true)) {
|
|
907
|
+
await finishStep(input.run, jobRun, stepRun, {
|
|
908
|
+
status: "skipped",
|
|
909
|
+
summary: `Skipped step ${stepRun.name}.`,
|
|
910
|
+
});
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const redactor = createRunRedactor({
|
|
914
|
+
actions: options.actions,
|
|
915
|
+
env: stepContext.env,
|
|
916
|
+
run: input.run,
|
|
917
|
+
secrets: input.execution.secrets,
|
|
918
|
+
step: stepRun,
|
|
919
|
+
});
|
|
920
|
+
const resolvedCommand = stepRun.kind === "shell"
|
|
921
|
+
? resolveWorkflowString(stepRun.command, stepContext)
|
|
922
|
+
: resolveWorkflowString(text(stepRun.uses), stepContext);
|
|
923
|
+
const startedStep = await updateStep(input.run.id, stepRun.id, {
|
|
266
924
|
started_at: nowIso(),
|
|
267
925
|
status: "running",
|
|
268
926
|
});
|
|
269
|
-
|
|
270
|
-
|
|
927
|
+
jobRun = await updateJob(input.run.id, jobRun.id, {
|
|
928
|
+
current_step: startedStep.name,
|
|
929
|
+
current_step_index: startedStep.index,
|
|
930
|
+
});
|
|
931
|
+
await updateRun(input.run.repository_id, input.run.id, {
|
|
932
|
+
current_job: jobRun.name,
|
|
933
|
+
current_job_id: jobRun.id,
|
|
271
934
|
current_step: startedStep.name,
|
|
272
935
|
current_step_index: startedStep.index,
|
|
273
936
|
status: "running",
|
|
274
|
-
summary: `Running ${startedStep.name}.`,
|
|
937
|
+
summary: `Running ${jobRun.name} / ${startedStep.name}.`,
|
|
275
938
|
});
|
|
276
|
-
await emitRunEvent(
|
|
277
|
-
command:
|
|
939
|
+
await emitRunEvent(input.run, {
|
|
940
|
+
command: await redactor(resolvedCommand),
|
|
941
|
+
job_id: jobRun.job_id,
|
|
942
|
+
job_name: jobRun.name,
|
|
943
|
+
job_run_id: jobRun.id,
|
|
278
944
|
status: "running",
|
|
279
945
|
step_id: startedStep.id,
|
|
280
946
|
step_index: startedStep.index,
|
|
281
947
|
step_name: startedStep.name,
|
|
282
948
|
type: "step.started",
|
|
283
949
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
950
|
+
if (startedStep.kind === "uses") {
|
|
951
|
+
const resolvedWith = Object.fromEntries(Object.entries((startedStep.metadata?.with && typeof startedStep.metadata.with === "object")
|
|
952
|
+
? startedStep.metadata.with
|
|
953
|
+
: {})
|
|
954
|
+
.map(([key, value]) => [key, resolveWorkflowString(String(value), stepContext)]));
|
|
955
|
+
try {
|
|
956
|
+
const result = await executeUsesStep({
|
|
957
|
+
artifactsRoot: input.artifactsRoot,
|
|
958
|
+
execution: input.execution,
|
|
959
|
+
jobRun,
|
|
960
|
+
run: input.run,
|
|
961
|
+
stepWith: resolvedWith,
|
|
962
|
+
stepRun: startedStep,
|
|
963
|
+
workspacePath,
|
|
964
|
+
});
|
|
965
|
+
const preview = await redactor(result.outputPreview || "");
|
|
966
|
+
if (preview) {
|
|
967
|
+
await emitRunEvent(input.run, {
|
|
968
|
+
chunk: preview,
|
|
969
|
+
job_id: jobRun.job_id,
|
|
970
|
+
job_name: jobRun.name,
|
|
971
|
+
job_run_id: jobRun.id,
|
|
972
|
+
status: "running",
|
|
973
|
+
step_id: startedStep.id,
|
|
974
|
+
step_index: startedStep.index,
|
|
975
|
+
step_name: startedStep.name,
|
|
976
|
+
stream: "stdout",
|
|
977
|
+
type: "step.output",
|
|
978
|
+
});
|
|
979
|
+
await emitRunEvent(input.run, {
|
|
980
|
+
chunk: preview,
|
|
981
|
+
job_id: jobRun.job_id,
|
|
982
|
+
job_name: jobRun.name,
|
|
983
|
+
job_run_id: jobRun.id,
|
|
984
|
+
status: "running",
|
|
985
|
+
step_id: startedStep.id,
|
|
986
|
+
step_index: startedStep.index,
|
|
987
|
+
step_name: startedStep.name,
|
|
988
|
+
stream: "stdout",
|
|
989
|
+
type: "job.output",
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
await finishStep(input.run, jobRun, startedStep, {
|
|
993
|
+
outputPreview: preview,
|
|
994
|
+
status: "success",
|
|
995
|
+
summary: result.summary,
|
|
996
|
+
});
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
const summary = await redactor(error instanceof Error ? error.message : "Action step failed.");
|
|
1001
|
+
await finishStep(input.run, jobRun, startedStep, {
|
|
1002
|
+
outputPreview: summary,
|
|
1003
|
+
status: input.activeState.cancelRequested ? "cancelled" : "failed",
|
|
1004
|
+
summary,
|
|
1005
|
+
});
|
|
1006
|
+
await markQueuedStepsForJob(input.run.id, jobRun.id, input.activeState.cancelRequested ? "cancelled" : "skipped");
|
|
1007
|
+
jobRun = await updateJob(input.run.id, jobRun.id, {
|
|
1008
|
+
current_step: null,
|
|
1009
|
+
current_step_index: null,
|
|
1010
|
+
finished_at: nowIso(),
|
|
1011
|
+
status: input.activeState.cancelRequested ? "cancelled" : "failed",
|
|
1012
|
+
summary,
|
|
1013
|
+
});
|
|
1014
|
+
await emitJobFinished(input.run, jobRun);
|
|
1015
|
+
return jobRun;
|
|
1016
|
+
}
|
|
295
1017
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1018
|
+
const shell = text(startedStep.metadata?.shell, text(options.actions?.shell, "bash"));
|
|
1019
|
+
const runtimeEnv = mergeRuntimeEnv({
|
|
1020
|
+
actions: options.actions,
|
|
1021
|
+
execution: input.execution,
|
|
1022
|
+
jobEnv: workflowJob.env,
|
|
1023
|
+
...(input.jobRun.matrix ? { matrix: input.jobRun.matrix } : {}),
|
|
1024
|
+
run: input.run,
|
|
1025
|
+
stepEnv: normalizeEnv(startedStep.metadata?.env),
|
|
1026
|
+
triggerContext: normalizeTriggerContext(input.run.trigger_context),
|
|
1027
|
+
workflow: input.workflow,
|
|
304
1028
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1029
|
+
const result = await runShellCommand({
|
|
1030
|
+
...(options.actions?.localRunner?.beforeSpawn ? { beforeSpawn: options.actions.localRunner.beforeSpawn } : {}),
|
|
1031
|
+
command: resolvedCommand,
|
|
1032
|
+
cwd: workspacePath,
|
|
1033
|
+
env: runtimeEnv,
|
|
1034
|
+
...(options.actions?.localRunner?.execTimeoutMs === undefined ? {} : { execTimeoutMs: options.actions.localRunner.execTimeoutMs }),
|
|
1035
|
+
...(options.actions?.localRunner?.gid === undefined ? {} : { gid: options.actions.localRunner.gid }),
|
|
1036
|
+
heartbeatIntervalMs: Math.max(250, Number(options.actions?.heartbeatIntervalMs) || DEFAULT_HEARTBEAT_INTERVAL_MS),
|
|
1037
|
+
...(options.actions?.localRunner?.uid === undefined ? {} : { uid: options.actions.localRunner.uid }),
|
|
1038
|
+
onHeartbeat: async () => {
|
|
1039
|
+
await emitRunEvent(input.run, {
|
|
1040
|
+
job_id: jobRun.job_id,
|
|
1041
|
+
job_name: jobRun.name,
|
|
1042
|
+
job_run_id: jobRun.id,
|
|
1043
|
+
status: "running",
|
|
1044
|
+
step_id: startedStep.id,
|
|
1045
|
+
step_index: startedStep.index,
|
|
1046
|
+
step_name: startedStep.name,
|
|
1047
|
+
type: "step.heartbeat",
|
|
1048
|
+
});
|
|
1049
|
+
await emitRunEvent(input.run, {
|
|
1050
|
+
job_id: jobRun.job_id,
|
|
1051
|
+
job_name: jobRun.name,
|
|
1052
|
+
job_run_id: jobRun.id,
|
|
1053
|
+
status: "running",
|
|
1054
|
+
step_id: startedStep.id,
|
|
1055
|
+
step_index: startedStep.index,
|
|
1056
|
+
step_name: startedStep.name,
|
|
1057
|
+
type: "job.heartbeat",
|
|
1058
|
+
});
|
|
1059
|
+
},
|
|
1060
|
+
onOutput: async (stream, chunk) => {
|
|
1061
|
+
const redacted = await redactor(chunk, stream);
|
|
1062
|
+
await emitRunEvent(input.run, {
|
|
1063
|
+
chunk: redacted,
|
|
1064
|
+
job_id: jobRun.job_id,
|
|
1065
|
+
job_name: jobRun.name,
|
|
1066
|
+
job_run_id: jobRun.id,
|
|
1067
|
+
status: "running",
|
|
1068
|
+
step_id: startedStep.id,
|
|
1069
|
+
step_index: startedStep.index,
|
|
1070
|
+
step_name: startedStep.name,
|
|
1071
|
+
stream,
|
|
1072
|
+
type: "step.output",
|
|
1073
|
+
});
|
|
1074
|
+
await emitRunEvent(input.run, {
|
|
1075
|
+
chunk: redacted,
|
|
1076
|
+
job_id: jobRun.job_id,
|
|
1077
|
+
job_name: jobRun.name,
|
|
1078
|
+
job_run_id: jobRun.id,
|
|
1079
|
+
status: "running",
|
|
1080
|
+
step_id: startedStep.id,
|
|
1081
|
+
step_index: startedStep.index,
|
|
1082
|
+
step_name: startedStep.name,
|
|
1083
|
+
stream,
|
|
1084
|
+
type: "job.output",
|
|
1085
|
+
});
|
|
1086
|
+
},
|
|
1087
|
+
onSpawn: (child) => {
|
|
1088
|
+
input.activeState.child = child;
|
|
1089
|
+
},
|
|
1090
|
+
shell: shell || "bash",
|
|
1091
|
+
});
|
|
1092
|
+
const redactedPreview = await redactor(result.outputPreview);
|
|
1093
|
+
if (input.activeState.cancelRequested || result.exitCode === 130) {
|
|
1094
|
+
await finishStep(input.run, jobRun, startedStep, {
|
|
1095
|
+
exitCode: result.exitCode,
|
|
1096
|
+
outputPreview: redactedPreview,
|
|
1097
|
+
status: "cancelled",
|
|
1098
|
+
summary: `Cancelled during step ${startedStep.name}.`,
|
|
1099
|
+
});
|
|
1100
|
+
await markQueuedStepsForJob(input.run.id, jobRun.id, "cancelled");
|
|
1101
|
+
jobRun = await updateJob(input.run.id, jobRun.id, {
|
|
1102
|
+
current_step: null,
|
|
1103
|
+
current_step_index: null,
|
|
1104
|
+
finished_at: nowIso(),
|
|
1105
|
+
status: "cancelled",
|
|
1106
|
+
summary: `Cancelled during job ${jobRun.name}.`,
|
|
1107
|
+
});
|
|
1108
|
+
await emitJobFinished(input.run, jobRun);
|
|
1109
|
+
return jobRun;
|
|
1110
|
+
}
|
|
1111
|
+
if (result.exitCode !== 0) {
|
|
1112
|
+
await finishStep(input.run, jobRun, startedStep, {
|
|
1113
|
+
exitCode: result.exitCode,
|
|
1114
|
+
outputPreview: redactedPreview,
|
|
1115
|
+
status: "failed",
|
|
1116
|
+
summary: `Failed step ${startedStep.name}.`,
|
|
1117
|
+
});
|
|
1118
|
+
await markQueuedStepsForJob(input.run.id, jobRun.id, "skipped");
|
|
1119
|
+
jobRun = await updateJob(input.run.id, jobRun.id, {
|
|
1120
|
+
current_step: null,
|
|
1121
|
+
current_step_index: null,
|
|
1122
|
+
finished_at: nowIso(),
|
|
1123
|
+
status: "failed",
|
|
1124
|
+
summary: `Job ${jobRun.name} failed in step ${startedStep.name}.`,
|
|
1125
|
+
});
|
|
1126
|
+
await emitJobFinished(input.run, jobRun);
|
|
1127
|
+
return jobRun;
|
|
1128
|
+
}
|
|
1129
|
+
await finishStep(input.run, jobRun, startedStep, {
|
|
1130
|
+
exitCode: result.exitCode,
|
|
1131
|
+
outputPreview: redactedPreview,
|
|
1132
|
+
status: "success",
|
|
1133
|
+
summary: `Completed step ${startedStep.name}.`,
|
|
314
1134
|
});
|
|
315
|
-
return run;
|
|
316
1135
|
}
|
|
317
|
-
|
|
318
|
-
|
|
1136
|
+
jobRun = await updateJob(input.run.id, jobRun.id, {
|
|
1137
|
+
current_step: null,
|
|
1138
|
+
current_step_index: null,
|
|
319
1139
|
finished_at: nowIso(),
|
|
320
|
-
|
|
321
|
-
|
|
1140
|
+
status: "success",
|
|
1141
|
+
summary: `Job ${jobRun.name} completed successfully.`,
|
|
322
1142
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
command: finishedStep.command,
|
|
326
|
-
metadata: {
|
|
327
|
-
exit_code: finishedStep.exit_code,
|
|
328
|
-
},
|
|
329
|
-
status: finishedStep.status,
|
|
330
|
-
step_id: finishedStep.id,
|
|
331
|
-
step_index: finishedStep.index,
|
|
332
|
-
step_name: finishedStep.name,
|
|
333
|
-
summary: text(event.summary),
|
|
334
|
-
type: "step.finished",
|
|
335
|
-
});
|
|
336
|
-
return run;
|
|
1143
|
+
await emitJobFinished(input.run, jobRun);
|
|
1144
|
+
return jobRun;
|
|
337
1145
|
}
|
|
338
1146
|
async function executeRun(repositoryId, runId) {
|
|
339
1147
|
let run = await readRequiredRun(repositoryId, runId);
|
|
@@ -343,7 +1151,7 @@ function createGitForgeActionsRuntime(options) {
|
|
|
343
1151
|
runner,
|
|
344
1152
|
started_at: nowIso(),
|
|
345
1153
|
status: "starting",
|
|
346
|
-
summary: "Preparing workflow
|
|
1154
|
+
summary: "Preparing workflow run.",
|
|
347
1155
|
});
|
|
348
1156
|
await emitRunEvent(run, {
|
|
349
1157
|
metadata: {
|
|
@@ -354,63 +1162,98 @@ function createGitForgeActionsRuntime(options) {
|
|
|
354
1162
|
type: "run.status",
|
|
355
1163
|
});
|
|
356
1164
|
const repositoryPath = await readRepositoryPath(repositoryId);
|
|
357
|
-
const
|
|
358
|
-
const
|
|
1165
|
+
const workflow = await readWorkflowAtRef(repositoryId, repositoryPath, run.ref, run.workflow_id);
|
|
1166
|
+
const execution = runExecutionContexts.get(run.id) || {
|
|
1167
|
+
actor: (run.execution_context?.actor && typeof run.execution_context.actor === "object") ? run.execution_context.actor : undefined,
|
|
1168
|
+
env: normalizeEnv(run.execution_context?.env) || {},
|
|
1169
|
+
inputs: (run.trigger_context?.inputs && typeof run.trigger_context.inputs === "object")
|
|
1170
|
+
? run.trigger_context.inputs
|
|
1171
|
+
: {},
|
|
1172
|
+
metadata: (run.execution_context?.metadata && typeof run.execution_context.metadata === "object") ? run.execution_context.metadata : undefined,
|
|
1173
|
+
secrets: {},
|
|
1174
|
+
};
|
|
359
1175
|
const activeState = {
|
|
360
1176
|
cancelRequested: false,
|
|
1177
|
+
child: null,
|
|
361
1178
|
};
|
|
362
1179
|
activeRuns.set(run.id, activeState);
|
|
363
1180
|
try {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
1181
|
+
const artifactsRoot = path.join(resolveActionsWorkspaceRoot(options.actions, run.repository_id, run.id), "artifacts");
|
|
1182
|
+
const allJobs = await storage.listWorkflowRunJobs(run.id);
|
|
1183
|
+
const pendingJobs = Array.from(allJobs).sort((left, right) => left.index - right.index);
|
|
1184
|
+
const completedByJobId = new Map();
|
|
1185
|
+
while (pendingJobs.length) {
|
|
1186
|
+
if (activeState.cancelRequested) {
|
|
1187
|
+
await markQueuedJobsAndSteps(run.id, "cancelled");
|
|
1188
|
+
return await finalizeRun(run, {
|
|
1189
|
+
eventType: "run.cancelled",
|
|
1190
|
+
status: "cancelled",
|
|
1191
|
+
summary: "Workflow run cancelled.",
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
const nextIndex = pendingJobs.findIndex((job) => ((job.needs || []).every((need) => {
|
|
1195
|
+
const results = completedByJobId.get(need) || [];
|
|
1196
|
+
const expected = allJobs.filter((entry) => entry.job_id === need).length;
|
|
1197
|
+
return results.length === expected && results.every((entry) => isTerminalJobStatus(entry.status));
|
|
1198
|
+
})));
|
|
1199
|
+
if (nextIndex < 0) {
|
|
1200
|
+
throw new GitHostError("forge_invalid_workflow_definition", `Workflow "${workflow.id}" has no runnable job order.`, {
|
|
1201
|
+
workflowId: workflow.id,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
const nextJob = pendingJobs.splice(nextIndex, 1)[0];
|
|
1205
|
+
const needs = Object.fromEntries((nextJob.needs || []).map((need) => {
|
|
1206
|
+
const statuses = (completedByJobId.get(need) || []).map((entry) => entry.status);
|
|
1207
|
+
return [need, { result: aggregateJobStatus(statuses) }];
|
|
1208
|
+
}));
|
|
1209
|
+
const finishedJob = await executeJob({
|
|
1210
|
+
activeState,
|
|
1211
|
+
artifactsRoot,
|
|
1212
|
+
execution,
|
|
1213
|
+
jobRun: nextJob,
|
|
1214
|
+
needs,
|
|
1215
|
+
repositoryPath,
|
|
1216
|
+
run,
|
|
1217
|
+
workflow,
|
|
1218
|
+
});
|
|
1219
|
+
const existing = completedByJobId.get(finishedJob.job_id) || [];
|
|
1220
|
+
existing.push(finishedJob);
|
|
1221
|
+
completedByJobId.set(finishedJob.job_id, existing);
|
|
1222
|
+
}
|
|
1223
|
+
const jobs = await storage.listWorkflowRunJobs(run.id);
|
|
1224
|
+
if (jobs.some((job) => job.status === "failed")) {
|
|
1225
|
+
const failedJob = jobs.find((job) => job.status === "failed");
|
|
1226
|
+
await markQueuedJobsAndSteps(run.id, "skipped");
|
|
1227
|
+
return await finalizeRun(run, {
|
|
1228
|
+
eventType: "run.failed",
|
|
1229
|
+
status: "failed",
|
|
1230
|
+
summary: text(failedJob?.summary, "Workflow run failed."),
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
if (jobs.some((job) => job.status === "cancelled")) {
|
|
1234
|
+
await markQueuedJobsAndSteps(run.id, "cancelled");
|
|
381
1235
|
return await finalizeRun(run, {
|
|
382
|
-
currentStep: null,
|
|
383
|
-
currentStepIndex: null,
|
|
384
1236
|
eventType: "run.cancelled",
|
|
385
1237
|
status: "cancelled",
|
|
386
1238
|
summary: "Workflow run cancelled.",
|
|
387
1239
|
});
|
|
388
1240
|
}
|
|
389
|
-
if (
|
|
390
|
-
await markQueuedSteps(run.id, failedStep ? failedStep.index : result.lastStepIndex, "skipped");
|
|
1241
|
+
if (jobs.every((job) => job.status === "skipped")) {
|
|
391
1242
|
return await finalizeRun(run, {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
status: "failed",
|
|
396
|
-
summary: failedStep
|
|
397
|
-
? `Workflow run failed in step ${failedStep.name}.`
|
|
398
|
-
: `Workflow run failed in step ${text(result.lastStepName, "unknown")}.`,
|
|
1243
|
+
eventType: "run.finished",
|
|
1244
|
+
status: "skipped",
|
|
1245
|
+
summary: "Workflow run was skipped.",
|
|
399
1246
|
});
|
|
400
1247
|
}
|
|
401
1248
|
return await finalizeRun(run, {
|
|
402
|
-
currentStep: null,
|
|
403
|
-
currentStepIndex: null,
|
|
404
1249
|
eventType: "run.finished",
|
|
405
1250
|
status: "success",
|
|
406
1251
|
summary: "Workflow run completed successfully.",
|
|
407
1252
|
});
|
|
408
1253
|
}
|
|
409
1254
|
catch (error) {
|
|
410
|
-
await
|
|
1255
|
+
await markQueuedJobsAndSteps(run.id, activeState.cancelRequested ? "cancelled" : "skipped");
|
|
411
1256
|
return await finalizeRun(run, {
|
|
412
|
-
currentStep: null,
|
|
413
|
-
currentStepIndex: null,
|
|
414
1257
|
eventType: activeState.cancelRequested ? "run.cancelled" : "run.failed",
|
|
415
1258
|
status: activeState.cancelRequested ? "cancelled" : "failed",
|
|
416
1259
|
summary: activeState.cancelRequested
|
|
@@ -426,6 +1269,7 @@ function createGitForgeActionsRuntime(options) {
|
|
|
426
1269
|
catch { }
|
|
427
1270
|
}
|
|
428
1271
|
activeRuns.delete(run.id);
|
|
1272
|
+
runExecutionContexts.delete(run.id);
|
|
429
1273
|
}
|
|
430
1274
|
}
|
|
431
1275
|
async function processQueue() {
|
|
@@ -457,7 +1301,6 @@ function createGitForgeActionsRuntime(options) {
|
|
|
457
1301
|
const workflows = await listRepositoryWorkflows({
|
|
458
1302
|
filters: {
|
|
459
1303
|
enabled: true,
|
|
460
|
-
trigger: triggerKind,
|
|
461
1304
|
},
|
|
462
1305
|
ref: target.ref,
|
|
463
1306
|
repositoryId,
|
|
@@ -473,7 +1316,6 @@ function createGitForgeActionsRuntime(options) {
|
|
|
473
1316
|
branch: target.branch || undefined,
|
|
474
1317
|
commitHash: target.commitHash,
|
|
475
1318
|
ref: target.ref,
|
|
476
|
-
releaseId: text(context.release_id) || null,
|
|
477
1319
|
triggerContext: context,
|
|
478
1320
|
triggerKind,
|
|
479
1321
|
}));
|
|
@@ -532,7 +1374,7 @@ function createGitForgeActionsRuntime(options) {
|
|
|
532
1374
|
return entry;
|
|
533
1375
|
};
|
|
534
1376
|
}
|
|
535
|
-
|
|
1377
|
+
const runtime = {
|
|
536
1378
|
bindActivityStorage,
|
|
537
1379
|
async listWorkflows(repositoryId, filters) {
|
|
538
1380
|
const repositoryPath = await readRepositoryPath(repositoryId);
|
|
@@ -551,6 +1393,11 @@ function createGitForgeActionsRuntime(options) {
|
|
|
551
1393
|
async runWorkflow(repositoryId, workflowId, input) {
|
|
552
1394
|
const target = await resolveRunTarget(repositoryId, input);
|
|
553
1395
|
const workflow = await readWorkflowAtRef(repositoryId, target.repositoryPath, target.ref, workflowId);
|
|
1396
|
+
if (!workflow.on?.workflow_dispatch && workflow.trigger !== "manual") {
|
|
1397
|
+
throw new GitHostError("forge_invalid_workflow_definition", `Workflow "${workflowId}" does not support manual dispatch.`, {
|
|
1398
|
+
workflowId,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
554
1401
|
return await queueWorkflowRun(repositoryId, workflow, {
|
|
555
1402
|
...input,
|
|
556
1403
|
branch: target.branch || undefined,
|
|
@@ -564,6 +1411,14 @@ function createGitForgeActionsRuntime(options) {
|
|
|
564
1411
|
if (isTerminalRunStatus(run.status))
|
|
565
1412
|
return run;
|
|
566
1413
|
const active = activeRuns.get(run.id);
|
|
1414
|
+
await emitRunEvent(run, {
|
|
1415
|
+
metadata: {
|
|
1416
|
+
actor_id: actor.id,
|
|
1417
|
+
},
|
|
1418
|
+
status: "running",
|
|
1419
|
+
summary: `Cancellation requested by ${text(actor.id)}.`,
|
|
1420
|
+
type: "run.cancellation_requested",
|
|
1421
|
+
});
|
|
567
1422
|
if (active) {
|
|
568
1423
|
active.cancelRequested = true;
|
|
569
1424
|
if (active.child && !active.child.killed) {
|
|
@@ -576,6 +1431,7 @@ function createGitForgeActionsRuntime(options) {
|
|
|
576
1431
|
summary: `Cancellation requested by ${text(actor.id)}.`,
|
|
577
1432
|
});
|
|
578
1433
|
}
|
|
1434
|
+
await markQueuedJobsAndSteps(run.id, "cancelled");
|
|
579
1435
|
const cancelled = await updateRun(repositoryId, runId, {
|
|
580
1436
|
finished_at: nowIso(),
|
|
581
1437
|
status: "cancelled",
|
|
@@ -594,9 +1450,17 @@ function createGitForgeActionsRuntime(options) {
|
|
|
594
1450
|
async readWorkflowRun(repositoryId, runId) {
|
|
595
1451
|
return await readRequiredRun(repositoryId, runId);
|
|
596
1452
|
},
|
|
597
|
-
async
|
|
1453
|
+
async listWorkflowRunJobs(repositoryId, runId, filters) {
|
|
1454
|
+
await readRequiredRun(repositoryId, runId);
|
|
1455
|
+
return await storage.listWorkflowRunJobs(runId, filters);
|
|
1456
|
+
},
|
|
1457
|
+
async listWorkflowRunSteps(repositoryId, runId, filters) {
|
|
1458
|
+
await readRequiredRun(repositoryId, runId);
|
|
1459
|
+
return await storage.listWorkflowRunSteps(runId, filters);
|
|
1460
|
+
},
|
|
1461
|
+
async listWorkflowRunArtifacts(repositoryId, runId, filters) {
|
|
598
1462
|
await readRequiredRun(repositoryId, runId);
|
|
599
|
-
return await storage.
|
|
1463
|
+
return await storage.listWorkflowRunArtifacts(runId, filters);
|
|
600
1464
|
},
|
|
601
1465
|
async listWorkflowRunEvents(repositoryId, runId, filters) {
|
|
602
1466
|
await readRequiredRun(repositoryId, runId);
|
|
@@ -620,6 +1484,7 @@ function createGitForgeActionsRuntime(options) {
|
|
|
620
1484
|
};
|
|
621
1485
|
},
|
|
622
1486
|
};
|
|
1487
|
+
return runtime;
|
|
623
1488
|
}
|
|
624
1489
|
export { createGitForgeActionsRuntime, isTerminalRunStatus };
|
|
625
1490
|
//# sourceMappingURL=forge_actions.js.map
|