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