Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 35 additions & 22 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -617,27 +617,38 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review
# OTEL_TRACES_SAMPLER=parentbased_traceidratio
# OTEL_TRACES_SAMPLER_ARG=0.05 # sample 5% of root traces when using parentbased_traceidratio

# --- Sentry error tracking (optional) ---
# SENTRY_DSN= # enables self-host Sentry capture; unset = complete no-op
# SENTRY_DSN_FILE= # optional mounted secret file; the generic *_FILE loader
# # (src/selfhost/load-file-secrets.ts) resolves it into SENTRY_DSN
# SENTRY_ENVIRONMENT=production # the code's own default (src/selfhost/sentry.ts); set any label you
# # want to slice by in Sentry (staging, selfhost, ...) — nothing
# # behaves differently per value, it only tags the events
# SENTRY_SERVER_NAME= # clean name for THIS instance in Sentry (e.g. loopover-us-east);
# --- PostHog error tracking (optional, replaces Sentry) ---
# POSTHOG_API_KEY= # enables self-host PostHog capture; unset = complete no-op. The
# # same key also activates MCP tool-call telemetry (src/mcp/telemetry.ts)
# # -- one PostHog project covers both surfaces.
# POSTHOG_API_KEY_FILE= # optional mounted secret file; the generic *_FILE loader
# # (src/selfhost/load-file-secrets.ts) resolves it into POSTHOG_API_KEY
# POSTHOG_HOST= # ingestion host; unset defaults to US-cloud (https://us.i.posthog.com).
# # Set https://eu.i.posthog.com for EU-cloud.
# POSTHOG_ENVIRONMENT=production # the code's own default; set any label you want to slice by in
# # PostHog (staging, selfhost, ...) — nothing behaves differently
# # per value, it only tags the events
# POSTHOG_SERVER_NAME= # clean name for THIS instance in PostHog (e.g. loopover-us-east);
# # unset defaults to the OS hostname — never the public-origin URL
# SENTRY_TRACES_SAMPLE_RATE= # traces/spans are off when blank/unset; errors still report. Set a LOW
# # rate (e.g. 0.05) with SENTRY_DSN to sample review tracing: each
# # sampled review emits a connected trace — the queue-job span
# # (whole-review latency) with the AI-provider span nested — so you can
# # filter slow or failed STAGES in Sentry without reading scattered logs.
# # Spans carry only safe dimensions (repo, job type, provider/model);
# # never prompts, diffs, tokens, or bodies.
# SENTRY_RELEASE= # custom images only: set this ONLY when you uploaded source maps for
# # the exact built bundle under this exact release id. Future official
# POSTHOG_MIN_SEVERITY= # error/critical reach PostHog by default; set to warning or info to
# # see more while actively debugging (POSTHOG_REPO_MIN_SEVERITY is
# # the same, but a JSON {repoFullName: severity} per-repo override map)
# POSTHOG_RELEASE= # custom images only: set this ONLY when you uploaded source maps for
# # the exact built bundle under this exact release id. Official
# # images bake LOOPOVER_VERSION=loopover-selfhost@<version> (see
# # scripts/deploy-selfhost-prebuilt.sh), so do not override
# # SENTRY_RELEASE for those images.
# # POSTHOG_RELEASE for those images.
#
# Distributed tracing (beta): set OTEL_TRACES_EXPORTER=otlp (below) with POSTHOG_API_KEY above and no explicit
# OTEL_EXPORTER_OTLP_* override to send traces straight to PostHog's own distributed-tracing product
# (https://posthog.com/docs/distributed-tracing) -- same project, same auth, no extra config needed.
#
# Uploading source maps for a CUSTOM image build (official images already ship symbolicated) --
# POSTHOG_CLI_API_KEY is a PERSONAL PostHog API key (distinct from the project-scoped POSTHOG_API_KEY above),
# used only at build/deploy time by scripts/deploy-selfhost-prebuilt.sh, never baked into the runtime image:
# POSTHOG_CLI_API_KEY= # personal API key with error_tracking read+write scopes
# POSTHOG_CLI_PROJECT_ID= # numeric PostHog project id
# POSTHOG_CLI_HOST= # override only for EU-cloud or self-hosted PostHog
# OTEL_METRIC_EXPORT_INTERVAL=10000 # ms between metric exports (default 10s here; CLI default is 60s)
# OTEL_METRICS_EXPORTER=otlp # metrics-side counterpart to OTEL_TRACES_EXPORTER; empty = no app metrics
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 # override only for an external collector
Expand All @@ -650,10 +661,12 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review
# data source after Grafana is up with: ./scripts/setup-github-datasource.sh (reads GITHUB_TOKEN below).
# GITHUB_TOKEN=<read-only-github-token> # fine-grained PAT: Pull requests:read, Issues:read, Contents:read
#
# In-Grafana Sentry error/issue visualization (#5369, distinct from SENTRY_DSN above, which is for error
# REPORTING). Install is automatic (GF_INSTALL_PLUGINS); add the data source after Grafana is up with:
# ./scripts/setup-sentry-datasource.sh (reads the two vars below). SENTRY_DSN is NOT reusable here -- a DSN
# authenticates event ingestion, not the read/query API this datasource needs.
# In-Grafana Sentry error/issue visualization (#5369). Self-host no longer sends new events to Sentry
# (error tracking now goes through PostHog, above) -- this panel only remains useful for an operator's own
# PRE-EXISTING Sentry issue history in an org they still separately maintain; pending removal (epic #8286).
# Install is automatic (GF_INSTALL_PLUGINS); add the data source after Grafana is up with:
# ./scripts/setup-sentry-datasource.sh (reads the two vars below). A Sentry event-ingestion DSN is NOT
# reusable here -- this datasource needs a separate read/query API token instead.
# SENTRY_API_TOKEN=<sentry-internal-integration-token> # Sentry -> Settings -> Developer Settings -> Custom
# # Integrations -> New Internal Integration, with Read access on
# # Project, Issue & Event, and Organization scopes
Expand Down
111 changes: 36 additions & 75 deletions .github/workflows/release-selfhost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ jobs:
# reads only event fields because a job's `environment:` is resolved before any step runs.
environment: ${{ github.event_name == 'workflow_dispatch' && github.actor == 'github-actions[bot]' && github.ref_type == 'tag' && inputs.create_github_release && contains(inputs.version, '-beta.') && 'release-beta' || 'release' }}
env:
SENTRY_ORG: jsonbored
SENTRY_PROJECT: loopover
SENTRY_CLI_PACKAGE: "@sentry/cli@3.6.0"
POSTHOG_CLI_PACKAGE: "@posthog/cli@0.9.1"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
Expand Down Expand Up @@ -90,7 +88,7 @@ jobs:
fi
# #1937: a stable X.Y.Z tag is the only kind that ever moved `latest` or an unmarked GitHub
# Release; a prerelease tag (X.Y.Z-rc.N / X.Y.Z-beta.N) publishes the SAME image/provenance/SBOM/
# Sentry pipeline below, just flagged as prerelease and never pushed under `latest` (see the
# PostHog pipeline below, just flagged as prerelease and never pushed under `latest` (see the
# "Resolve image tags" and "GitHub Release" steps).
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(rc|beta)\.[0-9]+)?$'; then
echo "expected semver version X.Y.Z, or a prerelease X.Y.Z-rc.N / X.Y.Z-beta.N, got $VERSION" >&2
Expand Down Expand Up @@ -118,7 +116,7 @@ jobs:
echo "prerelease=${PRERELEASE}"
} >> "$GITHUB_OUTPUT"

# Release jobs receive publishing/Sentry credentials, so avoid shared dependency caches here.
# Release jobs receive publishing/PostHog credentials, so avoid shared dependency caches here.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version-file: .nvmrc
Expand All @@ -140,57 +138,39 @@ jobs:
- name: Validate release source map
run: node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts

- name: Detect Sentry release token
id: sentry
- name: Detect PostHog release token
id: posthog
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
run: |
if [ -n "$SENTRY_AUTH_TOKEN" ]; then
if [ -n "$POSTHOG_CLI_API_KEY" ]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi

- name: Require Sentry token for official release
if: github.repository == 'JSONbored/loopover' && steps.sentry.outputs.enabled != 'true'
- name: Require PostHog token for official release
if: github.repository == 'JSONbored/loopover' && steps.posthog.outputs.enabled != 'true'
run: |
echo "::error::Configure SENTRY_AUTH_TOKEN in the release environment before publishing official Orb images."
echo "::error::Configure POSTHOG_CLI_API_KEY (a personal API key) in the release environment before publishing official Orb images."
exit 1

- name: Upload Sentry source maps
if: steps.sentry.outputs.enabled == 'true'
- name: Upload PostHog source maps
if: steps.posthog.outputs.enabled == 'true'
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'jsonbored' }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || 'loopover' }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_RELEASE: ${{ steps.version.outputs.release }}
SENTRY_REPOSITORY: ${{ github.repository }}
SENTRY_COMMIT_SHA: ${{ github.sha }}
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }}
POSTHOG_CLI_HOST: ${{ vars.POSTHOG_CLI_HOST }}
POSTHOG_RELEASE: ${{ steps.version.outputs.release }}
run: |
set -euo pipefail
test -n "$SENTRY_AUTH_TOKEN"
test -n "$SENTRY_ORG"
test -n "$SENTRY_PROJECT"
if [ -z "${SENTRY_URL:-}" ]; then unset SENTRY_URL; fi
npx -y "$SENTRY_CLI_PACKAGE" releases new "$SENTRY_RELEASE"
# `sentry-cli releases set-commits --commit` (even without --ignore-missing) can silently leave
# the release with ZERO associated commits against a repo connected via the modern GitHub App
# integration -- confirmed empirically cutting the first orb-v0.1.0-beta.1 release: the CLI
# printed a success table but `GET .../releases/<v>/commits/` stayed `[]`. A direct PUT to the
# release resource with an inline `commits` array resolves the same repo/commit against the
# SAME integration correctly and immediately, so use that instead of relying on the CLI here --
# it's what the later "Validate Sentry release" step's `SENTRY_REQUIRE_COMMITS=true` actually
# depends on being correct.
curl -sf -X PUT \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
"${SENTRY_URL:-https://sentry.io}/api/0/organizations/${SENTRY_ORG}/releases/$(node -e "process.stdout.write(encodeURIComponent(process.env.SENTRY_RELEASE))")/" \
-d "$(node -e "process.stdout.write(JSON.stringify({commits:[{repository: process.env.SENTRY_REPOSITORY, id: process.env.SENTRY_COMMIT_SHA}]}))")" \
>/dev/null
npx -y "$SENTRY_CLI_PACKAGE" sourcemaps inject dist
test -n "$POSTHOG_CLI_API_KEY"
test -n "$POSTHOG_CLI_PROJECT_ID"
# No separate "create release" step -- PostHog release metadata is a byproduct of the inject/
# upload calls below, unlike Sentry's releases/commits/deploys/finalize lifecycle this replaces.
npx -y "$POSTHOG_CLI_PACKAGE" sourcemap inject --directory dist --release-version "$POSTHOG_RELEASE"
node --experimental-strip-types scripts/validate-selfhost-sourcemap.ts
npx -y "$SENTRY_CLI_PACKAGE" sourcemaps upload --release="$SENTRY_RELEASE" --validate --wait --strict dist
npx -y "$POSTHOG_CLI_PACKAGE" sourcemap upload --directory dist --release-version "$POSTHOG_RELEASE"

- uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
Expand Down Expand Up @@ -270,50 +250,31 @@ jobs:
cache-from: type=gha,scope=release-orb
cache-to: type=gha,mode=max,scope=release-orb

- name: Finalize Sentry release
if: steps.sentry.outputs.enabled == 'true'
- name: Validate PostHog release
if: steps.posthog.outputs.enabled == 'true'
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'jsonbored' }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || 'loopover' }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_RELEASE: ${{ steps.version.outputs.release }}
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }}
POSTHOG_CLI_HOST: ${{ vars.POSTHOG_CLI_HOST }}
POSTHOG_RELEASE: ${{ steps.version.outputs.release }}
run: |
set -euo pipefail
if [ -z "${SENTRY_URL:-}" ]; then unset SENTRY_URL; fi
npx -y "$SENTRY_CLI_PACKAGE" releases finalize "$SENTRY_RELEASE"

- name: Validate Sentry release
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG || 'jsonbored' }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || 'loopover' }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_RELEASE: ${{ steps.version.outputs.release }}
SENTRY_REPOSITORY: ${{ github.repository }}
SENTRY_COMMIT_SHA: ${{ github.sha }}
SENTRY_REQUIRE_COMMITS: "true"
SENTRY_REQUIRE_DEPLOY: "false"
SENTRY_REQUIRE_FINALIZED: "true"
run: |
set -euo pipefail
# The CLI's `set-commits` was replaced with a direct PUT above after it silently no-op'd on
# orb-v0.1.0-beta.1, but Sentry's release-commits read path can still lag briefly behind a
# write. review-enrichment/src/upload-sourcemaps.ts's runReleaseValidation() already
# retry-polls this same script for REES's Railway deploy path -- mirror that here instead of
# failing the whole release build on a transient read-lag.
# PostHog's release-read path can lag briefly behind the upload write above.
# review-enrichment/src/upload-sourcemaps.ts's runReleaseValidation() already retry-polls this
# same script for REES's own deploy path -- mirror that here instead of failing the whole
# release build on a transient read-lag.
attempts=5
delay_secs=10
for attempt in $(seq 1 "$attempts"); do
if node review-enrichment/scripts/validate-sentry-release.mjs; then
if node review-enrichment/scripts/validate-posthog-release.mjs; then
exit 0
fi
if [ "$attempt" -lt "$attempts" ]; then
echo "::warning::Sentry release validation attempt ${attempt}/${attempts} failed; retrying in ${delay_secs}s"
echo "::warning::PostHog release validation attempt ${attempt}/${attempts} failed; retrying in ${delay_secs}s"
sleep "$delay_secs"
fi
done
echo "::error::Sentry release validation failed after ${attempts} attempts"
echo "::error::PostHog release validation failed after ${attempts} attempts"
exit 1

- name: GitHub Release
Expand Down Expand Up @@ -346,7 +307,7 @@ jobs:

Multi-arch (linux/amd64 + linux/arm64). See https://loopover.ai/docs/maintainer-self-hosting for setup.
Includes the Claude Code / Codex subscription CLIs by default; credentials stay runtime-only.
Sentry release id baked into the image: \`${RELEASE_ID}\`.
PostHog release id baked into the image: \`${RELEASE_ID}\`.
EOF
)"
# #1937: a prerelease tag never becomes the repo's "Latest release" and is visibly marked as such
Expand Down
2 changes: 1 addition & 1 deletion scripts/deploy-selfhost-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# ./scripts/deploy-selfhost-image.sh ghcr.io/jsonbored/loopover-selfhost:orb-v0.1.0
# LOOPOVER_IMAGE=ghcr.io/jsonbored/loopover-selfhost@sha256:... ./scripts/deploy-selfhost-image.sh
#
# The image itself carries official release metadata. Set SENTRY_RELEASE only for custom images whose
# The image itself carries official release metadata. Set POSTHOG_RELEASE only for custom images whose
# source maps were uploaded under that exact id.
#
# Optional Infisical secrets (#5120), see docs:
Expand Down
Loading