ensemble-review 1.0.35 → 1.0.37
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/hotfix.js +165 -0
- package/dist/shared/index.js +4 -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 +5 -2
- package/public/assets/{AiFixTab-CVIZl9I8.js → AiFixTab-BC3tDZ3z.js} +1 -1
- package/public/assets/{PrBotBehaviourTab-q0M7RU1e.js → PrBotBehaviourTab-C5j5uhLn.js} +1 -1
- package/public/assets/index-BrjUDY3j.js +138 -0
- package/public/assets/index-D1zhsqSF.css +1 -0
- package/public/index.html +2 -2
- package/dist/ai/check-tool.js +0 -6
- package/dist/api/routes/auth.js +0 -194
- package/dist/api/routes/billing.js +0 -209
- package/dist/api/routes/claude-review.js +0 -522
- package/dist/api/routes/contact.js +0 -318
- package/dist/api/routes/feed.js +0 -21
- package/dist/api/routes/inbox.js +0 -45
- package/dist/api/routes/mentions.js +0 -22
- package/dist/api/routes/teams.js +0 -150
- package/dist/api/routes/webhooks.js +0 -181
- package/dist/coding/activity.js +0 -80
- package/dist/coding/agent.js +0 -147
- package/dist/coding/ai-fix/checks-preview.js +0 -58
- package/dist/coding/ai-fix/comment-seed.js +0 -519
- package/dist/coding/ai-fix/prompts.js +0 -283
- package/dist/coding/ai-resolve/prompt.js +0 -161
- package/dist/coding/ai-resolve/routes.js +0 -97
- package/dist/coding/ai-resolve/run.js +0 -343
- package/dist/coding/ai-resolve/schema.js +0 -26
- package/dist/coding/ai-resolve/validate.js +0 -136
- package/dist/coding/checks/deny-scan.js +0 -75
- package/dist/coding/checks/discover.js +0 -541
- package/dist/coding/checks/docker.js +0 -38
- package/dist/coding/checks/env.js +0 -88
- package/dist/coding/checks/hooks.js +0 -91
- package/dist/coding/checks/index.js +0 -8
- package/dist/coding/checks/local.js +0 -40
- package/dist/coding/checks/sandbox.js +0 -206
- package/dist/coding/checks/session.js +0 -518
- package/dist/coding/checks/spawn.js +0 -123
- package/dist/coding/checks/spec.js +0 -140
- package/dist/coding/checks/types.js +0 -6
- package/dist/coding/partial-spend.js +0 -19
- package/dist/coding/safe-path.js +0 -75
- package/dist/coding/story-build/build-prompts.js +0 -178
- package/dist/coding/story-build/build.js +0 -332
- package/dist/coding/story-build/checkouts.js +0 -66
- package/dist/coding/story-build/docs-runner.js +0 -178
- package/dist/coding/story-build/docs.js +0 -334
- package/dist/coding/story-build/manager.js +0 -999
- package/dist/coding/story-build/plan-agent.js +0 -190
- package/dist/coding/story-build/plan.js +0 -338
- package/dist/coding/story-build/prompts.js +0 -147
- package/dist/coding/story-build/routes-docs.js +0 -113
- package/dist/coding/story-build/routes.js +0 -295
- package/dist/coding/story-build/schema.js +0 -53
- package/dist/db/automation-output.js +0 -349
- package/dist/db/credits.js +0 -79
- package/dist/db/schema.js +0 -321
- package/dist/db/schema.pg.js +0 -2197
- package/dist/db/sprint-repo-suggest.js +0 -257
- package/dist/db/sprint-stories.js +0 -42
- package/dist/db/sprint-story.js +0 -505
- package/dist/db/sprint-ticket-repos.js +0 -195
- package/dist/db/story-selection.js +0 -270
- package/dist/db/team-comparison.js +0 -124
- package/dist/db/workspace-comparison.js +0 -98
- package/dist/feed/fyi-provider.js +0 -10
- package/dist/github/local-user.js +0 -92
- package/dist/github/repo-files.js +0 -26
- package/dist/ml/worker.js +0 -31
- package/dist/platform/advisor/discovery/tier2.js +0 -88
- package/dist/platform/advisor/discovery/tier3.js +0 -57
- package/dist/platform/advisor/emitters/adapter.js +0 -5
- package/dist/platform/advisor/emitters/coderabbit.js +0 -175
- package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
- package/dist/platform/advisor/emitters/greptile.js +0 -176
- package/dist/platform/advisor/emitters/lint.js +0 -37
- package/dist/platform/advisor/emitters/manifest.js +0 -239
- package/dist/platform/advisor/emitters/prose.js +0 -183
- package/dist/platform/advisor/emitters/registry.js +0 -20
- package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
- package/dist/platform/advisor/evidence.js +0 -79
- package/dist/platform/advisor/findings.js +0 -15
- package/dist/platform/advisor/intents.js +0 -269
- package/dist/platform/advisor/measure/effect.js +0 -18
- package/dist/platform/advisor/outputs/brief.js +0 -68
- package/dist/platform/advisor/outputs/config-pr.js +0 -156
- package/dist/platform/advisor/refine/diff-guard.js +0 -38
- package/dist/platform/advisor/refine/refine.js +0 -35
- package/dist/platform/advisor/retro-check.js +0 -98
- package/dist/platform/advisor/routes.js +0 -648
- package/dist/platform/advisor/store.js +0 -321
- package/dist/platform/advisor/taxonomy.js +0 -67
- package/dist/platform/annotations/prompts.js +0 -84
- package/dist/platform/annotations/routes.js +0 -86
- package/dist/platform/annotations/runner.js +0 -234
- package/dist/platform/annotations/targets.js +0 -203
- package/dist/platform/insights/grounded-figures.js +0 -162
- package/dist/platform/insights/period-questions.js +0 -235
- package/dist/platform/work-plan/hash.js +0 -99
- package/dist/platform/work-plan/prompt.js +0 -209
- package/dist/platform/work-plan/routes.js +0 -350
- package/dist/pro/bind.js +0 -309
- package/dist/pro/contract.js +0 -36
- package/dist/pro/event-hooks.js +0 -37
- package/dist/pro/migrate.js +0 -64
- package/dist/review/events.js +0 -24
- package/dist/review/local-settings.js +0 -78
- package/dist/review/memory/capture.js +0 -0
- package/dist/review/memory/retrieval.js +0 -272
- package/dist/review/memory/routes.js +0 -78
- package/dist/review/persist.js +0 -203
- package/dist/review/plugin-providers.js +0 -12
- package/dist/review/prompt.js +0 -264
- package/dist/review/review-manager.js +0 -259
- package/dist/review/routing.js +0 -164
- package/dist/review/sprint-assess/agent.js +0 -145
- package/dist/review/sprint-assess/manager.js +0 -382
- package/dist/review/sprint-assess/prompts.js +0 -99
- package/dist/review/sprint-assess/routes.js +0 -109
- package/dist/review/sprint-assess/schema.js +0 -70
- package/dist/review/sprint-assess/validate.js +0 -180
- package/dist/review/sprint-story/manager.js +0 -98
- package/dist/review/sprint-story/prompt.js +0 -45
- package/dist/review/sprint-story/validate.js +0 -127
- package/dist/review/story-docs/build-prompts.js +0 -153
- package/dist/review/story-docs/build-validate.js +0 -162
- package/dist/review/story-report/agent.js +0 -203
- package/dist/review/story-report/fingerprint.js +0 -45
- package/dist/review/story-report/manager.js +0 -287
- package/dist/review/story-report/routes.js +0 -154
- package/dist/review/story-report/story-detail.js +0 -148
- package/dist/review/story-report/validate.js +0 -105
- package/dist/review/ticket-review/sweep-waits.js +0 -20
- package/dist/review/write-deny.js +0 -170
- package/dist/shared/conflict-ai.js +0 -24
- package/dist/sync/benchmark-rollup.js +0 -50
- package/public/assets/index-CbEfmqOD.css +0 -1
- package/public/assets/index-DBK4RWQu.js +0 -136
|
@@ -1366,6 +1366,34 @@
|
|
|
1366
1366
|
"when": 1793520000000,
|
|
1367
1367
|
"tag": "0194_story_build_claims",
|
|
1368
1368
|
"breakpoints": true
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
"idx": 195,
|
|
1372
|
+
"version": "6",
|
|
1373
|
+
"when": 1793606400000,
|
|
1374
|
+
"tag": "0195_releases",
|
|
1375
|
+
"breakpoints": true
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
"idx": 196,
|
|
1379
|
+
"version": "6",
|
|
1380
|
+
"when": 1793692800000,
|
|
1381
|
+
"tag": "0196_story_build_hotfix",
|
|
1382
|
+
"breakpoints": true
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
"idx": 197,
|
|
1386
|
+
"version": "6",
|
|
1387
|
+
"when": 1793779200000,
|
|
1388
|
+
"tag": "0197_release_items_live_tag",
|
|
1389
|
+
"breakpoints": true
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
"idx": 198,
|
|
1393
|
+
"version": "6",
|
|
1394
|
+
"when": 1793865600000,
|
|
1395
|
+
"tag": "0198_story_build_port_report",
|
|
1396
|
+
"breakpoints": true
|
|
1369
1397
|
}
|
|
1370
1398
|
]
|
|
1371
1399
|
}
|
package/dist/db/queries.js
CHANGED
|
@@ -491,6 +491,13 @@ export async function deleteWorkspace(id, accountId) {
|
|
|
491
491
|
// 1b. EXTRA External memberships (migration 0191). The deleted workspace's own rows go (the
|
|
492
492
|
// FK cascades too); a repo re-homed to Default by step 1 loses any extra row it had THERE,
|
|
493
493
|
// because a repo is never External in its own home.
|
|
494
|
+
// RELEASES (migration 0195): the workspace's releases follow its repos to Default — a release is
|
|
495
|
+
// history of those repos' tags, and the cascade would delete it.
|
|
496
|
+
await tx
|
|
497
|
+
.update(schema.releases)
|
|
498
|
+
.set({ workspaceId: defaultId })
|
|
499
|
+
.where(and(eq(schema.releases.accountId, accountId), eq(schema.releases.workspaceId, id)))
|
|
500
|
+
.execute();
|
|
494
501
|
await tx
|
|
495
502
|
.delete(workspaceExternalRepos)
|
|
496
503
|
.where(and(eq(workspaceExternalRepos.accountId, accountId), eq(workspaceExternalRepos.workspaceId, id)))
|
|
@@ -8205,6 +8212,20 @@ export async function deleteRepo(id, accountId) {
|
|
|
8205
8212
|
// nothing else, but a surviving row would keep this repo's file paths after the repo is gone
|
|
8206
8213
|
// AND, on Postgres, block the `repos` delete outright.
|
|
8207
8214
|
await tx.delete(schema.repoFileCoupling).where(eq(schema.repoFileCoupling.repoId, id)).execute();
|
|
8215
|
+
// RELEASES (migration 0195): the repo's tags, their ranges and its release items (the release
|
|
8216
|
+
// rows themselves are the workspace's and stay). Their repo FKs cascade; explicit all the same.
|
|
8217
|
+
await tx
|
|
8218
|
+
.delete(schema.repoTagRanges)
|
|
8219
|
+
.where(and(eq(schema.repoTagRanges.repoId, id), eq(schema.repoTagRanges.accountId, accountId)))
|
|
8220
|
+
.execute();
|
|
8221
|
+
await tx
|
|
8222
|
+
.delete(schema.repoTags)
|
|
8223
|
+
.where(and(eq(schema.repoTags.repoId, id), eq(schema.repoTags.accountId, accountId)))
|
|
8224
|
+
.execute();
|
|
8225
|
+
await tx
|
|
8226
|
+
.delete(schema.releaseItems)
|
|
8227
|
+
.where(and(eq(schema.releaseItems.repoId, id), eq(schema.releaseItems.accountId, accountId)))
|
|
8228
|
+
.execute();
|
|
8208
8229
|
// Docs folders offered from this repo (migration 0155). The composite FK cascades; explicit
|
|
8209
8230
|
// anyway, for the same stated reason as the rows above.
|
|
8210
8231
|
await tx
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// THE RELEASE CHECK'S DB HALF (docs/RELEASES.md § The release check) — gathers the stored inputs of
|
|
2
|
+
// shared `foldReleaseCheck` and answers GET /api/releases. DB ONLY: never a GitHub or tracker call.
|
|
3
|
+
//
|
|
4
|
+
// • Repos: the workspace's members (`resolveWorkspaceScope`), their stored tags and range increments
|
|
5
|
+
// (db/repo-tags.ts), `writable` from the synced `viewerPermission` (the ONE WRITE_PERMISSIONS set).
|
|
6
|
+
// • PRs: every PR in a stored range, plus every PR (in the workspace's repos) on a ticket one of them
|
|
7
|
+
// names — the ships-whole and open-work rules need the ticket's PRs everywhere.
|
|
8
|
+
// • Tickets: `tracker_tickets` rows ON THE WORKSPACE'S TRACKER SITE only (a row of another site is a
|
|
9
|
+
// ticket of a tracker the workspace no longer uses: not a link). The best row per ticket is the
|
|
10
|
+
// newest successful read; a fresher `sprint_tickets` row's status wins. DONE = the This Sprint
|
|
11
|
+
// stage resolver (`sprintStageOf` over `sprintLayoutFor`) says Complete — never a second resolver.
|
|
12
|
+
// A range PR with a ticket key THE detection rule finds but no stored row yet is `not_read` (blocks).
|
|
13
|
+
// • A 'history' range (the oldest tag) is read fresh from the stored PRs, never from its frozen row.
|
|
14
|
+
// • Bot-ness: `globalAutomationUserIds`, never the hidden-bots list.
|
|
15
|
+
// • Accepted repos: `sprint_ticket_repos` (the reader's accepted repos per ticket).
|
|
16
|
+
import { and, desc, eq, inArray, isNotNull, lte, or } from 'drizzle-orm';
|
|
17
|
+
import { foldReleaseCheck, releaseSettingsConfigured, tagKind, trackerTicketIdent, } from '../shared/index.js';
|
|
18
|
+
import { globalAutomationUserIds } from './automation-ids.js';
|
|
19
|
+
import { db, schema } from './client.js';
|
|
20
|
+
import { WRITE_PERMISSIONS, resolveWorkspaceScope } from './queries.js';
|
|
21
|
+
import { getReleaseSettings } from './release-settings.js';
|
|
22
|
+
import { getRepoTags, getTagRanges } from './repo-tags.js';
|
|
23
|
+
import { sprintLayoutFor, sprintStageOf } from './sprint-status-layout.js';
|
|
24
|
+
const { repos, pullRequests, trackerTickets, sprintTickets, sprintTicketRepos, releases } = schema;
|
|
25
|
+
const CHUNK = 400;
|
|
26
|
+
function chunks(xs, n = CHUNK) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let i = 0; i < xs.length; i += n)
|
|
29
|
+
out.push(xs.slice(i, i + n));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** Done = the workspace's Complete stage, through THE This Sprint stage resolver. With no stored
|
|
33
|
+
* layout (the sprint was never read) the resolver's category fallback decides (`done` → Complete,
|
|
34
|
+
* so "Won't do" counts). Pure. */
|
|
35
|
+
export function ticketIsDone(layout, statusName, statusCategory) {
|
|
36
|
+
if (statusName == null && statusCategory == null)
|
|
37
|
+
return false;
|
|
38
|
+
return sprintStageOf(layout ?? { order: [] }, { name: statusName ?? '', category: statusCategory }) === 'done';
|
|
39
|
+
}
|
|
40
|
+
const READ_STATES = new Set(['ok', 'not_found', 'no_access', 'failed']);
|
|
41
|
+
/** The ticket from its rows (one per PR): the newest successful read, else the newest attempt. Pure. */
|
|
42
|
+
export function bestTicketRow(rows) {
|
|
43
|
+
const ms = (d) => (d == null ? 0 : d.getTime());
|
|
44
|
+
const ok = rows.filter((r) => r.state === 'ok' && r.fetchedAt != null).sort((a, b) => ms(b.fetchedAt) - ms(a.fetchedAt));
|
|
45
|
+
if (ok[0] != null)
|
|
46
|
+
return ok[0];
|
|
47
|
+
return [...rows].sort((a, b) => ms(b.checkedAt) - ms(a.checkedAt))[0] ?? null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* THE ONE detection rule (tracker/enricher.ts `detectKeysWithAccess`: title + head branch + stored
|
|
51
|
+
* links) over these PRs, against the WORKSPACE's tracker: prId → keys. DB only (no tracker call). A
|
|
52
|
+
* key detected on a range PR with no stored row is a ticket the worker has not read yet — it BLOCKS
|
|
53
|
+
* (`not_read`), never passes as an untracked PR.
|
|
54
|
+
*/
|
|
55
|
+
async function detectedKeysByPr(accountId, workspaceId, prIds) {
|
|
56
|
+
const out = new Map();
|
|
57
|
+
if (prIds.length === 0)
|
|
58
|
+
return out;
|
|
59
|
+
// LAZY: tracker/runtime.ts opens the database at import.
|
|
60
|
+
const [{ trackerContext }, { readWorkspaceTrackerRow, trackerAccessOf }, { detectKeysWithAccess, withParsedLinks, prLinkColumns }] = await Promise.all([import('../tracker/runtime.js'), import('../tracker/settings.js'), import('../tracker/enricher.js')]);
|
|
61
|
+
const ctx = trackerContext();
|
|
62
|
+
const access = trackerAccessOf(ctx.host, await readWorkspaceTrackerRow(ctx, accountId, workspaceId));
|
|
63
|
+
for (const part of chunks(prIds)) {
|
|
64
|
+
const rows = await db
|
|
65
|
+
.select({ id: pullRequests.id, title: pullRequests.title, headRefName: pullRequests.headRefName, ...prLinkColumns(pullRequests) })
|
|
66
|
+
.from(pullRequests)
|
|
67
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.id, part)))
|
|
68
|
+
.execute();
|
|
69
|
+
for (const r of rows) {
|
|
70
|
+
const keys = detectKeysWithAccess(access, withParsedLinks(r));
|
|
71
|
+
if (keys != null && keys.length > 0)
|
|
72
|
+
out.set(r.id, keys);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A 'history' range (the oldest tag: no base) is read FRESH from the stored PRs, never from its frozen
|
|
79
|
+
* row: PRs the deep backfill brings in after the row was computed must enter it. Merged into the
|
|
80
|
+
* default branch no later than the tag's commit (or by the tag's own commit). prIds per tag name.
|
|
81
|
+
*/
|
|
82
|
+
async function historyPrIds(accountId, repoId, defaultBranch, tag) {
|
|
83
|
+
const rows = await db
|
|
84
|
+
.select({ id: pullRequests.id })
|
|
85
|
+
.from(pullRequests)
|
|
86
|
+
.where(and(eq(pullRequests.accountId, accountId), eq(pullRequests.repoId, repoId), eq(pullRequests.state, 'merged'), eq(pullRequests.baseRefName, defaultBranch), or(eq(pullRequests.mergeCommitOid, tag.sha), and(isNotNull(pullRequests.mergedAt), lte(pullRequests.mergedAt, tag.committedAt)))))
|
|
87
|
+
.execute();
|
|
88
|
+
return [...new Set(rows.map((r) => r.id))].sort((a, b) => a - b);
|
|
89
|
+
}
|
|
90
|
+
/** Every stored input of the fold for one workspace (the caller checked the settings are set). */
|
|
91
|
+
export async function loadReleaseCheckInput(accountId, workspaceId, repoIds, settings) {
|
|
92
|
+
const repoRows = repoIds.length === 0
|
|
93
|
+
? []
|
|
94
|
+
: await db
|
|
95
|
+
.select({
|
|
96
|
+
id: repos.id,
|
|
97
|
+
owner: repos.owner,
|
|
98
|
+
name: repos.name,
|
|
99
|
+
defaultBranch: repos.defaultBranch,
|
|
100
|
+
defaultBranchName: repos.defaultBranchName,
|
|
101
|
+
viewerPermission: repos.viewerPermission,
|
|
102
|
+
})
|
|
103
|
+
.from(repos)
|
|
104
|
+
.where(and(eq(repos.accountId, accountId), inArray(repos.id, [...repoIds])))
|
|
105
|
+
.execute();
|
|
106
|
+
const ids = repoRows.map((r) => r.id);
|
|
107
|
+
const [tags, storedRanges] = await Promise.all([getRepoTags(accountId, ids), getTagRanges(accountId, ids)]);
|
|
108
|
+
const repoRowById = new Map(repoRows.map((r) => [r.id, r]));
|
|
109
|
+
const ranges = await Promise.all(storedRanges.map(async (r) => {
|
|
110
|
+
if (r.method !== 'history')
|
|
111
|
+
return r;
|
|
112
|
+
const tag = tags.find((t) => t.repoId === r.repoId && t.name === r.tagName);
|
|
113
|
+
const row = repoRowById.get(r.repoId);
|
|
114
|
+
const def = row?.defaultBranch ?? row?.defaultBranchName ?? null;
|
|
115
|
+
if (tag == null || tag.sha !== r.sha || tag.committedAt == null || def == null)
|
|
116
|
+
return r;
|
|
117
|
+
return { ...r, prIds: await historyPrIds(accountId, r.repoId, def, { sha: tag.sha, committedAt: tag.committedAt }), unresolvedPrNumbers: [] };
|
|
118
|
+
}));
|
|
119
|
+
// The workspace's tracker site (LAZY: db/stories.ts reaches the tracker runtime).
|
|
120
|
+
const { workspaceSite } = await import('./stories.js');
|
|
121
|
+
const site = await workspaceSite(accountId, workspaceId);
|
|
122
|
+
const rangePrIds = [...new Set(ranges.flatMap((r) => r.prIds))];
|
|
123
|
+
const ticketRows = [];
|
|
124
|
+
const detectedOnly = new Map();
|
|
125
|
+
const ticketSelect = {
|
|
126
|
+
prId: trackerTickets.prId,
|
|
127
|
+
issueKey: trackerTickets.issueKey,
|
|
128
|
+
url: trackerTickets.url,
|
|
129
|
+
state: trackerTickets.state,
|
|
130
|
+
title: trackerTickets.title,
|
|
131
|
+
statusName: trackerTickets.statusName,
|
|
132
|
+
statusCategory: trackerTickets.statusCategory,
|
|
133
|
+
fetchedAt: trackerTickets.fetchedAt,
|
|
134
|
+
checkedAt: trackerTickets.checkedAt,
|
|
135
|
+
};
|
|
136
|
+
if (site != null) {
|
|
137
|
+
for (const part of chunks(rangePrIds)) {
|
|
138
|
+
ticketRows.push(...(await db
|
|
139
|
+
.select(ticketSelect)
|
|
140
|
+
.from(trackerTickets)
|
|
141
|
+
.where(and(eq(trackerTickets.accountId, accountId), eq(trackerTickets.provider, site.provider), eq(trackerTickets.apiRoot, site.apiRoot), inArray(trackerTickets.prId, part)))
|
|
142
|
+
.execute()));
|
|
143
|
+
}
|
|
144
|
+
// Keys the detection rule finds on a range PR but no stored row answers for that PR yet.
|
|
145
|
+
const rowKeys = new Map();
|
|
146
|
+
for (const r of ticketRows)
|
|
147
|
+
rowKeys.set(r.prId, (rowKeys.get(r.prId) ?? new Set()).add(r.issueKey));
|
|
148
|
+
for (const [prId, keys] of await detectedKeysByPr(accountId, workspaceId, rangePrIds)) {
|
|
149
|
+
const missing = keys.filter((k) => !(rowKeys.get(prId)?.has(k) ?? false));
|
|
150
|
+
if (missing.length > 0)
|
|
151
|
+
detectedOnly.set(prId, missing);
|
|
152
|
+
}
|
|
153
|
+
// Every PR on those tickets (the ticket's other repos, its open work).
|
|
154
|
+
const keys = [...new Set([...ticketRows.map((r) => r.issueKey), ...[...detectedOnly.values()].flat()])];
|
|
155
|
+
const have = new Set(ticketRows.map((r) => `${r.prId}#${r.issueKey}`));
|
|
156
|
+
for (const part of chunks(keys)) {
|
|
157
|
+
const more = await db
|
|
158
|
+
.select(ticketSelect)
|
|
159
|
+
.from(trackerTickets)
|
|
160
|
+
.where(and(eq(trackerTickets.accountId, accountId), eq(trackerTickets.provider, site.provider), eq(trackerTickets.apiRoot, site.apiRoot), inArray(trackerTickets.issueKey, part)))
|
|
161
|
+
.execute();
|
|
162
|
+
for (const r of more)
|
|
163
|
+
if (!have.has(`${r.prId}#${r.issueKey}`))
|
|
164
|
+
ticketRows.push(r);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// The PRs, within the workspace's repos only.
|
|
168
|
+
const prIdSet = [...new Set([...rangePrIds, ...ticketRows.map((r) => r.prId)])];
|
|
169
|
+
const prRows = [];
|
|
170
|
+
if (ids.length > 0) {
|
|
171
|
+
for (const part of chunks(prIdSet)) {
|
|
172
|
+
prRows.push(...(await db
|
|
173
|
+
.select({
|
|
174
|
+
id: pullRequests.id,
|
|
175
|
+
repoId: pullRequests.repoId,
|
|
176
|
+
number: pullRequests.number,
|
|
177
|
+
title: pullRequests.title,
|
|
178
|
+
state: pullRequests.state,
|
|
179
|
+
baseRefName: pullRequests.baseRefName,
|
|
180
|
+
mergedAt: pullRequests.mergedAt,
|
|
181
|
+
authorId: pullRequests.authorId,
|
|
182
|
+
})
|
|
183
|
+
.from(pullRequests)
|
|
184
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, ids), inArray(pullRequests.id, part)))
|
|
185
|
+
.execute()));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const bots = prRows.some((p) => p.authorId != null) ? await globalAutomationUserIds() : new Set();
|
|
189
|
+
const identOf = (issueKey) => trackerTicketIdent(site.provider, site.apiRoot, issueKey);
|
|
190
|
+
const identsByPr = new Map();
|
|
191
|
+
const rowsByIdent = new Map();
|
|
192
|
+
for (const r of ticketRows) {
|
|
193
|
+
const ident = identOf(r.issueKey);
|
|
194
|
+
const list = identsByPr.get(r.prId) ?? [];
|
|
195
|
+
if (!list.includes(ident))
|
|
196
|
+
list.push(ident);
|
|
197
|
+
identsByPr.set(r.prId, list);
|
|
198
|
+
rowsByIdent.set(ident, [...(rowsByIdent.get(ident) ?? []), r]);
|
|
199
|
+
}
|
|
200
|
+
/** Detected on a range PR, no stored row anywhere on the site: ident → key (read state `not_read`). */
|
|
201
|
+
const unreadKeys = new Map();
|
|
202
|
+
for (const [prId, keys] of detectedOnly) {
|
|
203
|
+
for (const key of keys) {
|
|
204
|
+
const ident = identOf(key);
|
|
205
|
+
const list = identsByPr.get(prId) ?? [];
|
|
206
|
+
if (!list.includes(ident))
|
|
207
|
+
list.push(ident);
|
|
208
|
+
identsByPr.set(prId, list);
|
|
209
|
+
if (!rowsByIdent.has(ident))
|
|
210
|
+
unreadKeys.set(ident, key);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const repoById = new Map(repoRows.map((r) => [r.id, r]));
|
|
214
|
+
const defaultOf = (repoId) => {
|
|
215
|
+
const r = repoById.get(repoId);
|
|
216
|
+
return r?.defaultBranch ?? r?.defaultBranchName ?? null;
|
|
217
|
+
};
|
|
218
|
+
const prs = prRows.map((p) => {
|
|
219
|
+
const r = repoById.get(p.repoId);
|
|
220
|
+
const def = defaultOf(p.repoId);
|
|
221
|
+
return {
|
|
222
|
+
id: p.id,
|
|
223
|
+
repoId: p.repoId,
|
|
224
|
+
number: p.number,
|
|
225
|
+
title: p.title,
|
|
226
|
+
url: `https://github.com/${r.owner}/${r.name}/pull/${p.number}`,
|
|
227
|
+
isBot: p.authorId != null && bots.has(p.authorId),
|
|
228
|
+
intoDefaultBranch: def != null && p.baseRefName === def,
|
|
229
|
+
state: p.state,
|
|
230
|
+
mergedAt: p.mergedAt,
|
|
231
|
+
ticketIdents: identsByPr.get(p.id) ?? [],
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
// The tickets: best stored read, a fresher sprint read's status wins, Done by the stage resolver.
|
|
235
|
+
const idents = [...rowsByIdent.keys(), ...unreadKeys.keys()];
|
|
236
|
+
const sprintByIdent = new Map();
|
|
237
|
+
const accepted = [];
|
|
238
|
+
for (const part of chunks(idents)) {
|
|
239
|
+
const [srows, arows] = await Promise.all([
|
|
240
|
+
db
|
|
241
|
+
.select({
|
|
242
|
+
ident: sprintTickets.ident,
|
|
243
|
+
statusName: sprintTickets.statusName,
|
|
244
|
+
statusCategory: sprintTickets.statusCategory,
|
|
245
|
+
fetchedAt: sprintTickets.fetchedAt,
|
|
246
|
+
title: sprintTickets.title,
|
|
247
|
+
url: sprintTickets.url,
|
|
248
|
+
})
|
|
249
|
+
.from(sprintTickets)
|
|
250
|
+
.where(and(eq(sprintTickets.accountId, accountId), eq(sprintTickets.workspaceId, workspaceId), inArray(sprintTickets.ident, part)))
|
|
251
|
+
.execute(),
|
|
252
|
+
db
|
|
253
|
+
.select({ ident: sprintTicketRepos.ident, repoId: sprintTicketRepos.repoId })
|
|
254
|
+
.from(sprintTicketRepos)
|
|
255
|
+
.where(and(eq(sprintTicketRepos.accountId, accountId), eq(sprintTicketRepos.workspaceId, workspaceId), inArray(sprintTicketRepos.ident, part)))
|
|
256
|
+
.execute(),
|
|
257
|
+
]);
|
|
258
|
+
for (const s of srows)
|
|
259
|
+
sprintByIdent.set(s.ident, s);
|
|
260
|
+
accepted.push(...arows.filter((a) => repoById.has(a.repoId)));
|
|
261
|
+
}
|
|
262
|
+
const layout = idents.length > 0 ? await sprintLayoutFor(accountId, workspaceId) : null;
|
|
263
|
+
const tickets = idents.map((ident) => {
|
|
264
|
+
const unreadKey = unreadKeys.get(ident);
|
|
265
|
+
if (unreadKey != null) {
|
|
266
|
+
// A sprint read of it is a real read of the ticket; with none, it is not read yet (blocks).
|
|
267
|
+
const sprint = sprintByIdent.get(ident);
|
|
268
|
+
if (sprint == null)
|
|
269
|
+
return { ident, key: unreadKey, title: null, url: null, statusName: null, done: false, readState: 'not_read' };
|
|
270
|
+
return {
|
|
271
|
+
ident,
|
|
272
|
+
key: unreadKey,
|
|
273
|
+
title: sprint.title,
|
|
274
|
+
url: sprint.url,
|
|
275
|
+
statusName: sprint.statusName,
|
|
276
|
+
done: ticketIsDone(layout, sprint.statusName, sprint.statusCategory),
|
|
277
|
+
readState: 'ok',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const rows = rowsByIdent.get(ident);
|
|
281
|
+
const best = bestTicketRow(rows);
|
|
282
|
+
const readState = best.state === 'ok' && best.fetchedAt != null
|
|
283
|
+
? 'ok'
|
|
284
|
+
: READ_STATES.has(best.state) && best.state !== 'ok'
|
|
285
|
+
? best.state
|
|
286
|
+
: 'not_read';
|
|
287
|
+
const sprint = sprintByIdent.get(ident);
|
|
288
|
+
const useSprint = sprint != null && (best.fetchedAt == null || sprint.fetchedAt.getTime() > best.fetchedAt.getTime());
|
|
289
|
+
const statusName = useSprint ? sprint.statusName : best.statusName;
|
|
290
|
+
const statusCategory = useSprint ? sprint.statusCategory : best.statusCategory;
|
|
291
|
+
return {
|
|
292
|
+
ident,
|
|
293
|
+
key: best.issueKey,
|
|
294
|
+
title: best.title ?? sprint?.title ?? null,
|
|
295
|
+
url: best.url ?? sprint?.url ?? null,
|
|
296
|
+
statusName,
|
|
297
|
+
done: readState === 'ok' && ticketIsDone(layout, statusName, statusCategory),
|
|
298
|
+
readState,
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
const tagsByRepo = new Map();
|
|
302
|
+
for (const t of tags)
|
|
303
|
+
tagsByRepo.set(t.repoId, [...(tagsByRepo.get(t.repoId) ?? []), t]);
|
|
304
|
+
const rangesByRepo = new Map();
|
|
305
|
+
for (const r of ranges)
|
|
306
|
+
rangesByRepo.set(r.repoId, [...(rangesByRepo.get(r.repoId) ?? []), r]);
|
|
307
|
+
return {
|
|
308
|
+
settings,
|
|
309
|
+
repos: [...repoRows]
|
|
310
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.id - b.id)
|
|
311
|
+
.map((r) => ({
|
|
312
|
+
repoId: r.id,
|
|
313
|
+
fullName: `${r.owner}/${r.name}`,
|
|
314
|
+
name: r.name,
|
|
315
|
+
writable: WRITE_PERMISSIONS.has(r.viewerPermission ?? ''),
|
|
316
|
+
tags: (tagsByRepo.get(r.id) ?? []).map((t) => ({
|
|
317
|
+
name: t.name,
|
|
318
|
+
sha: t.sha,
|
|
319
|
+
committedAt: t.committedAt,
|
|
320
|
+
taggedAt: t.taggedAt,
|
|
321
|
+
onDefaultBranch: t.onDefaultBranch,
|
|
322
|
+
moved: t.movedAt != null,
|
|
323
|
+
})),
|
|
324
|
+
ranges: (rangesByRepo.get(r.id) ?? []).map((x) => ({
|
|
325
|
+
tagName: x.tagName,
|
|
326
|
+
sha: x.sha,
|
|
327
|
+
baseTagName: x.baseTagName,
|
|
328
|
+
baseSha: x.baseSha,
|
|
329
|
+
method: x.method,
|
|
330
|
+
prIds: x.prIds,
|
|
331
|
+
unresolvedPrNumbers: x.unresolvedPrNumbers,
|
|
332
|
+
unmatchedCommits: x.unmatchedCommits,
|
|
333
|
+
truncated: x.truncated,
|
|
334
|
+
})),
|
|
335
|
+
})),
|
|
336
|
+
prs,
|
|
337
|
+
tickets,
|
|
338
|
+
acceptedRepos: accepted,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** GET /api/releases. `rawWorkspace` absent / unknown / unparseable ⇒ the Default workspace. */
|
|
342
|
+
export async function getReleases(accountId, rawWorkspace) {
|
|
343
|
+
const scope = await resolveWorkspaceScope(accountId, rawWorkspace);
|
|
344
|
+
const workspaceId = scope.workspaceId;
|
|
345
|
+
const settings = (await getReleaseSettings(accountId, workspaceId)) ?? { releaseSuffix: null, hotfixSuffix: null };
|
|
346
|
+
const latest = (await db
|
|
347
|
+
.select({ id: releases.id, createdAt: releases.createdAt, state: releases.state })
|
|
348
|
+
.from(releases)
|
|
349
|
+
.where(and(eq(releases.accountId, accountId), eq(releases.workspaceId, workspaceId)))
|
|
350
|
+
.orderBy(desc(releases.createdAt), desc(releases.id))
|
|
351
|
+
.limit(1)
|
|
352
|
+
.execute())[0];
|
|
353
|
+
const base = {
|
|
354
|
+
workspaceId,
|
|
355
|
+
configured: releaseSettingsConfigured(settings),
|
|
356
|
+
releaseSuffix: settings.releaseSuffix,
|
|
357
|
+
hotfixSuffix: settings.hotfixSuffix,
|
|
358
|
+
latestRelease: latest != null ? { id: latest.id, createdAt: latest.createdAt.toISOString(), state: latest.state } : null,
|
|
359
|
+
};
|
|
360
|
+
if (!base.configured)
|
|
361
|
+
return { ...base, latestReleaseTagAt: null, check: null };
|
|
362
|
+
const input = await loadReleaseCheckInput(accountId, workspaceId, scope.repoIds, settings);
|
|
363
|
+
let latestTagMs = null;
|
|
364
|
+
for (const r of input.repos) {
|
|
365
|
+
for (const t of r.tags) {
|
|
366
|
+
const k = tagKind(t.name, settings);
|
|
367
|
+
if ((k === 'release' || k === 'hotfix') && t.committedAt != null) {
|
|
368
|
+
latestTagMs = Math.max(latestTagMs ?? 0, t.committedAt.getTime());
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
...base,
|
|
374
|
+
latestReleaseTagAt: latestTagMs == null ? null : new Date(latestTagMs).toISOString(),
|
|
375
|
+
check: foldReleaseCheck(input),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
//# sourceMappingURL=release-check.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// RELEASE SETTINGS (docs/RELEASES.md § Terms and settings) — the ONE reader and writer of
|
|
2
|
+
// `workspaces.release_suffix` / `workspaces.hotfix_suffix` (migration 0195). Per workspace, no
|
|
3
|
+
// inheritance, NO default: null until the reader sets one. Callers validate with shared
|
|
4
|
+
// `validateReleaseSettings` first (the route does; the Settings form runs the same function).
|
|
5
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
6
|
+
import { normaliseReleaseSuffix, releaseSettingsConfigured, } from '../shared/index.js';
|
|
7
|
+
import { db, schema } from './client.js';
|
|
8
|
+
const { workspaces, workspaceRepos, repoTags, releases } = schema;
|
|
9
|
+
/** The workspace's settings, RAW (null = not set); null when the workspace is not this account's. */
|
|
10
|
+
export async function getReleaseSettings(accountId, workspaceId) {
|
|
11
|
+
const row = (await db
|
|
12
|
+
.select({ releaseSuffix: workspaces.releaseSuffix, hotfixSuffix: workspaces.hotfixSuffix })
|
|
13
|
+
.from(workspaces)
|
|
14
|
+
.where(and(eq(workspaces.id, workspaceId), eq(workspaces.accountId, accountId)))
|
|
15
|
+
.limit(1)
|
|
16
|
+
.execute())[0];
|
|
17
|
+
if (!row)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
releaseSuffix: normaliseReleaseSuffix(row.releaseSuffix),
|
|
21
|
+
hotfixSuffix: normaliseReleaseSuffix(row.hotfixSuffix),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Every workspace's settings of this account that has BOTH suffixes set (the range planner keeps
|
|
25
|
+
* every range after the oldest baseline any of them gives). */
|
|
26
|
+
export async function configuredReleaseSettings(accountId) {
|
|
27
|
+
const rows = await db
|
|
28
|
+
.select({ releaseSuffix: workspaces.releaseSuffix, hotfixSuffix: workspaces.hotfixSuffix })
|
|
29
|
+
.from(workspaces)
|
|
30
|
+
.where(eq(workspaces.accountId, accountId))
|
|
31
|
+
.execute();
|
|
32
|
+
return rows
|
|
33
|
+
.map((r) => ({ releaseSuffix: normaliseReleaseSuffix(r.releaseSuffix), hotfixSuffix: normaliseReleaseSuffix(r.hotfixSuffix) }))
|
|
34
|
+
.filter(releaseSettingsConfigured);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Replace both suffixes (an empty string stores NULL). Returns false when the workspace is not this
|
|
38
|
+
* account's. Does NOT validate — the caller already ran `validateReleaseSettings`.
|
|
39
|
+
*/
|
|
40
|
+
export async function setReleaseSettings(accountId, workspaceId, settings) {
|
|
41
|
+
const rows = await db
|
|
42
|
+
.update(workspaces)
|
|
43
|
+
.set({
|
|
44
|
+
releaseSuffix: normaliseReleaseSuffix(settings.releaseSuffix),
|
|
45
|
+
hotfixSuffix: normaliseReleaseSuffix(settings.hotfixSuffix),
|
|
46
|
+
})
|
|
47
|
+
.where(and(eq(workspaces.id, workspaceId), eq(workspaces.accountId, accountId)))
|
|
48
|
+
.returning({ id: workspaces.id })
|
|
49
|
+
.execute();
|
|
50
|
+
return rows.length > 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A release already exists for this workspace: a release row, or a stored tag in one of its repos
|
|
54
|
+
* that ends in the CURRENT release suffix (a team may have tagged by hand before Ensemble). The Settings
|
|
55
|
+
* form warns before a change. DB only.
|
|
56
|
+
*/
|
|
57
|
+
export async function workspaceHasReleases(accountId, workspaceId, releaseSuffix) {
|
|
58
|
+
const made = await db
|
|
59
|
+
.select({ id: releases.id })
|
|
60
|
+
.from(releases)
|
|
61
|
+
.where(and(eq(releases.accountId, accountId), eq(releases.workspaceId, workspaceId)))
|
|
62
|
+
.limit(1)
|
|
63
|
+
.execute();
|
|
64
|
+
if (made.length > 0)
|
|
65
|
+
return true;
|
|
66
|
+
if (releaseSuffix == null)
|
|
67
|
+
return false;
|
|
68
|
+
const repoIds = (await db
|
|
69
|
+
.select({ repoId: workspaceRepos.repoId })
|
|
70
|
+
.from(workspaceRepos)
|
|
71
|
+
.where(and(eq(workspaceRepos.accountId, accountId), eq(workspaceRepos.workspaceId, workspaceId)))
|
|
72
|
+
.execute()).map((r) => r.repoId);
|
|
73
|
+
if (repoIds.length === 0)
|
|
74
|
+
return false;
|
|
75
|
+
const names = await db
|
|
76
|
+
.select({ name: repoTags.name })
|
|
77
|
+
.from(repoTags)
|
|
78
|
+
.where(and(eq(repoTags.accountId, accountId), inArray(repoTags.repoId, repoIds)))
|
|
79
|
+
.execute();
|
|
80
|
+
return names.some((n) => n.name.length > releaseSuffix.length && n.name.endsWith(releaseSuffix));
|
|
81
|
+
}
|
|
82
|
+
/** The wire shape for GET / PUT /api/workspaces/:id/release-settings; null = not this account's. */
|
|
83
|
+
export async function getReleaseSettingsResponse(accountId, workspaceId) {
|
|
84
|
+
const settings = await getReleaseSettings(accountId, workspaceId);
|
|
85
|
+
if (!settings)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
workspaceId,
|
|
89
|
+
...settings,
|
|
90
|
+
configured: releaseSettingsConfigured(settings),
|
|
91
|
+
hasReleases: await workspaceHasReleases(accountId, workspaceId, settings.releaseSuffix),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=release-settings.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// RELEASES' READS of the synced tags and tag ranges (docs/RELEASES.md) — DB only, account-scoped.
|
|
2
|
+
// The release check (a pure fold, next to the Releases routes) builds on these two:
|
|
3
|
+
// getRepoTags(accountId, repoIds) every stored tag of those repos
|
|
4
|
+
// getTagRanges(accountId, repoIds) every stored range increment, PR NUMBERS resolved to local
|
|
5
|
+
// PR ids within (account, repo) — a number not stored yet is
|
|
6
|
+
// kept in `unresolvedPrNumbers`, never dropped.
|
|
7
|
+
// The writers are sync/repo-tags.ts and sync/tag-ranges.ts.
|
|
8
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
9
|
+
import { db, schema } from './client.js';
|
|
10
|
+
const { repoTags, repoTagRanges, pullRequests } = schema;
|
|
11
|
+
export async function getRepoTags(accountId, repoIds) {
|
|
12
|
+
if (repoIds.length === 0)
|
|
13
|
+
return [];
|
|
14
|
+
const rows = await db
|
|
15
|
+
.select({
|
|
16
|
+
repoId: repoTags.repoId,
|
|
17
|
+
name: repoTags.name,
|
|
18
|
+
sha: repoTags.sha,
|
|
19
|
+
tagObjectOid: repoTags.tagObjectOid,
|
|
20
|
+
committedAt: repoTags.committedAt,
|
|
21
|
+
taggedAt: repoTags.taggedAt,
|
|
22
|
+
onDefaultBranch: repoTags.onDefaultBranch,
|
|
23
|
+
previousSha: repoTags.previousSha,
|
|
24
|
+
movedAt: repoTags.movedAt,
|
|
25
|
+
firstSeenAt: repoTags.firstSeenAt,
|
|
26
|
+
})
|
|
27
|
+
.from(repoTags)
|
|
28
|
+
.where(and(eq(repoTags.accountId, accountId), inArray(repoTags.repoId, [...repoIds])))
|
|
29
|
+
.execute();
|
|
30
|
+
return rows.map(({ tagObjectOid, ...r }) => ({ ...r, annotated: tagObjectOid != null }));
|
|
31
|
+
}
|
|
32
|
+
export async function getTagRanges(accountId, repoIds) {
|
|
33
|
+
if (repoIds.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const rows = await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(repoTagRanges)
|
|
38
|
+
.where(and(eq(repoTagRanges.accountId, accountId), inArray(repoTagRanges.repoId, [...repoIds])))
|
|
39
|
+
.execute();
|
|
40
|
+
if (rows.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
// number → id, per repo. ONE read of the repos' PR numbers (indexed on account, repo, number).
|
|
43
|
+
const wanted = [...new Set(rows.flatMap((r) => r.prNumbers ?? []))];
|
|
44
|
+
const idByKey = new Map();
|
|
45
|
+
for (let i = 0; i < wanted.length; i += 400) {
|
|
46
|
+
const part = wanted.slice(i, i + 400);
|
|
47
|
+
const prs = await db
|
|
48
|
+
.select({ id: pullRequests.id, repoId: pullRequests.repoId, number: pullRequests.number })
|
|
49
|
+
.from(pullRequests)
|
|
50
|
+
.where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, [...repoIds]), inArray(pullRequests.number, part)))
|
|
51
|
+
.execute();
|
|
52
|
+
for (const p of prs)
|
|
53
|
+
idByKey.set(`${p.repoId}#${p.number}`, p.id);
|
|
54
|
+
}
|
|
55
|
+
return rows.map((r) => {
|
|
56
|
+
const prIds = [];
|
|
57
|
+
const unresolvedPrNumbers = [];
|
|
58
|
+
for (const n of r.prNumbers ?? []) {
|
|
59
|
+
const id = idByKey.get(`${r.repoId}#${n}`);
|
|
60
|
+
if (id != null)
|
|
61
|
+
prIds.push(id);
|
|
62
|
+
else
|
|
63
|
+
unresolvedPrNumbers.push(n);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
repoId: r.repoId,
|
|
67
|
+
tagName: r.tagName,
|
|
68
|
+
sha: r.sha,
|
|
69
|
+
baseTagName: r.baseTagName,
|
|
70
|
+
baseSha: r.baseSha,
|
|
71
|
+
method: r.method,
|
|
72
|
+
prIds,
|
|
73
|
+
unresolvedPrNumbers,
|
|
74
|
+
commitCount: r.commitCount,
|
|
75
|
+
unmatchedCommits: r.unmatchedCommits,
|
|
76
|
+
truncated: r.truncated,
|
|
77
|
+
movedFromSha: r.movedFromSha,
|
|
78
|
+
computedAt: r.computedAt,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=repo-tags.js.map
|