@atolis-hq/wake 0.2.15 → 0.2.17
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.
|
@@ -77,6 +77,9 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
77
77
|
a:hover { text-decoration: underline; }
|
|
78
78
|
.resource-list { list-style: none; padding: 0; margin: 0 0 1rem; }
|
|
79
79
|
.resource-list li { display: flex; align-items: baseline; gap: 0.4rem; margin-bottom: 0.35rem; font-size: 0.8rem; }
|
|
80
|
+
.btn { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18); color: #fff; border-radius: 6px; padding: 0.22rem 0.55rem; cursor: pointer; font-size: 0.78rem; margin-top: 0.4rem; }
|
|
81
|
+
.btn:hover:not(:disabled) { border-color: var(--accent-light); background: rgba(45, 212, 191, 0.16); }
|
|
82
|
+
.btn:disabled { cursor: default; opacity: 0.62; }
|
|
80
83
|
</style>
|
|
81
84
|
</head>
|
|
82
85
|
<body>
|
|
@@ -229,6 +232,23 @@ async function openItem(repo, number) {
|
|
|
229
232
|
if (detail.item.wake.workspacePath) {
|
|
230
233
|
body.appendChild(el('p', { class: 'meta', text: 'workspace: ' + detail.item.wake.workspacePath }));
|
|
231
234
|
}
|
|
235
|
+
const lastRun = detail.runs.at(-1);
|
|
236
|
+
if (lastRun && lastRun.sentinel === 'FAILED') {
|
|
237
|
+
const retryBtn = el('button', { type: 'button', class: 'btn', text: 'Retry' });
|
|
238
|
+
retryBtn.addEventListener('click', async () => {
|
|
239
|
+
retryBtn.disabled = true;
|
|
240
|
+
retryBtn.textContent = 'Queuing retry…';
|
|
241
|
+
try {
|
|
242
|
+
await postJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/retry');
|
|
243
|
+
retryBtn.textContent = 'Retry queued';
|
|
244
|
+
} catch (err) {
|
|
245
|
+
retryBtn.disabled = false;
|
|
246
|
+
retryBtn.textContent = 'Retry';
|
|
247
|
+
document.getElementById('status-summary').textContent = 'retry failed: ' + err.message;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
body.appendChild(retryBtn);
|
|
251
|
+
}
|
|
232
252
|
const resources = detail.item.correlatedResources || [];
|
|
233
253
|
if (resources.length > 0) {
|
|
234
254
|
body.appendChild(el('h3', { text: 'Resources' }));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { createProjectionUpdater } from '../../core/projection-updater.js';
|
|
3
4
|
import { configuredTicketSource } from '../../domain/sources.js';
|
|
5
|
+
import { createEventEnvelope } from '../../lib/event-log.js';
|
|
4
6
|
import { writeJsonFile } from '../../lib/json-file.js';
|
|
5
7
|
import { indexHtml } from './ui-assets.js';
|
|
6
8
|
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
@@ -50,13 +52,18 @@ function parseItemPath(segments) {
|
|
|
50
52
|
}
|
|
51
53
|
export function createUiServer(options) {
|
|
52
54
|
const now = options.now ?? (() => new Date());
|
|
55
|
+
const projectionUpdater = createProjectionUpdater({
|
|
56
|
+
stateStore: options.stateStore,
|
|
57
|
+
resourceIndex: options.resourceIndex,
|
|
58
|
+
config: options.config,
|
|
59
|
+
});
|
|
53
60
|
return createServer((req, res) => {
|
|
54
|
-
void handleRequest(req, res, options, now).catch((error) => {
|
|
61
|
+
void handleRequest(req, res, options, now, projectionUpdater).catch((error) => {
|
|
55
62
|
sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
56
63
|
});
|
|
57
64
|
});
|
|
58
65
|
}
|
|
59
|
-
async function handleRequest(req, res, options, now) {
|
|
66
|
+
async function handleRequest(req, res, options, now, projectionUpdater) {
|
|
60
67
|
const url = new URL(req.url ?? '/', 'http://internal');
|
|
61
68
|
// Whether a token is required is a bind-time decision — the caller (see
|
|
62
69
|
// ui-command.ts) only ever supplies a token when it configured a
|
|
@@ -86,6 +93,47 @@ async function handleRequest(req, res, options, now) {
|
|
|
86
93
|
.filter((part) => part.length > 0)
|
|
87
94
|
.map((s) => decodeURIComponent(s));
|
|
88
95
|
const resource = segments[0];
|
|
96
|
+
if (req.method === 'POST' &&
|
|
97
|
+
resource === 'work-items' &&
|
|
98
|
+
segments.length === 3 &&
|
|
99
|
+
segments[2] === 'retry') {
|
|
100
|
+
const workItemKey = segments[1] ?? '';
|
|
101
|
+
const item = await stateStore.readIssueState(workItemKey);
|
|
102
|
+
if (item === null) {
|
|
103
|
+
sendJson(res, 404, { error: 'work item not found' });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const context = item.context;
|
|
107
|
+
if (context.lastRunSentinel !== 'FAILED') {
|
|
108
|
+
sendJson(res, 409, { error: 'work item last run is not failed' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const occurredAt = now().toISOString();
|
|
112
|
+
const retryId = `retry-${workItemKey}-${now().getTime()}`;
|
|
113
|
+
const retryEvent = createEventEnvelope({
|
|
114
|
+
eventId: retryId,
|
|
115
|
+
workItemKey,
|
|
116
|
+
streamScope: 'work-item',
|
|
117
|
+
direction: 'internal',
|
|
118
|
+
sourceSystem: 'wake',
|
|
119
|
+
sourceEventType: 'wake.retry.requested',
|
|
120
|
+
sourceRefs: { repo: item.issue.repo, issueNumber: item.issue.number },
|
|
121
|
+
occurredAt,
|
|
122
|
+
ingestedAt: occurredAt,
|
|
123
|
+
trigger: 'immediate',
|
|
124
|
+
payload: { requestedBy: 'ui' },
|
|
125
|
+
});
|
|
126
|
+
const appended = await stateStore.appendEventEnvelope(retryEvent);
|
|
127
|
+
await projectionUpdater.rebuildFromEvents([appended]);
|
|
128
|
+
const tickRequest = {
|
|
129
|
+
requestId: randomUUID(),
|
|
130
|
+
requestedAt: now().toISOString(),
|
|
131
|
+
requestedBy: 'ui:retry',
|
|
132
|
+
};
|
|
133
|
+
await writeJsonFile(stateStore.paths.tickRequestFile, tickRequest);
|
|
134
|
+
sendJson(res, 202, { workItemKey, retryEventId: retryId });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
89
137
|
if (req.method === 'POST' && resource === 'tick' && segments.length === 1) {
|
|
90
138
|
const request = {
|
|
91
139
|
requestId: randomUUID(),
|
|
@@ -4,7 +4,7 @@ import { createRunnerCliAdapter } from '../adapters/runner/runner-cli-adapter.js
|
|
|
4
4
|
import { runSandboxResumeCommand } from './sandbox-resume.js';
|
|
5
5
|
import { runSelfUpdateCommand, runSelfUpdateLoop } from './self-update-command.js';
|
|
6
6
|
import { runStopCommand } from './stop-command.js';
|
|
7
|
-
import { wakeVersion } from '../version.js';
|
|
7
|
+
import { resolveWakeVersion, wakeVersion } from '../version.js';
|
|
8
8
|
async function ensureDockerfile(input) {
|
|
9
9
|
const targetPath = resolve(input.wakeRoot, 'docker', 'Dockerfile');
|
|
10
10
|
try {
|
|
@@ -89,6 +89,7 @@ export async function runSandboxCommand(input) {
|
|
|
89
89
|
throw new Error('Sandbox build requires config.dev.repoRoot');
|
|
90
90
|
}
|
|
91
91
|
const effectiveDevMode = input.config.dev?.mode ?? 'packaged';
|
|
92
|
+
const buildVersion = effectiveDevMode === 'source' ? resolveWakeVersion({ repoRoot }) : wakeVersion;
|
|
92
93
|
await ensureDockerfile({
|
|
93
94
|
wakeRoot: input.wakeRoot,
|
|
94
95
|
devMode: effectiveDevMode,
|
|
@@ -98,7 +99,9 @@ export async function runSandboxCommand(input) {
|
|
|
98
99
|
image: input.config.sandbox.image,
|
|
99
100
|
dockerfile: resolve(input.wakeRoot, 'docker', 'Dockerfile'),
|
|
100
101
|
contextDir: repoRoot,
|
|
101
|
-
|
|
102
|
+
buildArgs: effectiveDevMode === 'packaged'
|
|
103
|
+
? { WAKE_VERSION: buildVersion }
|
|
104
|
+
: { WAKE_BUILD_TAG: buildVersion },
|
|
102
105
|
});
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
@@ -285,6 +285,21 @@ async function applyEvent(current, event, ctx, config) {
|
|
|
285
285
|
},
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
|
+
if (event.sourceEventType === 'wake.retry.requested') {
|
|
289
|
+
const nextContext = { ...current.context };
|
|
290
|
+
delete nextContext.lastRunSentinel;
|
|
291
|
+
delete nextContext.lastFailureClass;
|
|
292
|
+
delete nextContext.blockedFromStage;
|
|
293
|
+
return parseIssueStateRecord({
|
|
294
|
+
...current,
|
|
295
|
+
context: nextContext,
|
|
296
|
+
wake: {
|
|
297
|
+
...current.wake,
|
|
298
|
+
syncedAt: event.ingestedAt,
|
|
299
|
+
recentEventIds: [...current.wake.recentEventIds, event.eventId].slice(-10),
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
288
303
|
if (event.sourceEventType === 'wake.workspace.cleaned') {
|
|
289
304
|
return parseIssueStateRecord({
|
|
290
305
|
...current,
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1,127 @@
|
|
|
1
|
-
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const defaultRepoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
function gitOutputFromRoot(repoRoot, args) {
|
|
7
|
+
try {
|
|
8
|
+
return execFileSync('git', args, {
|
|
9
|
+
cwd: repoRoot,
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
12
|
+
}).trim();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function defaultReadTextFile(path) {
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(path, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function defaultListTextFiles(path) {
|
|
27
|
+
if (!existsSync(path)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const files = [];
|
|
31
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
32
|
+
const entryPath = resolve(path, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
files.push(...defaultListTextFiles(entryPath));
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile()) {
|
|
37
|
+
const content = defaultReadTextFile(entryPath);
|
|
38
|
+
if (content !== undefined) {
|
|
39
|
+
files.push({ path: entryPath, content });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
function currentHeadHash(repoRoot, readTextFile) {
|
|
46
|
+
const head = readTextFile(resolve(repoRoot, '.git', 'HEAD'))?.trim();
|
|
47
|
+
if (head === undefined || head.length === 0) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
if (!head.startsWith('ref: ')) {
|
|
51
|
+
return head;
|
|
52
|
+
}
|
|
53
|
+
const refPath = head.slice('ref: '.length).trim();
|
|
54
|
+
return readTextFile(resolve(repoRoot, '.git', ...refPath.split('/')))?.trim() ?? '';
|
|
55
|
+
}
|
|
56
|
+
function looseTagRefs(repoRoot, listTextFiles) {
|
|
57
|
+
const tagsRoot = resolve(repoRoot, '.git', 'refs', 'tags');
|
|
58
|
+
return listTextFiles(tagsRoot).map((file) => ({
|
|
59
|
+
name: file.path.replaceAll('\\', '/').slice(tagsRoot.replaceAll('\\', '/').length + 1),
|
|
60
|
+
hash: file.content.trim(),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
function packedTagRefs(repoRoot, readTextFile) {
|
|
64
|
+
const packedRefs = readTextFile(resolve(repoRoot, '.git', 'packed-refs'));
|
|
65
|
+
if (packedRefs === undefined) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const refs = [];
|
|
69
|
+
let previousTag;
|
|
70
|
+
for (const line of packedRefs.split(/\r?\n/)) {
|
|
71
|
+
if (line.length === 0 || line.startsWith('#')) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (line.startsWith('^')) {
|
|
75
|
+
if (previousTag !== undefined) {
|
|
76
|
+
previousTag.hash = line.slice(1).trim();
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
previousTag = undefined;
|
|
81
|
+
const [hash, ref] = line.split(' ');
|
|
82
|
+
if (hash !== undefined && ref?.startsWith('refs/tags/') === true) {
|
|
83
|
+
previousTag = { name: ref.slice('refs/tags/'.length), hash };
|
|
84
|
+
refs.push(previousTag);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return refs;
|
|
88
|
+
}
|
|
89
|
+
function exactTagFromGitFiles(repoRoot, readTextFile, listTextFiles) {
|
|
90
|
+
const headHash = currentHeadHash(repoRoot, readTextFile);
|
|
91
|
+
if (headHash.length === 0) {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
const matchingTag = [
|
|
95
|
+
...looseTagRefs(repoRoot, listTextFiles),
|
|
96
|
+
...packedTagRefs(repoRoot, readTextFile),
|
|
97
|
+
].find((tag) => tag.hash === headHash);
|
|
98
|
+
return matchingTag?.name ?? '';
|
|
99
|
+
}
|
|
100
|
+
export function resolveWakeVersion(options = {}) {
|
|
101
|
+
const repoRoot = options.repoRoot ?? defaultRepoRoot;
|
|
102
|
+
const gitOutput = options.gitOutput ?? ((args) => gitOutputFromRoot(repoRoot, args));
|
|
103
|
+
const readTextFile = options.readTextFile ?? defaultReadTextFile;
|
|
104
|
+
const listTextFiles = options.listTextFiles ?? defaultListTextFiles;
|
|
105
|
+
const exactTag = gitOutput(['describe', '--tags', '--exact-match', 'HEAD']);
|
|
106
|
+
if (exactTag.length > 0) {
|
|
107
|
+
return exactTag;
|
|
108
|
+
}
|
|
109
|
+
const exactFileTag = exactTagFromGitFiles(repoRoot, readTextFile, listTextFiles);
|
|
110
|
+
if (exactFileTag.length > 0) {
|
|
111
|
+
return exactFileTag;
|
|
112
|
+
}
|
|
113
|
+
const latestTag = gitOutput(['describe', '--tags', '--abbrev=0']);
|
|
114
|
+
const shortHash = gitOutput(['rev-parse', '--short', 'HEAD']);
|
|
115
|
+
if (latestTag.length > 0 && shortHash.length > 0) {
|
|
116
|
+
return `${latestTag}+g${shortHash}`;
|
|
117
|
+
}
|
|
118
|
+
if (shortHash.length > 0) {
|
|
119
|
+
return `g${shortHash}`;
|
|
120
|
+
}
|
|
121
|
+
const headHash = currentHeadHash(repoRoot, readTextFile);
|
|
122
|
+
if (headHash.length > 0) {
|
|
123
|
+
return `g${headHash.slice(0, 7)}`;
|
|
124
|
+
}
|
|
125
|
+
return '0.1.0-dev';
|
|
126
|
+
}
|
|
127
|
+
export const wakeVersion = "g743a5ea";
|