ensemble-review 1.0.36 → 1.0.38
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/dist/api/plugins/rate-limit.js +22 -0
- package/dist/api/routes/releases.js +181 -0
- package/dist/app.js +3 -0
- package/dist/coding/story-build/base-refs.js +10 -2
- package/dist/coding/story-build/loop/driver.js +1 -1
- package/dist/coding/story-build/loop/routes.js +9 -1
- package/dist/coding/story-build/persist.js +53 -7
- package/dist/coding/story-build/plan-persist.js +1 -1
- package/dist/coding/story-build/push-hotfix.js +293 -0
- package/dist/coding/story-build/push.js +5 -0
- package/dist/coding/story-build/routes-build.js +25 -3
- package/dist/db/erase-account.js +14 -0
- package/dist/db/hotfix-bases.js +31 -0
- package/dist/db/migrations/0195_releases.sql +128 -0
- package/dist/db/migrations/0196_story_build_hotfix.sql +54 -0
- package/dist/db/migrations/0197_release_items_live_tag.sql +11 -0
- package/dist/db/migrations/0198_story_build_port_report.sql +7 -0
- package/dist/db/migrations/meta/_journal.json +28 -0
- package/dist/db/queries.js +21 -0
- package/dist/db/release-check.js +378 -0
- package/dist/db/release-settings.js +94 -0
- package/dist/db/repo-tags.js +82 -0
- package/dist/db/schema.sqlite.js +246 -0
- package/dist/github/mutations.js +67 -0
- package/dist/github/queries.js +5 -0
- package/dist/github/settle-query.js +11 -1
- package/dist/github/tag-queries.js +101 -0
- package/dist/releases/create.js +539 -0
- package/dist/releases/github.js +78 -0
- package/dist/releases/notes.js +152 -0
- package/dist/releases/refresh.js +43 -0
- package/dist/sandbox/session/manager.js +207 -12
- package/dist/sandbox/session/plan.js +19 -7
- package/dist/sandbox/session/routes.js +12 -2
- package/dist/sandbox/workspace/hotfix.js +30 -0
- package/dist/sandbox/workspace/index.js +38 -2
- package/dist/sandbox/workspace/port-report.js +137 -0
- package/dist/sandbox/workspace/seed.js +40 -0
- package/dist/shared/brand-mark.js +76 -0
- package/dist/shared/hotfix.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/release-check.js +528 -0
- package/dist/shared/release-write.js +264 -0
- package/dist/shared/releases.js +89 -0
- package/dist/shared/story-build.js +2 -0
- package/dist/sync/repo-tags.js +368 -0
- package/dist/sync/settle-sweep.js +47 -2
- package/dist/sync/sync-repo.js +12 -0
- package/dist/sync/tag-ranges.js +350 -0
- package/dist/sync/tags-step.js +77 -0
- package/dist/sync/upsert.js +23 -0
- package/package.json +3 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/{AiFixTab-CVIZl9I8.js → AiFixTab-BSirFzeA.js} +1 -1
- package/public/assets/{PrBotBehaviourTab-q0M7RU1e.js → PrBotBehaviourTab-C2tE_eCn.js} +1 -1
- package/public/assets/index-Bhnig73n.js +138 -0
- package/public/assets/index-DwUNcLc4.css +1 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon-48.png +0 -0
- package/public/favicon.svg +1 -5
- package/public/index.html +7 -8
- package/public/assets/index-CbEfmqOD.css +0 -1
- package/public/assets/index-DBK4RWQu.js +0 -136
|
@@ -471,6 +471,26 @@ function tierFor(method, path) {
|
|
|
471
471
|
// GET /api/ticket-prs — the ticket tab: stored ticket rows joined to PRs, DB-only.
|
|
472
472
|
if (!mutating && path === '/api/ticket-prs')
|
|
473
473
|
return [TIERS.read];
|
|
474
|
+
// GET /api/releases — This Sprint → Releases: the release check over stored tags, ranges and ticket
|
|
475
|
+
// rows (db/release-check.ts). DB only, never a GitHub or tracker call. `read`, DECIDED; EXACT `===`
|
|
476
|
+
// (the release WRITES will be their own paths with their own tiers).
|
|
477
|
+
if (!mutating && path === '/api/releases')
|
|
478
|
+
return [TIERS.read];
|
|
479
|
+
// GET /api/releases/:id — one stored release (its notes and step results). DB only.
|
|
480
|
+
if (!mutating && /^\/api\/releases\/\d+$/.test(path))
|
|
481
|
+
return [TIERS.read];
|
|
482
|
+
// POST /api/releases/refresh — Refresh: re-reads the held tickets from the tracker (the customer's
|
|
483
|
+
// quota, as every tracker call → `search`). No GitHub call.
|
|
484
|
+
if (method === 'POST' && path === '/api/releases/refresh')
|
|
485
|
+
return [TIERS.search];
|
|
486
|
+
// POST /api/releases/preview — the "Create release…" dialog: a model summary per story when AI is on
|
|
487
|
+
// (model dollars, the reader's click) → `ai` + `ai_hourly`; no GitHub or tracker call.
|
|
488
|
+
if (method === 'POST' && path === '/api/releases/preview')
|
|
489
|
+
return [TIERS.ai, TIERS.aiHourly];
|
|
490
|
+
// POST /api/releases — Confirm: re-reads every ticket from the tracker (the customer's quota, as every
|
|
491
|
+
// tracker call → `search`) and writes release tags, pre-releases and tracker comments (`github_write`).
|
|
492
|
+
if (method === 'POST' && path === '/api/releases')
|
|
493
|
+
return [TIERS.search, TIERS.githubWrite];
|
|
474
494
|
// A TRACKER WRITE (tracker/status-change.ts): GET /api/ticket-status/transitions (one or two
|
|
475
495
|
// Jira calls, on the picker's open) and POST /api/ticket-status (the transition + its re-read) both
|
|
476
496
|
// spend the customer's Jira quota with the saved token → `search`, as every other tracker call.
|
|
@@ -602,6 +622,8 @@ function tierFor(method, path) {
|
|
|
602
622
|
// GET list / :id / stream / diff / repos/:repoId/diff / prs/:prId/diff / plan · POST interrupt /
|
|
603
623
|
// network/allow / discard / plan/decisions / auto-push → read
|
|
604
624
|
// (auto-push only stores the reader's opt-in; the push it arms runs once, on the host, after a build turn)
|
|
625
|
+
// (a HOTFIX build's push is the same route: the hotfix branch, two PRs and one PR-body edit → github_write)
|
|
626
|
+
// GET /api/story-builds/hotfix-bases → read (DB only: the synced tags)
|
|
605
627
|
// POST …/:id/auto-approve · …/:id/ticket-review · GET /api/story-builds/active → read (DB only: the
|
|
606
628
|
// automatic approval's build turn is started by the plan turn's end and asks the spend gate itself)
|
|
607
629
|
// POST …/:id/loop/continue THE REVIEW LOOP's Continue: may start reviews, a fix turn AND pushes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { RELEASE_NOTES_SECTION_MAX, RELEASE_SUFFIX_MAX, validateReleaseSettings, } from '../../shared/index.js';
|
|
2
|
+
import { ReleaseRequestError, createRelease, getReleaseDetail, previewRelease } from '../../releases/create.js';
|
|
3
|
+
import { getReleases } from '../../db/release-check.js';
|
|
4
|
+
import { refreshReleases } from '../../releases/refresh.js';
|
|
5
|
+
import { getReleaseSettingsResponse, setReleaseSettings } from '../../db/release-settings.js';
|
|
6
|
+
import { accountIdOf } from '../plugins/auth.js';
|
|
7
|
+
// RELEASES (docs/RELEASES.md; docs/API.md § Releases). CORE, always registered.
|
|
8
|
+
//
|
|
9
|
+
// GET /api/workspaces/:id/release-settings the workspace's release + hotfix suffixes (null = not
|
|
10
|
+
// set; there is no default), `configured` (both set) and
|
|
11
|
+
// `hasReleases` (the form warns before a change).
|
|
12
|
+
// PUT /api/workspaces/:id/release-settings the WHOLE setting: `{ releaseSuffix, hotfixSuffix }`,
|
|
13
|
+
// null or "" clears one. 400 with the SAME sentence
|
|
14
|
+
// shared `validateReleaseSettings` gives the Settings form.
|
|
15
|
+
// GET /api/releases?workspace=<id> This Sprint → Releases: the release check (shared
|
|
16
|
+
// `foldReleaseCheck` over db/release-check.ts's stored
|
|
17
|
+
// inputs), the newest release, whether both suffixes are
|
|
18
|
+
// set. DB only, NEVER a GitHub or tracker call; tier `read`.
|
|
19
|
+
// POST /api/releases/refresh?workspace=<id> Refresh (a reader's press): the tickets the check holds by
|
|
20
|
+
// their own state read again from the tracker
|
|
21
|
+
// (releases/refresh.ts), then the check. Tier `search`.
|
|
22
|
+
// POST /api/releases/preview?workspace=<id> "Create release…" opening: `{repoIds}` → the snapshot,
|
|
23
|
+
// the notes (a model summary per story when AI is on — a
|
|
24
|
+
// reader's click, never refused by the spend gate) and the
|
|
25
|
+
// comment preview. No GitHub or tracker call; tier `ai`.
|
|
26
|
+
// POST /api/releases?workspace=<id> Confirm: `{repoIds, snapshot, sections?}` → re-reads the
|
|
27
|
+
// tickets and the tags, then the writes (releases/create.ts).
|
|
28
|
+
// Tracker reads + GitHub writes: tier `search` + `github_write`.
|
|
29
|
+
// GET /api/releases/:id one stored release (its notes and per-step results).
|
|
30
|
+
//
|
|
31
|
+
// Both are DB only — the workspace-CRUD `read` tier (api/plugins/rate-limit.ts). 404 on a workspace
|
|
32
|
+
// that is not this account's, like every id-addressed workspace route.
|
|
33
|
+
const idParamSchema = {
|
|
34
|
+
type: 'object',
|
|
35
|
+
required: ['id'],
|
|
36
|
+
properties: { id: { type: 'integer' } },
|
|
37
|
+
};
|
|
38
|
+
const suffixSchema = { type: ['string', 'null'], maxLength: RELEASE_SUFFIX_MAX * 4 };
|
|
39
|
+
const repoIdsSchema = { type: 'array', minItems: 1, maxItems: 200, items: { type: 'integer' } };
|
|
40
|
+
const snapshotSchema = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
required: ['repos', 'tickets'],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
repos: {
|
|
46
|
+
type: 'array',
|
|
47
|
+
maxItems: 200,
|
|
48
|
+
items: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
required: ['repoId', 'tagName', 'sha'],
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
properties: {
|
|
53
|
+
repoId: { type: 'integer' },
|
|
54
|
+
tagName: { type: 'string', minLength: 1, maxLength: 300 },
|
|
55
|
+
sha: { type: 'string', minLength: 1, maxLength: 64 },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
tickets: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
maxItems: 2000,
|
|
62
|
+
items: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
required: ['ident', 'key'],
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
properties: { ident: { type: 'string', minLength: 1, maxLength: 600 }, key: { type: 'string', minLength: 1, maxLength: 200 } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export async function releaseRoutes(app) {
|
|
72
|
+
app.get('/api/releases', async (req) => {
|
|
73
|
+
const q = req.query;
|
|
74
|
+
return getReleases(accountIdOf(req), q.workspace);
|
|
75
|
+
});
|
|
76
|
+
const refusal = (reply, err) => {
|
|
77
|
+
if (err instanceof ReleaseRequestError) {
|
|
78
|
+
reply.status(err.status);
|
|
79
|
+
return { error: err.code, message: err.message };
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
};
|
|
83
|
+
app.post('/api/releases/refresh', async (req) => {
|
|
84
|
+
const q = req.query;
|
|
85
|
+
return refreshReleases(accountIdOf(req), q.workspace);
|
|
86
|
+
});
|
|
87
|
+
app.post('/api/releases/preview', {
|
|
88
|
+
schema: {
|
|
89
|
+
body: { type: 'object', required: ['repoIds'], additionalProperties: false, properties: { repoIds: repoIdsSchema } },
|
|
90
|
+
},
|
|
91
|
+
}, async (req, reply) => {
|
|
92
|
+
const q = req.query;
|
|
93
|
+
try {
|
|
94
|
+
return await previewRelease(accountIdOf(req), q.workspace, req.body);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return refusal(reply, err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
app.post('/api/releases', {
|
|
101
|
+
schema: {
|
|
102
|
+
body: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
required: ['repoIds', 'snapshot'],
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
properties: {
|
|
107
|
+
repoIds: repoIdsSchema,
|
|
108
|
+
snapshot: snapshotSchema,
|
|
109
|
+
sections: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
maxItems: 200,
|
|
112
|
+
items: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
required: ['repoId', 'markdown'],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
properties: { repoId: { type: 'integer' }, markdown: { type: 'string', maxLength: RELEASE_NOTES_SECTION_MAX } },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}, async (req, reply) => {
|
|
123
|
+
const q = req.query;
|
|
124
|
+
try {
|
|
125
|
+
return await createRelease(accountIdOf(req), q.workspace, req.body);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
return refusal(reply, err);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
app.get('/api/releases/:id', { schema: { params: idParamSchema } }, async (req, reply) => {
|
|
132
|
+
const { id } = req.params;
|
|
133
|
+
const detail = await getReleaseDetail(accountIdOf(req), id);
|
|
134
|
+
if (detail == null) {
|
|
135
|
+
reply.status(404);
|
|
136
|
+
return { error: 'NotFound', message: `Release ${id} not found` };
|
|
137
|
+
}
|
|
138
|
+
return detail;
|
|
139
|
+
});
|
|
140
|
+
app.get('/api/workspaces/:id/release-settings', { schema: { params: idParamSchema } }, async (req, reply) => {
|
|
141
|
+
const accountId = accountIdOf(req);
|
|
142
|
+
const { id } = req.params;
|
|
143
|
+
const res = await getReleaseSettingsResponse(accountId, id);
|
|
144
|
+
if (!res) {
|
|
145
|
+
reply.status(404);
|
|
146
|
+
return { error: 'NotFound', message: `Workspace ${id} not found` };
|
|
147
|
+
}
|
|
148
|
+
return res;
|
|
149
|
+
});
|
|
150
|
+
app.put('/api/workspaces/:id/release-settings', {
|
|
151
|
+
schema: {
|
|
152
|
+
params: idParamSchema,
|
|
153
|
+
body: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
required: ['releaseSuffix', 'hotfixSuffix'],
|
|
157
|
+
properties: { releaseSuffix: suffixSchema, hotfixSuffix: suffixSchema },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
}, async (req, reply) => {
|
|
161
|
+
const accountId = accountIdOf(req);
|
|
162
|
+
const { id } = req.params;
|
|
163
|
+
const body = req.body;
|
|
164
|
+
const problem = validateReleaseSettings(body);
|
|
165
|
+
if (problem) {
|
|
166
|
+
reply.status(400);
|
|
167
|
+
return { error: 'InvalidReleaseSettings', message: problem };
|
|
168
|
+
}
|
|
169
|
+
if (!(await setReleaseSettings(accountId, id, body))) {
|
|
170
|
+
reply.status(404);
|
|
171
|
+
return { error: 'NotFound', message: `Workspace ${id} not found` };
|
|
172
|
+
}
|
|
173
|
+
const res = await getReleaseSettingsResponse(accountId, id);
|
|
174
|
+
if (!res) {
|
|
175
|
+
reply.status(404);
|
|
176
|
+
return { error: 'NotFound', message: `Workspace ${id} not found` };
|
|
177
|
+
}
|
|
178
|
+
return res;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=releases.js.map
|
package/dist/app.js
CHANGED
|
@@ -18,6 +18,7 @@ import { registerTracker } from './tracker/index.js';
|
|
|
18
18
|
import { registerSonar } from './sonar/index.js';
|
|
19
19
|
import { repoRoutes } from './api/routes/repos.js';
|
|
20
20
|
import { workspaceRoutes } from './api/routes/workspaces.js';
|
|
21
|
+
import { releaseRoutes } from './api/routes/releases.js';
|
|
21
22
|
import { sandboxSettingsRoutes } from './api/routes/sandbox-settings.js';
|
|
22
23
|
import { userRoutes } from './api/routes/users.js';
|
|
23
24
|
import { timelineRoutes } from './api/routes/timeline.js';
|
|
@@ -161,6 +162,8 @@ export async function buildApp() {
|
|
|
161
162
|
// a repo belongs to exactly one — so assignment is a MOVE and there is no "unassign" route.
|
|
162
163
|
// Account-scoped, no AI, no GitHub calls.
|
|
163
164
|
await app.register(workspaceRoutes);
|
|
165
|
+
// Releases (CORE, docs/RELEASES.md): the per-workspace release + hotfix suffixes. DB only.
|
|
166
|
+
await app.register(releaseRoutes);
|
|
164
167
|
await app.register(userRoutes);
|
|
165
168
|
await app.register(timelineRoutes);
|
|
166
169
|
await app.register(prRoutes);
|
|
@@ -20,6 +20,11 @@ export const STORY_BUILD_REF_PREFIX = 'refs/pierre/story-build/';
|
|
|
20
20
|
export function storyBuildBaseRef(buildId, repoId) {
|
|
21
21
|
return `${STORY_BUILD_REF_PREFIX}${buildId}/${repoId}`;
|
|
22
22
|
}
|
|
23
|
+
/** HOTFIX: the ref pinning one repository's MAIN checkout base (the default branch at the build's start). It
|
|
24
|
+
* shares the repository's sweep key (`buildId/repoId`), so it is kept and dropped with that base. */
|
|
25
|
+
export function storyBuildMainRef(buildId, repoId) {
|
|
26
|
+
return `${STORY_BUILD_REF_PREFIX}${buildId}/main/${repoId}`;
|
|
27
|
+
}
|
|
23
28
|
/** The sweep's keep-list spelling of a key: `buildId/repoId` or `buildId/stack/prRowId`. */
|
|
24
29
|
export function storyBuildRefKey(k) {
|
|
25
30
|
return k.stackPrId != null ? `${k.buildId}/stack/${k.stackPrId}` : `${k.buildId}/${k.repoId}`;
|
|
@@ -32,6 +37,9 @@ export function parseStoryBuildBaseRef(ref) {
|
|
|
32
37
|
const stack = /^(\d+)\/stack\/(\d+)$/.exec(rest);
|
|
33
38
|
if (stack != null)
|
|
34
39
|
return { buildId: Number(stack[1]), stackPrId: Number(stack[2]) };
|
|
40
|
+
const main = /^(\d+)\/main\/(\d+)$/.exec(rest);
|
|
41
|
+
if (main != null)
|
|
42
|
+
return { buildId: Number(main[1]), repoId: Number(main[2]) };
|
|
35
43
|
const m = /^(\d+)\/(\d+)$/.exec(rest);
|
|
36
44
|
if (m == null)
|
|
37
45
|
return null;
|
|
@@ -58,8 +66,8 @@ export async function dropStoryBuildBaseRefs(fullName, buildId, repoId) {
|
|
|
58
66
|
if (!existsSync(join(dir, '.git')))
|
|
59
67
|
return;
|
|
60
68
|
await withRepoLock(`${owner}/${name}`, async () => {
|
|
61
|
-
const
|
|
62
|
-
const refs = (await gitOut(dir, ['for-each-ref', '--format=%(refname)',
|
|
69
|
+
const prefixes = repoId != null ? [storyBuildBaseRef(buildId, repoId), storyBuildMainRef(buildId, repoId)] : [`${STORY_BUILD_REF_PREFIX}${buildId}/`];
|
|
70
|
+
const refs = (await gitOut(dir, ['for-each-ref', '--format=%(refname)', ...prefixes]).catch(() => ''))
|
|
63
71
|
.split('\n')
|
|
64
72
|
.map((l) => l.trim())
|
|
65
73
|
.filter(Boolean);
|
|
@@ -181,7 +181,7 @@ export async function maybeStartLoop(ctx, input) {
|
|
|
181
181
|
if (running.has(buildId))
|
|
182
182
|
return 'skipped';
|
|
183
183
|
const b = await getStoryBuildRow(ctx, accountId, buildId);
|
|
184
|
-
if (!b || !b.autoPush || b.loopClaimedAt != null || b.autoPushClaimedAt != null || b.status !== 'ready')
|
|
184
|
+
if (!b || !b.autoPush || b.hotfix === true || b.loopClaimedAt != null || b.autoPushClaimedAt != null || b.status !== 'ready')
|
|
185
185
|
return 'skipped';
|
|
186
186
|
const targets = (await loopTargets(ctx, accountId, buildId)).filter(isLive);
|
|
187
187
|
if (targets.length === 0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { STORY_BUILD_API, STORY_BUILD_BRANCH_MAX, STORY_BUILD_LOOP_ROUNDS_MAX, STORY_BUILD_LOOP_ROUNDS_MIN, } from '../../../shared/index.js';
|
|
1
|
+
import { HOTFIX_NO_LOOP_LINE, STORY_BUILD_API, STORY_BUILD_BRANCH_MAX, STORY_BUILD_LOOP_ROUNDS_MAX, STORY_BUILD_LOOP_ROUNDS_MIN, } from '../../../shared/index.js';
|
|
2
2
|
import { getStoryBuildRow } from '../persist.js';
|
|
3
3
|
import { continueLoop, setLoopSettings, stopLoop, storyLoopWire } from './driver.js';
|
|
4
4
|
const NOT_FOUND = { error: 'NotFound', message: 'Not found.' };
|
|
@@ -73,6 +73,14 @@ export function registerStoryBuildLoopRoutes(app, ctx) {
|
|
|
73
73
|
if (!body) {
|
|
74
74
|
return reply.code(400).send({ error: 'BadRequest', message: `Send \`on\` (true or false) and/or \`rounds\` (${STORY_BUILD_LOOP_ROUNDS_MIN} to ${STORY_BUILD_LOOP_ROUNDS_MAX}).` });
|
|
75
75
|
}
|
|
76
|
+
// A HOTFIX build never runs the review loop (v1; docs/STORY-BUILD.md § Hotfix builds).
|
|
77
|
+
if (body.on === true) {
|
|
78
|
+
const row = await getStoryBuildRow(ctx, accountId, id);
|
|
79
|
+
if (row == null)
|
|
80
|
+
return reply.code(404).send(NOT_FOUND);
|
|
81
|
+
if (row.hotfix === true)
|
|
82
|
+
return reply.code(409).send({ error: 'HotfixNoLoop', message: HOTFIX_NO_LOOP_LINE });
|
|
83
|
+
}
|
|
76
84
|
const r = await setLoopSettings(ctx, accountId, id, body);
|
|
77
85
|
if (r.status === 'not_found')
|
|
78
86
|
return reply.code(404).send(NOT_FOUND);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, desc, eq, inArray, asc, notInArray } from 'drizzle-orm';
|
|
2
|
-
import { aiRunProvider, isFlowDiagram, isLegacyDesignDiagram, isStoryBuildOpen, isStoryBuildRunning, storyBuildDocsEffectiveDecisions, } from '../../shared/index.js';
|
|
2
|
+
import { aiRunProvider, hotfixBranchName, hotfixPortOf, isFlowDiagram, isLegacyDesignDiagram, isStoryBuildOpen, isStoryBuildRunning, storyBuildDocsEffectiveDecisions, } from '../../shared/index.js';
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
4
|
const B = (ctx) => ctx.schema.storyBuilds;
|
|
5
5
|
const R = (ctx) => ctx.schema.storyBuildRepos;
|
|
@@ -99,6 +99,7 @@ export async function insertStoryBuild(ctx, a) {
|
|
|
99
99
|
...(a.session ? { sessionPhase: 'booting', effort: a.session.effort, lastActiveAt: a.session.lastActiveAt } : {}),
|
|
100
100
|
wantDiagram: a.wantDiagram,
|
|
101
101
|
wantDocs: a.wantDocs,
|
|
102
|
+
...(a.hotfix === true ? { hotfix: true } : {}),
|
|
102
103
|
createdAt: new Date(),
|
|
103
104
|
startedAt: new Date(),
|
|
104
105
|
})
|
|
@@ -146,13 +147,14 @@ export async function upsertStoryBuildRepo(ctx, a) {
|
|
|
146
147
|
stackedOnPrId: a.stackedOnPrId,
|
|
147
148
|
branch: a.branch,
|
|
148
149
|
};
|
|
150
|
+
const hotfix = () => a.hotfix ? { hotfixTag: a.hotfix.tagName, readOnly: a.hotfix.readOnly, mainBaseRef: a.hotfix.mainBaseRef, mainBaseSha: a.hotfix.mainBaseSha } : {};
|
|
149
151
|
const rows = (await ctx.db
|
|
150
152
|
.insert(r)
|
|
151
|
-
.values(values)
|
|
153
|
+
.values({ ...values, ...hotfix() })
|
|
152
154
|
.onConflictDoUpdate({
|
|
153
155
|
target: [r.buildId, r.repoId],
|
|
154
156
|
// A separate object from `values` (CLAUDE.md: never share one between insert and `set:`).
|
|
155
|
-
set: { ref: a.ref, buildOrder: a.buildOrder, include: a.include, status: a.status, baseRef: a.baseRef, baseSha: a.baseSha, stackedOnPrId: a.stackedOnPrId, branch: a.branch },
|
|
157
|
+
set: { ref: a.ref, buildOrder: a.buildOrder, include: a.include, status: a.status, baseRef: a.baseRef, baseSha: a.baseSha, stackedOnPrId: a.stackedOnPrId, branch: a.branch, ...hotfix() },
|
|
156
158
|
})
|
|
157
159
|
.returning({ id: r.id })
|
|
158
160
|
.execute());
|
|
@@ -185,13 +187,55 @@ export async function deleteStoryBuildRepoRowsExcept(ctx, accountId, buildId, ke
|
|
|
185
187
|
.execute());
|
|
186
188
|
return rows.length;
|
|
187
189
|
}
|
|
188
|
-
/** The stored patch of one repository (the `…/diff` route; never part of `StoryBuild`).
|
|
189
|
-
|
|
190
|
+
/** The stored patch of one repository (the `…/diff` route; never part of `StoryBuild`). `target: 'main'` = a
|
|
191
|
+
* hotfix build's MAIN FIX (null on a row without a main checkout). */
|
|
192
|
+
export async function getStoryBuildRepoPatch(ctx, accountId, buildId, repoId, target = 'release') {
|
|
190
193
|
const row = await getStoryBuildRepoRow(ctx, accountId, buildId, repoId);
|
|
191
194
|
if (row == null)
|
|
192
195
|
return null;
|
|
196
|
+
if (target === 'main') {
|
|
197
|
+
if (row.mainBaseSha == null)
|
|
198
|
+
return null;
|
|
199
|
+
return { baseSha: row.mainBaseSha, patch: row.mainPatch ?? null, filesChanged: row.mainFilesChangedJson ?? [] };
|
|
200
|
+
}
|
|
193
201
|
return { baseSha: row.baseSha, patch: row.patch ?? null, filesChanged: row.filesChangedJson ?? [] };
|
|
194
202
|
}
|
|
203
|
+
const MAIN_STATUSES = new Set(['ready', 'skipped', 'failed', 'pushed']);
|
|
204
|
+
/** A hotfix build's per-repository facts on the wire (null on an ordinary build). */
|
|
205
|
+
export function toStoryBuildRepoHotfix(row, hotfixBuild) {
|
|
206
|
+
if (!hotfixBuild)
|
|
207
|
+
return null;
|
|
208
|
+
return {
|
|
209
|
+
tagName: row.hotfixTag ?? null,
|
|
210
|
+
readOnly: row.readOnly === true,
|
|
211
|
+
hotfixBranch: row.hotfixTag != null ? hotfixBranchName(row.hotfixTag) : null,
|
|
212
|
+
mainBaseRef: row.mainBaseRef ?? null,
|
|
213
|
+
mainBaseSha: row.mainBaseSha ?? null,
|
|
214
|
+
mainStatus: row.mainStatus != null && MAIN_STATUSES.has(row.mainStatus) ? row.mainStatus : null,
|
|
215
|
+
mainHasPatch: row.mainPatch != null && row.mainPatch.length > 0,
|
|
216
|
+
mainFilesChanged: row.mainFilesChangedJson ?? [],
|
|
217
|
+
mainInsertions: row.mainStatJson?.insertions ?? null,
|
|
218
|
+
mainDeletions: row.mainStatJson?.deletions ?? null,
|
|
219
|
+
mainError: row.mainError ?? null,
|
|
220
|
+
mainPushedBranch: row.mainPushedBranch ?? null,
|
|
221
|
+
mainPushedPrNumber: row.mainPushedPrNumber ?? null,
|
|
222
|
+
mainPushedPrUrl: row.mainPushedPrUrl ?? null,
|
|
223
|
+
mainPushError: row.mainPushError ?? null,
|
|
224
|
+
mainPushErrorCode: row.mainPushErrorCode ?? null,
|
|
225
|
+
branch: row.hotfixBranchJson ?? null,
|
|
226
|
+
// THE PORT REPORT: the agent's claim, checked against the two stored patches (derived on read).
|
|
227
|
+
port: row.readOnly === true ? null : storyBuildRepoPort(row),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** One hotfix repository's port report (shared `hotfixPortOf` over the stored claim and the two patches). */
|
|
231
|
+
export function storyBuildRepoPort(row) {
|
|
232
|
+
return hotfixPortOf({
|
|
233
|
+
claim: row.portReportJson ?? null,
|
|
234
|
+
releaseFiles: row.patch != null && row.patch.length > 0 ? (row.filesChangedJson ?? []) : [],
|
|
235
|
+
mainFiles: row.mainPatch != null && row.mainPatch.length > 0 ? (row.mainFilesChangedJson ?? []) : [],
|
|
236
|
+
mainChanged: row.mainPatch != null && row.mainPatch.length > 0,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
195
239
|
// ---- the wire mapper --------------------------------------------------------------------------------
|
|
196
240
|
const iso = (d) => d == null ? null : (d instanceof Date ? d : new Date(typeof d === 'number' && d < 1e12 ? d * 1000 : d)).toISOString();
|
|
197
241
|
const sumOrNull = (xs) => {
|
|
@@ -199,7 +243,7 @@ const sumOrNull = (xs) => {
|
|
|
199
243
|
return got.length === 0 ? null : got.reduce((a, b) => a + b, 0);
|
|
200
244
|
};
|
|
201
245
|
/** One repository row → wire. `candidate` supplies the one-line base note; `fullName` the repo's name. */
|
|
202
|
-
export function toStoryBuildRepo(row, fullName, candidate) {
|
|
246
|
+
export function toStoryBuildRepo(row, fullName, candidate, hotfixBuild = false) {
|
|
203
247
|
return {
|
|
204
248
|
id: row.id,
|
|
205
249
|
repoId: row.repoId,
|
|
@@ -228,6 +272,7 @@ export function toStoryBuildRepo(row, fullName, candidate) {
|
|
|
228
272
|
pushedAt: iso(row.pushedAt),
|
|
229
273
|
pushError: row.pushError ?? null,
|
|
230
274
|
pushErrorCode: row.pushErrorCode ?? null,
|
|
275
|
+
...(hotfixBuild ? { hotfix: toStoryBuildRepoHotfix(row, true) } : {}),
|
|
231
276
|
...(row.model != null || row.provider != null ? { provider: aiRunProvider(row.provider, row.model) } : {}),
|
|
232
277
|
model: row.model ?? null,
|
|
233
278
|
effort: row.effort ?? null,
|
|
@@ -244,7 +289,7 @@ export function toStoryBuild(row, repoRows, opts) {
|
|
|
244
289
|
const byRepo = new Map(candidates.map((c) => [c.repoId, c]));
|
|
245
290
|
const repos = [...repoRows]
|
|
246
291
|
.sort((a, b) => a.buildOrder - b.buildOrder || a.id - b.id)
|
|
247
|
-
.map((r) => toStoryBuildRepo(r, opts.repoNames.get(r.repoId) ?? byRepo.get(r.repoId)?.fullName ?? `#${r.repoId}`, byRepo.get(r.repoId) ?? null));
|
|
292
|
+
.map((r) => toStoryBuildRepo(r, opts.repoNames.get(r.repoId) ?? byRepo.get(r.repoId)?.fullName ?? `#${r.repoId}`, byRepo.get(r.repoId) ?? null, row.hotfix === true));
|
|
248
293
|
const built = repos.filter((r) => r.include && (r.status === 'ready' || r.status === 'pushed'));
|
|
249
294
|
const running = isStoryBuildRunning(row.status) || row.docsStatus === 'running';
|
|
250
295
|
return {
|
|
@@ -289,6 +334,7 @@ export function toStoryBuild(row, repoRows, opts) {
|
|
|
289
334
|
ticketReviewDoneAt: iso(row.ticketReviewDoneAt),
|
|
290
335
|
endToEnd: row.endToEnd === true,
|
|
291
336
|
endToEndTicket: row.e2eTicketJson ?? null,
|
|
337
|
+
hotfix: row.hotfix === true,
|
|
292
338
|
createdAt: iso(row.createdAt) ?? new Date(0).toISOString(),
|
|
293
339
|
startedAt: iso(row.startedAt),
|
|
294
340
|
completedAt: iso(row.completedAt),
|
|
@@ -171,7 +171,7 @@ export async function approvePlanRevision(ctx, a) {
|
|
|
171
171
|
if (rows.length === 0 || rows[0].approvedPlanId != null)
|
|
172
172
|
return false;
|
|
173
173
|
await db.update(b).set({ approvedPlanId: a.plan.id, approvedAt: a.now }).where(and(eq(b.id, a.buildId), eq(b.accountId, a.accountId))).execute();
|
|
174
|
-
for (const [position, p] of revision.stack.entries()) {
|
|
174
|
+
for (const [position, p] of a.writePrs === false ? [] : revision.stack.entries()) {
|
|
175
175
|
await db
|
|
176
176
|
.insert(pr)
|
|
177
177
|
.values({
|