Skip to content

Commit 975e26d

Browse files
authored
Merge branch 'main' into release-please--branches--main--groups--engine-and-dependents
2 parents bc8e751 + c4eb125 commit 975e26d

4 files changed

Lines changed: 214 additions & 0 deletions

File tree

.github/workflows/mcp-release-please.yml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,127 @@ jobs:
192192
env:
193193
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
194194
run: gh workflow run publish-ui-kit.yml --ref "${{ steps.release.outputs['packages/loopover-ui-kit--tag_name'] }}" -f released_by_release_please=true
195+
196+
# Self-heal for a known, reproducible, upstream googleapis/release-please limitation
197+
# (googleapis/release-please#1946, #1444, #1406 -- all the same "There are untagged, merged release
198+
# PRs outstanding - aborting" message, unresolved upstream): createReleases() (which actually tags a
199+
# merged Release PR) sometimes finds nothing to do, and createPullRequests() then aborts on every
200+
# subsequent run without ever tagging the merged PR -- confirmed live across two separate
201+
# linked-versions release cycles this session (#7127, #7133), both requiring a manual dispatch of
202+
# each publish-*.yml workflow plus manually flipping the merged PR's `autorelease: pending` label to
203+
# `autorelease: tagged` to unstick it. Traced as far as release-please's own source
204+
# (manifest.ts/workspace.ts) allows without being able to modify the third-party action itself.
205+
#
206+
# Rather than depend on release-please's own `steps.release.outputs` above (unreliable exactly when
207+
# this bug is in play, since createReleases() never actually tagged anything), this reconciles from
208+
# the OTHER direction: compare each package's committed package.json version against what's
209+
# ACTUALLY live on npm, and self-heal by dispatching that package's publish workflow directly
210+
# whenever they disagree -- the same "bare manual dispatch" human-override path each publish-*.yml
211+
# already documents, just triggered automatically instead of by a human noticing red CI on an
212+
# unrelated PR days later. Runs on every push (cheap: a handful of `npm view` calls, no-op when
213+
# everything's already in sync), independent of whether the release-please job above succeeded,
214+
# aborted, or hit a transient error.
215+
#
216+
# Engine publishes first and is awaited before mcp/miner: they carry a real runtime dependency on
217+
# it, and their own isolated pack/smoke-test step resolves @loopover/engine from the real npm
218+
# registry (no local workspace symlink available in that isolated temp dir), so it fails with
219+
# ETARGET if engine hasn't actually finished publishing yet (confirmed live: the first attempt at
220+
# this exact reconciliation, dispatched in parallel, hit that exact race).
221+
reconcile-stale-releases:
222+
runs-on: ubuntu-latest
223+
needs: release-please
224+
if: ${{ !cancelled() }}
225+
environment: release
226+
timeout-minutes: 25
227+
permissions:
228+
contents: read
229+
actions: write
230+
pull-requests: write
231+
steps:
232+
- name: Checkout
233+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
234+
with:
235+
persist-credentials: false
236+
- name: Setup Node
237+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
238+
with:
239+
node-version: 24.18.0
240+
- name: Publish any package whose committed version isn't live on npm yet
241+
env:
242+
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
243+
run: |
244+
set -euo pipefail
245+
246+
needs_publish() {
247+
local dir="$1" pkg="$2"
248+
local local_version published_version
249+
local_version="$(node -p "require('./$dir/package.json').version")"
250+
published_version="$(npm view "$pkg" version 2>/dev/null || echo "")"
251+
if [ "$local_version" != "$published_version" ]; then
252+
echo "$pkg: committed $local_version, npm has ${published_version:-<none>}."
253+
return 0
254+
fi
255+
echo "$pkg: already in sync at $local_version."
256+
return 1
257+
}
258+
259+
dispatch_and_wait() {
260+
local workflow="$1"
261+
gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY"
262+
sleep 8
263+
local run_id
264+
run_id="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --limit 1 --json databaseId --jq '.[0].databaseId')"
265+
echo "Dispatched $workflow as run $run_id, waiting for it to complete..."
266+
while [ "$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json status --jq '.status')" != "completed" ]; do
267+
sleep 15
268+
done
269+
local conclusion
270+
conclusion="$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json conclusion --jq '.conclusion')"
271+
echo "$workflow (run $run_id) concluded: $conclusion"
272+
[ "$conclusion" = "success" ]
273+
}
274+
275+
engine_ok=true
276+
if needs_publish packages/loopover-engine @loopover/engine; then
277+
dispatch_and_wait publish-engine.yml || engine_ok=false
278+
fi
279+
280+
if [ "$engine_ok" = "true" ]; then
281+
if needs_publish packages/loopover-mcp @loopover/mcp; then
282+
dispatch_and_wait publish-mcp.yml || echo "::warning::publish-mcp.yml did not succeed -- left for manual follow-up."
283+
fi
284+
if needs_publish packages/loopover-miner @loopover/miner; then
285+
dispatch_and_wait publish-miner.yml || echo "::warning::publish-miner.yml did not succeed -- left for manual follow-up."
286+
fi
287+
else
288+
echo "::warning::Skipping mcp/miner reconciliation -- publish-engine.yml did not succeed, and they depend on it."
289+
fi
290+
291+
if needs_publish packages/loopover-ui-kit @loopover/ui-kit; then
292+
dispatch_and_wait publish-ui-kit.yml || echo "::warning::publish-ui-kit.yml did not succeed -- left for manual follow-up."
293+
fi
294+
295+
# Once every committed version is confirmed live on npm (the loop above didn't need to run, or
296+
# completed successfully for everything it dispatched), any merged Release PR still carrying
297+
# release-please's own `autorelease: pending` label is exactly the stuck state this job exists
298+
# to fix -- flip it so release-please's NEXT run stops aborting on it.
299+
- name: Un-stick release-please's own tracking on any merged-but-still-pending release PR
300+
env:
301+
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
302+
run: |
303+
set -euo pipefail
304+
for dir_pkg in "packages/loopover-engine:@loopover/engine" "packages/loopover-mcp:@loopover/mcp" "packages/loopover-miner:@loopover/miner" "packages/loopover-ui-kit:@loopover/ui-kit"; do
305+
dir="${dir_pkg%%:*}"
306+
pkg="${dir_pkg##*:}"
307+
local_version="$(node -p "require('./$dir/package.json').version")"
308+
published_version="$(npm view "$pkg" version 2>/dev/null || echo "")"
309+
if [ "$local_version" != "$published_version" ]; then
310+
echo "::warning::$pkg still not live on npm (committed $local_version, npm has ${published_version:-<none>}) -- leaving autorelease labels alone."
311+
exit 0
312+
fi
313+
done
314+
numbers="$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --label "autorelease: pending" --json number --jq '.[].number')"
315+
for number in $numbers; do
316+
echo "Flipping autorelease label on merged PR #$number -- its packages are confirmed live on npm."
317+
gh pr edit "$number" --repo "$GITHUB_REPOSITORY" --remove-label "autorelease: pending" --add-label "autorelease: tagged"
318+
done

apps/loopover-ui/src/lib/selfhost-env-reference.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export type SelfHostEnvReferenceRow = {
55
};
66

77
export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
8+
{
9+
name: "ADMIN_GITHUB_LOGINS",
10+
firstReference: "src/queue/processors.ts",
11+
},
812
{
913
name: "AI_ADVISORY",
1014
firstReference: "src/selfhost/ai.ts",
@@ -21,10 +25,18 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
2125
name: "AI_ADVISORY_MODEL",
2226
firstReference: "src/server.ts",
2327
},
28+
{
29+
name: "AI_BYOK_DAILY_REPO_LIMIT",
30+
firstReference: "src/services/ai-review.ts",
31+
},
2432
{
2533
name: "AI_COMBINE",
2634
firstReference: "src/selfhost/ai.ts",
2735
},
36+
{
37+
name: "AI_DAILY_NEURON_BUDGET",
38+
firstReference: "src/services/ai-review.ts",
39+
},
2840
{
2941
name: "AI_DUAL_REVIEW",
3042
firstReference: "src/selfhost/ai.ts",
@@ -41,6 +53,14 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
4153
name: "AI_EMBED_MODEL",
4254
firstReference: "src/selfhost/ai.ts",
4355
},
56+
{
57+
name: "AI_GATEWAY_ID",
58+
firstReference: "src/services/ai-review.ts",
59+
},
60+
{
61+
name: "AI_MAX_OUTPUT_TOKENS",
62+
firstReference: "src/services/ai-review.ts",
63+
},
4464
{
4565
name: "AI_ON_MERGE",
4666
firstReference: "src/selfhost/ai.ts",
@@ -49,6 +69,22 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
4969
name: "AI_PROVIDER",
5070
firstReference: "src/selfhost/ai-config.ts",
5171
},
72+
{
73+
name: "AI_PUBLIC_COMMENTS_ENABLED",
74+
firstReference: "src/services/ai-review.ts",
75+
},
76+
{
77+
name: "AI_REVIEW_PLAN",
78+
firstReference: "src/services/ai-review.ts",
79+
},
80+
{
81+
name: "AI_SUMMARIES_ENABLED",
82+
firstReference: "src/services/ai-review.ts",
83+
},
84+
{
85+
name: "AI_VISION",
86+
firstReference: "src/queue/processors.ts",
87+
},
5288
{
5389
name: "AI_VISION_API_KEY",
5490
firstReference: "src/server.ts",
@@ -133,6 +169,14 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
133169
name: "CONFIG_DIR_EMPTY_ACKNOWLEDGED",
134170
firstReference: "src/server.ts",
135171
},
172+
{
173+
name: "CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT",
174+
firstReference: "src/queue/processors.ts",
175+
},
176+
{
177+
name: "CONTRIBUTOR_EVIDENCE_BATCH_SIZE",
178+
firstReference: "src/queue/processors.ts",
179+
},
136180
{
137181
name: "CRON_INTERVAL_MS",
138182
firstReference: "src/server.ts",
@@ -177,6 +221,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
177221
name: "GITHUB_APP_PRIVATE_KEY",
178222
firstReference: "src/selfhost/orb-collector.ts",
179223
},
224+
{
225+
name: "GITHUB_APP_SLUG",
226+
firstReference: "src/queue/processors.ts",
227+
},
180228
{
181229
name: "GITHUB_CACHE_TTL_SECONDS",
182230
firstReference: "src/server.ts",
@@ -193,6 +241,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
193241
name: "GITHUB_INSTALLATION_CONCURRENCY_LIMIT",
194242
firstReference: "src/selfhost/installation-concurrency-admission.ts",
195243
},
244+
{
245+
name: "GITHUB_PUBLIC_TOKEN",
246+
firstReference: "src/queue/ai-review-orchestration.ts",
247+
},
196248
{
197249
name: "HOME",
198250
firstReference: "src/selfhost/ai.ts",
@@ -209,6 +261,10 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
209261
name: "LOOPOVER_REPO_CONFIG_DIR",
210262
firstReference: "src/server.ts",
211263
},
264+
{
265+
name: "LOOPOVER_REVIEW_CONTINUOUS",
266+
firstReference: "src/queue/processors.ts",
267+
},
212268
{
213269
name: "LOOPOVER_VERSION",
214270
firstReference: "src/selfhost/otel.ts",
@@ -486,17 +542,26 @@ export const SELFHOST_ENV_REFERENCE_ROWS: SelfHostEnvReferenceRow[] = [
486542
export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
487543
"| Name | First reference |",
488544
"| --- | --- |",
545+
"| `ADMIN_GITHUB_LOGINS` | `src/queue/processors.ts` |",
489546
"| `AI_ADVISORY` | `src/selfhost/ai.ts` |",
490547
"| `AI_ADVISORY_API_KEY` | `src/server.ts` |",
491548
"| `AI_ADVISORY_BASE_URL` | `src/server.ts` |",
492549
"| `AI_ADVISORY_MODEL` | `src/server.ts` |",
550+
"| `AI_BYOK_DAILY_REPO_LIMIT` | `src/services/ai-review.ts` |",
493551
"| `AI_COMBINE` | `src/selfhost/ai.ts` |",
552+
"| `AI_DAILY_NEURON_BUDGET` | `src/services/ai-review.ts` |",
494553
"| `AI_DUAL_REVIEW` | `src/selfhost/ai.ts` |",
495554
"| `AI_EMBED_API_KEY` | `src/server.ts` |",
496555
"| `AI_EMBED_BASE_URL` | `src/server.ts` |",
497556
"| `AI_EMBED_MODEL` | `src/selfhost/ai.ts` |",
557+
"| `AI_GATEWAY_ID` | `src/services/ai-review.ts` |",
558+
"| `AI_MAX_OUTPUT_TOKENS` | `src/services/ai-review.ts` |",
498559
"| `AI_ON_MERGE` | `src/selfhost/ai.ts` |",
499560
"| `AI_PROVIDER` | `src/selfhost/ai-config.ts` |",
561+
"| `AI_PUBLIC_COMMENTS_ENABLED` | `src/services/ai-review.ts` |",
562+
"| `AI_REVIEW_PLAN` | `src/services/ai-review.ts` |",
563+
"| `AI_SUMMARIES_ENABLED` | `src/services/ai-review.ts` |",
564+
"| `AI_VISION` | `src/queue/processors.ts` |",
500565
"| `AI_VISION_API_KEY` | `src/server.ts` |",
501566
"| `AI_VISION_BASE_URL` | `src/server.ts` |",
502567
"| `AI_VISION_MODEL` | `src/server.ts` |",
@@ -518,6 +583,8 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
518583
"| `CODEX_AI_TIMEOUT_MS` | `src/selfhost/ai.ts` |",
519584
"| `CODEX_HOME` | `src/selfhost/ai.ts` |",
520585
"| `CONFIG_DIR_EMPTY_ACKNOWLEDGED` | `src/server.ts` |",
586+
"| `CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT` | `src/queue/processors.ts` |",
587+
"| `CONTRIBUTOR_EVIDENCE_BATCH_SIZE` | `src/queue/processors.ts` |",
521588
"| `CRON_INTERVAL_MS` | `src/server.ts` |",
522589
"| `DATABASE_PATH` | `src/server.ts` |",
523590
"| `DATABASE_URL` | `src/selfhost/preflight.ts` |",
@@ -529,14 +596,17 @@ export const SELFHOST_ENV_REFERENCE_MARKDOWN = [
529596
"| `FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP` | `src/selfhost/foreground-liveness.ts` |",
530597
"| `GITHUB_APP_ID` | `src/selfhost/orb-collector.ts` |",
531598
"| `GITHUB_APP_PRIVATE_KEY` | `src/selfhost/orb-collector.ts` |",
599+
"| `GITHUB_APP_SLUG` | `src/queue/processors.ts` |",
532600
"| `GITHUB_CACHE_TTL_SECONDS` | `src/server.ts` |",
533601
"| `GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS` | `src/selfhost/installation-concurrency-admission.ts` |",
534602
"| `GITHUB_INSTALLATION_CONCURRENCY_ENABLED` | `src/selfhost/installation-concurrency-admission.ts` |",
535603
"| `GITHUB_INSTALLATION_CONCURRENCY_LIMIT` | `src/selfhost/installation-concurrency-admission.ts` |",
604+
"| `GITHUB_PUBLIC_TOKEN` | `src/queue/ai-review-orchestration.ts` |",
536605
"| `HOME` | `src/selfhost/ai.ts` |",
537606
"| `LOOPOVER_ENABLE_PAGERDUTY` | `src/services/notify-pagerduty.ts` |",
538607
"| `LOOPOVER_ENABLE_UNSAFE_CODEX_REVIEWER` | `src/selfhost/ai.ts` |",
539608
"| `LOOPOVER_REPO_CONFIG_DIR` | `src/server.ts` |",
609+
"| `LOOPOVER_REVIEW_CONTINUOUS` | `src/queue/processors.ts` |",
540610
"| `LOOPOVER_VERSION` | `src/selfhost/otel.ts` |",
541611
"| `MAINTENANCE_ADMISSION_DEFER_MS` | `src/selfhost/maintenance-admission.ts` |",
542612
"| `MAINTENANCE_ADMISSION_DRAIN_AGE_MS` | `src/selfhost/maintenance-admission.ts` |",

scripts/gen-selfhost-env-reference.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ export const DEFAULT_SOURCE_ROOTS = [
1010
"src/server.ts",
1111
"src/services/notify-discord.ts",
1212
"src/services/notify-pagerduty.ts",
13+
// The AI review pipeline reads self-host AI_* knobs (AI_SUMMARIES_ENABLED, AI_PUBLIC_COMMENTS_ENABLED,
14+
// AI_MAX_OUTPUT_TOKENS, AI_BYOK_DAILY_REPO_LIMIT) here, not under src/selfhost, so they were absent from the
15+
// generated reference despite being declared self-host vars in env.d.ts (#6993).
16+
"src/services/ai-review.ts",
17+
"src/queue/ai-review-orchestration.ts",
18+
"src/queue/processors.ts",
1319
"scripts/build-selfhost.mjs",
1420
"scripts/migrate-selfhost-sqlite-to-postgres.ts",
1521
"scripts/smoke-observability-traces.mjs",

test/unit/selfhost-env-reference-script.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,17 @@ describe("gen-selfhost-env-reference (#2081)", () => {
161161
expect(readFileSync(outputAbs, "utf8")).toBe(generated);
162162
});
163163
});
164+
165+
describe("AI review-pipeline self-host env vars (#6993)", () => {
166+
it("scans the AI review source roots so their self-host AI_* vars are collected", () => {
167+
// Against the REAL repo with the default (now-extended) source roots: the four AI review-pipeline knobs are
168+
// read in src/services/ai-review.ts etc., not under src/selfhost, so they only surface once those roots are
169+
// scanned. Before #6993 none of these appeared in the generated reference.
170+
const byName = new Map(collectSelfHostEnvVars({}).map((row) => [row.name, row.firstReference]));
171+
for (const name of ["AI_SUMMARIES_ENABLED", "AI_PUBLIC_COMMENTS_ENABLED", "AI_MAX_OUTPUT_TOKENS", "AI_BYOK_DAILY_REPO_LIMIT"]) {
172+
expect(byName.has(name), name).toBe(true);
173+
}
174+
// AI_MAX_OUTPUT_TOKENS is read only in ai-review.ts, so its first reference proves the new root is the source.
175+
expect(byName.get("AI_MAX_OUTPUT_TOKENS")).toBe("src/services/ai-review.ts");
176+
});
177+
});

0 commit comments

Comments
 (0)