Skip to content

Commit 262140e

Browse files
authored
Merge branch 'main' into fix/selfhost-pr-state-review-cache
2 parents f282f34 + 9e9b004 commit 262140e

40 files changed

Lines changed: 1999 additions & 39 deletions

.claude/skills/contributing-to-gittensory/reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ path filter matched; on push to `main`, everything runs.
2626
| changes | `git diff --check` + path filter | `git diff --check` | trailing whitespace / conflict markers |
2727
| lint → actionlint | workflow lint | `npm run actionlint` | any `.github/workflows/*.yml` violation |
2828
| lint → migrations | migration guard | `npm run db:migrations:check` | duplicate/gap/misnamed migration number |
29+
| lint → cf-typegen | worker types drift | `npm run cf-typegen:check` | committed `worker-configuration.d.ts` is stale (run `npm run cf-typegen`) |
2930
| lint → typecheck | `tsc --noEmit` | `npm run typecheck` | any backend type error |
3031
| test (1/2) | sharded vitest + coverage | `npm run test:coverage` (unsharded) | any failing `test/**/*.test.ts` (excl. `test/workers/**`) |
3132
| workers | workers-pool vitest | `npm run test:workers` | any failing `test/workers/**` |
3233
| mcp → build | MCP pkg build | `npm run build:mcp` | MCP package build error |
3334
| mcp → pack | tarball hygiene | `npm run test:mcp-pack` | unexpected/forbidden file or stale README in the npm tarball |
3435
| ui → openapi drift | spec check | `npm run ui:openapi:check` | committed `openapi.json` is stale (run `npm run ui:openapi`) |
36+
| ui → openapi settings-parity | schema/type structural diff | `npm run ui:openapi:settings-parity` | `RepositorySettingsSchema` (src/openapi/schemas.ts) is missing a field the `RepositorySettings` type has |
3537
| ui → version audit | MCP version copy | `npm run ui:version-audit` | stale MCP version strings / non-`@latest` install copy (hits npm registry) |
3638
| ui → lint | `eslint .` (UI) | `npm run ui:lint` | ESLint **incl. Prettier formatting** + design-token rules |
3739
| ui → typecheck | `tsc --noEmit` (UI) | `npm run ui:typecheck` | UI type error |

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ jobs:
5656
- 'scripts/**'
5757
- 'migrations/**'
5858
- '.github/workflows/**'
59+
- 'wrangler.jsonc'
60+
- 'worker-configuration.d.ts'
5961
# The UI's own app/extension code -- triggers the FULL toolchain (lint/typecheck/test/build).
6062
# A dependency bump (package.json/package-lock.json) stays here too since it can break the UI
6163
# build or types in ways only that full toolchain would catch.
@@ -190,6 +192,12 @@ jobs:
190192
- name: Check migrations
191193
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
192194
run: npm run db:migrations:check
195+
# Guards against the same staleness-drift class as db:migrations:check/ui:openapi:check: two PRs that
196+
# each independently add a wrangler.jsonc binding can both pass CI in isolation, then merge sequentially
197+
# and leave a stale committed worker-configuration.d.ts with zero prior gate signal (#2557).
198+
- name: cf-typegen drift check
199+
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
200+
run: npm run cf-typegen:check
193201
- name: Typecheck
194202
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
195203
run: npm run typecheck
@@ -336,6 +344,12 @@ jobs:
336344
- name: OpenAPI drift check
337345
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.uiContract == 'true' }}
338346
run: npm run ui:openapi:check
347+
# #2556: RepositorySettingsSchema (hand-authored Zod) can silently drift from the actual
348+
# RepositorySettings TS type -- the OpenAPI drift check above only verifies the generated spec
349+
# matches the Zod schema, never that the schema matches the type the API actually serializes.
350+
- name: OpenAPI settings-parity check
351+
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.uiContract == 'true' }}
352+
run: npm run ui:openapi:settings-parity
339353
# Checks apps/gittensory-ui/src' known-latest MCP version string against the published package, so
340354
# its dependency is `ui` (the file it scans) + `mcp` (the package it checks against) -- NOT the
341355
# OpenAPI contract, which this script never reads.

.github/workflows/release-selfhost.yml

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
# git tag orb-v0.1.0 && git push origin orb-v0.1.0
55
#
66
# Pull: docker pull ghcr.io/<owner>/gittensory-selfhost:orb-v0.1.0
7+
#
8+
# Prerelease tags (#1937): orb-v0.1.0-rc.1 / orb-v0.1.0-beta.1 run the identical pipeline but never move
9+
# `latest` and are marked prerelease on the GitHub Release -- for beta-testing an image before it becomes
10+
# the stable/latest recommendation.
11+
#
12+
# git tag orb-v0.1.0-rc.1 && git push origin orb-v0.1.0-rc.1
713
name: release-orb
814

915
on:
@@ -13,7 +19,7 @@ on:
1319
workflow_dispatch:
1420
inputs:
1521
version:
16-
description: "Version to publish (e.g. 0.1.0)"
22+
description: "Version to publish (e.g. 0.1.0, or a prerelease 0.1.0-rc.1 / 0.1.0-beta.1)"
1723
required: true
1824

1925
permissions:
@@ -66,14 +72,23 @@ jobs:
6672
*) echo "expected an orb-v<semver> tag, got $REF_NAME" >&2; exit 1 ;;
6773
esac
6874
fi
69-
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
70-
echo "expected semver version X.Y.Z, got $VERSION" >&2
75+
# #1937: a stable X.Y.Z tag is the only kind that ever moved `latest` or an unmarked GitHub
76+
# Release; a prerelease tag (X.Y.Z-rc.N / X.Y.Z-beta.N) publishes the SAME image/provenance/SBOM/
77+
# Sentry pipeline below, just flagged as prerelease and never pushed under `latest` (see the
78+
# "Resolve image tags" and "GitHub Release" steps).
79+
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta)\.[0-9]+)?$'; then
80+
echo "expected semver version X.Y.Z, or a prerelease X.Y.Z-rc.N / X.Y.Z-beta.N, got $VERSION" >&2
7181
exit 1
7282
fi
83+
PRERELEASE=false
84+
if printf '%s' "$VERSION" | grep -Eq -- '-(rc|beta)\.[0-9]+$'; then
85+
PRERELEASE=true
86+
fi
7387
{
7488
echo "v=${VERSION}"
7589
echo "tag=orb-v${VERSION}"
7690
echo "release=gittensory-orb@${VERSION}"
91+
echo "prerelease=${PRERELEASE}"
7792
} >> "$GITHUB_OUTPUT"
7893
7994
# Release jobs receive publishing/Sentry credentials, so avoid shared dependency caches here.
@@ -139,15 +154,31 @@ jobs:
139154
username: ${{ github.actor }}
140155
password: ${{ secrets.GITHUB_TOKEN }}
141156

157+
# #1937: `latest` must never move to a prerelease build -- an operator who blindly pulls `latest`
158+
# for a trial should always land on the newest STABLE image, not an in-flight rc/beta.
159+
- name: Resolve image tags
160+
id: tags
161+
env:
162+
PRERELEASE: ${{ steps.version.outputs.prerelease }}
163+
VERSION_TAG: ${{ steps.version.outputs.tag }}
164+
run: |
165+
set -euo pipefail
166+
{
167+
echo "list<<GTORBTAGS"
168+
echo "type=raw,value=${VERSION_TAG}"
169+
if [ "$PRERELEASE" != "true" ]; then
170+
echo "type=raw,value=latest"
171+
fi
172+
echo "type=sha,format=short"
173+
echo "GTORBTAGS"
174+
} >> "$GITHUB_OUTPUT"
175+
142176
- name: Image metadata
143177
id: meta
144178
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
145179
with:
146180
images: ghcr.io/${{ github.repository_owner }}/gittensory-selfhost
147-
tags: |
148-
type=raw,value=${{ steps.version.outputs.tag }}
149-
type=raw,value=latest
150-
type=sha,format=short
181+
tags: ${{ steps.tags.outputs.list }}
151182
labels: |
152183
org.opencontainers.image.title=gittensory-orb
153184
org.opencontainers.image.description=Self-hostable Gittensory review engine
@@ -210,6 +241,7 @@ jobs:
210241
RELEASE_TAG: ${{ steps.version.outputs.tag }}
211242
RELEASE_ID: ${{ steps.version.outputs.release }}
212243
REPOSITORY_OWNER: ${{ github.repository_owner }}
244+
PRERELEASE: ${{ steps.version.outputs.prerelease }}
213245
run: |
214246
set -euo pipefail
215247
NOTES="$(cat <<EOF
@@ -224,14 +256,23 @@ jobs:
224256
Sentry release id baked into the image: \`${RELEASE_ID}\`.
225257
EOF
226258
)"
259+
# #1937: a prerelease tag never becomes the repo's "Latest release" and is visibly marked as such
260+
# on GitHub -- distinct from the image-tag `latest` decision above (Resolve image tags), which
261+
# this flag also drives at the version-resolution step.
262+
PRERELEASE_ARGS=()
263+
if [ "$PRERELEASE" = "true" ]; then
264+
PRERELEASE_ARGS=(--prerelease --latest=false)
265+
fi
227266
if gh release view "$REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
228267
gh release edit "$REF_NAME" --repo "$GITHUB_REPOSITORY" \
229268
--title "gittensory-orb ${RELEASE_TAG}" \
230-
--notes "$NOTES"
269+
--notes "$NOTES" \
270+
"${PRERELEASE_ARGS[@]}"
231271
else
232272
gh release create "$REF_NAME" --repo "$GITHUB_REPOSITORY" \
233273
--verify-tag \
234274
--title "gittensory-orb ${RELEASE_TAG}" \
275+
"${PRERELEASE_ARGS[@]}" \
235276
--notes "$NOTES" \
236277
--generate-notes
237278
fi

.gittensory.yml.example

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ gate:
135135
# the gate. Bool. Default: false.
136136
aiAdvisory: false
137137

138+
# Oversized-PR gate. A PR at/above BOTH the file-count and line-count thresholds
139+
# (engine defaults, not configurable here) gets a manual-review HOLD finding —
140+
# never a hard blocker, so it's dry-run/advisory friendly regardless of mode.
141+
# off | advisory | block. Default: off. Config-as-code only — no DB column or
142+
# dashboard toggle; this can only be set here.
143+
size:
144+
mode: off
145+
138146
# Composite merge-readiness gate (no min score).
139147
# off | advisory | block. Default: off.
140148
mergeReadiness: off
@@ -145,6 +153,19 @@ gate:
145153
# off | advisory | block. Default: off.
146154
manifestPolicy: off
147155

156+
# Self-authored-linked-issue gate — blocks (or advises on) a PR whose only
157+
# linked issue was opened by the PR's own author, which the readiness/quality
158+
# signals can't otherwise catch. off | advisory | block. Default: advisory.
159+
# DB-backed (dashboard-settable too); this overrides the stored value.
160+
selfAuthoredLinkedIssue: advisory
161+
162+
# Gate-wide dry-run. When true, the gate evaluates and posts its verdict as
163+
# normal but never actually blocks/merges/closes based on it — useful for
164+
# trialing a stricter pack or mode change against real traffic before
165+
# enforcing it. Bool. Default: false. Config-as-code only — no DB column or
166+
# dashboard toggle; this can only be set here.
167+
dryRun: false
168+
148169
# First-time-contributor grace. RESERVED / currently INERT: this value is
149170
# parsed and stored, but the gate does not read it — a first-time
150171
# contributor with a real blocker is one-shot closed the same as a
@@ -162,6 +183,15 @@ gate:
162183
# PR, so it is opt-in rather than a new default.
163184
premergeContentRecheck: false
164185

186+
# Force-rebase-before-merge window in minutes (#2552, anti-race). When the base branch has advanced within
187+
# this many minutes of the actual merge-decision moment, an agent-driven merge forces an update_branch +
188+
# fresh CI recheck cycle before merging, instead of trusting a mergeable_state: clean read that may already
189+
# be stale relative to a sibling commit that just landed on the base. A bounded retry cap prevents a
190+
# fast-moving base from live-locking the PR — after a few forced attempts it falls through to a normal
191+
# merge with an audit note. Positive integer (minutes), or omit/null. Default: null (never force).
192+
# DB-backed (dashboard-settable too); this overrides the stored value.
193+
requireFreshRebaseWindow: 10
194+
165195
# AI maintainer review. Opt-in; the AI capabilities are switched on at the
166196
# deployment level.
167197
aiReview:
@@ -188,6 +218,11 @@ gate:
188218
# String or null. Default: null (the key record's model, else a conservative
189219
# per-provider default).
190220
model: null
221+
# Minimum calibrated AI-reviewer confidence (0-1) for a consensus defect to
222+
# become a blocker; below this it stays advisory-only. Number 0–1, or null.
223+
# Default: null (engine uses 0.93). Config-as-code only — no DB column or
224+
# dashboard toggle; this can only be set here.
225+
closeConfidence: null
191226

192227

193228
# ----------------------------------------------------------------------------

apps/gittensory-ui/public/openapi.json

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8631,6 +8631,63 @@
86318631
},
86328632
"contributorCapLabel": {
86338633
"type": "string"
8634+
},
8635+
"sizeGateMode": {
8636+
"type": "string",
8637+
"enum": [
8638+
"off",
8639+
"advisory",
8640+
"block"
8641+
]
8642+
},
8643+
"gateDryRun": {
8644+
"type": "boolean"
8645+
},
8646+
"premergeContentRecheck": {
8647+
"type": "boolean"
8648+
},
8649+
"aiReviewMode": {
8650+
"type": "string",
8651+
"enum": [
8652+
"off",
8653+
"advisory",
8654+
"block"
8655+
]
8656+
},
8657+
"aiReviewByok": {
8658+
"type": "boolean"
8659+
},
8660+
"aiReviewProvider": {
8661+
"type": "string",
8662+
"nullable": true,
8663+
"enum": [
8664+
"anthropic",
8665+
"openai",
8666+
null
8667+
]
8668+
},
8669+
"aiReviewModel": {
8670+
"type": "string",
8671+
"nullable": true
8672+
},
8673+
"aiReviewAllAuthors": {
8674+
"type": "boolean"
8675+
},
8676+
"aiReviewCloseConfidence": {
8677+
"type": "number",
8678+
"nullable": true
8679+
},
8680+
"closeOwnerAuthors": {
8681+
"type": "boolean"
8682+
},
8683+
"badgeEnabled": {
8684+
"type": "boolean"
8685+
},
8686+
"requireFreshRebaseWindowMinutes": {
8687+
"type": "integer",
8688+
"nullable": true,
8689+
"minimum": 0,
8690+
"exclusiveMinimum": true
86348691
}
86358692
},
86368693
"required": [
@@ -8651,6 +8708,10 @@
86518708
"selfAuthoredLinkedIssueGateMode",
86528709
"firstTimeContributorGrace",
86538710
"slopAiAdvisory",
8711+
"aiReviewMode",
8712+
"aiReviewByok",
8713+
"aiReviewAllAuthors",
8714+
"closeOwnerAuthors",
86548715
"autoLabelEnabled",
86558716
"gittensorLabel",
86568717
"blacklistLabel",
@@ -9260,6 +9321,31 @@
92609321
},
92619322
"blacklistLabel": {
92629323
"type": "string"
9324+
},
9325+
"badgeEnabled": {
9326+
"type": "boolean"
9327+
},
9328+
"aiReviewMode": {
9329+
"type": "string",
9330+
"enum": [
9331+
"off",
9332+
"advisory",
9333+
"block"
9334+
]
9335+
},
9336+
"aiReviewByok": {
9337+
"type": "boolean"
9338+
},
9339+
"aiReviewProvider": {
9340+
"type": "string",
9341+
"nullable": true
9342+
},
9343+
"aiReviewModel": {
9344+
"type": "string",
9345+
"nullable": true
9346+
},
9347+
"aiReviewAllAuthors": {
9348+
"type": "boolean"
92639349
}
92649350
},
92659351
"required": [
@@ -9285,6 +9371,12 @@
92859371
"createMissingLabel",
92869372
"includeMaintainerAuthors",
92879373
"requireLinkedIssue",
9374+
"badgeEnabled",
9375+
"aiReviewMode",
9376+
"aiReviewByok",
9377+
"aiReviewProvider",
9378+
"aiReviewModel",
9379+
"aiReviewAllAuthors",
92889380
"commandAuthorization"
92899381
]
92909382
},

apps/gittensory-ui/src/routes/docs.self-hosting-releases.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function SelfHostingReleases() {
4242
{
4343
title: "latest",
4444
description:
45-
"Moves with the newest release. Useful for trials, not for controlled production.",
45+
"Moves with the newest STABLE release only — never a prerelease. Useful for trials, not for controlled production.",
4646
},
4747
{
4848
title: "sha",
@@ -56,6 +56,22 @@ function SelfHostingReleases() {
5656
docker pull ghcr.io/jsonbored/gittensory-selfhost:latest`}
5757
/>
5858

59+
<h2>Prerelease (beta/rc) images</h2>
60+
<p>
61+
A tag like <code>orb-v0.1.0-rc.1</code> or <code>orb-v0.1.0-beta.1</code> runs the identical
62+
build/provenance/SBOM/Sentry pipeline as a stable release, but is marked prerelease on
63+
GitHub and is never pushed under <code>latest</code>. External beta testers should pull the
64+
exact prerelease tag, not <code>latest</code>.
65+
</p>
66+
<CodeBlock
67+
lang="bash"
68+
code={`docker pull ghcr.io/jsonbored/gittensory-selfhost:orb-v0.1.0-rc.1`}
69+
/>
70+
<Callout variant="note">
71+
Stable release behavior is unchanged: a plain <code>X.Y.Z</code> tag still moves{" "}
72+
<code>latest</code> and publishes an unmarked (non-prerelease) GitHub Release.
73+
</Callout>
74+
5975
<h2>Upgrade flow</h2>
6076
<ol>
6177
<li>Read release notes for env, migration, or behavior changes.</li>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Force-rebase-before-merge gate (#2552): optional per-repo window (minutes). NULL (the default) means the
2+
-- gate never forces a rebase -- byte-identical behavior for every existing row. When set, an agent-driven
3+
-- merge whose base branch advanced within this window forces an update_branch + fresh CI recheck before
4+
-- merging, instead of trusting a mergeable_state: clean read that may already be stale relative to the base.
5+
ALTER TABLE repository_settings ADD COLUMN require_fresh_rebase_window_minutes INTEGER;

0 commit comments

Comments
 (0)