Skip to content

Release staging to main#745

Merged
jaeyunha merged 30 commits into
mainfrom
release/staging-main-743
Jun 26, 2026
Merged

Release staging to main#745
jaeyunha merged 30 commits into
mainfrom
release/staging-main-743

Conversation

@jaeyunha

Copy link
Copy Markdown
Member

Staging → main release

Promotes current staging to main after the latest staged PR #743.

Current delta

  • Source staging/release head: 4d191ed3
  • Base main head: 6fe5e1c3
  • Commit ancestry delta: 30 commits ahead / 0 behind
  • File delta: 46 files changed, 4887 insertions(+), 3497 deletions(-)

Included staged PRs / themes

Validation

In progress from /home/jaeyunha/wt/exponential/staging-main:

  • make check
  • make test
  • make test-e2e

Release notes

Fresh release branch release/staging-main-743 was created from origin/staging because the older release/staging-main-713 branch was stale after #713 was squash-merged and #743 landed.

jaeyunha and others added 30 commits June 17, 2026 12:45
* feat: ingest Gong customer call findings

* fix(gong): signature verification, TOCTOU race, empty-id guard, speaker fallback, RFC 7807 412

- Add per-integration sharedSecret generated at OAuth state-save time and
  persisted through completeGongInstall into workspace_integration metadata.
  GongIngestCall reads body bytes, verifies HMAC-SHA256 via X-Gong-Signature
  header using verifyGongSignature (crypto/subtle.ConstantTimeCompare) before
  any DB write.

- Fix resolveGongInstall to return a hard error (surfaced as 400) when
  integrationID is empty, eliminating silent fallback to most-recent
  integration.

- Fix TOCTOU race in createOrLinkGongFinding: extracted tryCreateGongFinding
  with SELECT ... FOR UPDATE on the issue table, wrapped in a retry loop
  (max 3 attempts) that retries on pgconn code 23505 unique violation.

- Fix gongExternalSpeaker: change fallback return from true to false so
  internal members missing from the participants list are not mis-classified
  as external/customer speakers.

- Fix GongConnect 412 response: replace raw map[string]string via problem.JSON
  with problem.Write to emit RFC 7807 application/problem+json.

- Regenerate packages/sdk/src/generated.ts from packages/proto/openapi.yaml;
  gong paths now appear in the TypeScript SDK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(gong): move call ingestion webhook to public router

Gong's servers post to POST /gong/{integrationID}/calls with only an
X-Gong-Signature header — no session cookie or bearer token. The route
was previously mounted under the session-authenticated protected group,
so every inbound webhook received 401 before the HMAC check could run.

Move the route to the publicProvider group (same group as SlackEvents
and GitLabWebhook) so unauthenticated Gong requests reach the handler.
Remove the canManage/auth.FromContext guard from GongIngestCall (the
HMAC signature check is the correct auth boundary for this endpoint).
Add resolveGongWebhookInstall to look up the integration by ID alone,
mirroring the resolveGitLabWebhookInstall pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: add zendesk ticket issue linking

* fix(zendesk): address code review findings in ticket linking

- Replace dynamic SQL string concatenation in resolveZendeskInstall with
  two separate static QueryRow calls (one for integrationID path, one for
  subdomain path) to eliminate SQL injection risk from Go string concat
- Restore early-return guard in queueSentryAutomations when links slice
  is empty so the workflow_state category query no longer runs on every
  issue state-change with no Sentry/Zendesk links
- Change ZendeskTicketCreate error response from 400 to 500 for internal
  server failures (DB errors, missing workflow state); 400 is reserved
  for input validation failures
- Assign getZendeskSourceLink(event) to a const in issue-detail-view.tsx
  to avoid calling the function twice in the same render

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Intercom conversation issue linking

* fix(intercom): address security and correctness issues from code review

- intercomSigningSecret: drop AUTH_INTERCOM_SECRET fallback; return ""
  when INTERCOM_SIGNING_SECRET is absent so the 503 path fires correctly
  instead of accepting requests signed with the OAuth client secret
- IntercomIssueUnlink: check RowsAffected() and return 404 when no row
  matched, consistent with IntercomIssueLink behaviour
- intercomIssueDescriptionHTML: guard Permalink with HasPrefix("https://")
  to block javascript: URI injection before building the <a href>
- IntercomConnect 412 path: replace problem.JSON map with problem.Write
  for RFC 7807 consistency

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Front conversation issue integration

* fix(integrations): harden Front integration security and correctness

- SSRF: replace unvalidated frontAPIBaseURL with normalizeFrontBaseURL
  that requires https and a .frontapp.com host on admin-supplied values
- HTTP client timeout: pass 10s-timeout client to validateFrontToken
  instead of http.DefaultClient (no timeout)
- Dead branch: failFrontJob now sets status "error" (not "degraded") on
  401/403 permission failures so operators know the token needs rotation
- FrontIssueCreate: distinguish validation sentinels (400) from DB/infra
  errors (500) via isFrontValidationError helper
- frontIssueDescriptionHTML: build combined HTML first, call
  sanitizehtml.RichText once instead of twice

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: add salesforce case links

* fix(salesforce): address SSRF, auth bypass, and double-call findings from code review

- Validate fetchSalesforceUserInfo endpoint against salesforceOAuthBaseURL()
  before making the outbound HTTP request to prevent SSRF via a crafted
  token.ID in the OAuth response.
- Remove the Authorization: Bearer fallback in salesforceSignedAction so
  HMAC signature verification is mandatory for every request; no static
  secret bypass is accepted.
- Move HTTP status checks before json.Decode in exchangeSalesforceOAuth
  and fetchSalesforceUserInfo; include response body in error messages so
  non-2xx failures are surfaced accurately.
- Assign getSalesforceSourceLink(event) to a const in the render path of
  issue-detail-view.tsx to avoid calling the function twice per render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Jira guided import slice

* fix(jira-import): address security and correctness issues from code review

- [CRITICAL] Fix silent import truncation: getJSON now decodes 2xx
  responses directly via json.NewDecoder (no 4096 limit); limit kept
  only on error branch for safe logging
- [CRITICAL] Fix JQL injection: validate projectKey against
  ^[A-Z][A-Z0-9_]{0,9}$ before use; wrap value in double-quoted JQL
- [HIGH] Require HTTPS in normalizeJiraBaseURL; reject http:// with a
  clear error
- [HIGH] Fix N+1 query: fetch team key once before the issue import loop
  instead of querying per issue via jiraTeamIdentifier
- [HIGH] Add pagination to issues(): startAt-based loop fetches all
  pages; surface jiraTruncatedError warning when a hard cap truncates;
  add Total/StartAt to jiraSearchResponse
- [MEDIUM] openapi.yaml: add writeOnly:true and description to token
  field on WorkspaceImportExportActionRequest
- [LOW] web: change jiraBaseUrl initial state to "" (placeholder only)
- Update handler_test.go to expect quoted JQL and include total in mock

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(jira-import): make maxResults=0 fetch all pages; remove dead helper; add pagination test

- issues(): replace the maxResults<=0→100 reset with an `unlimited` bool.
  When unlimited, only startAt>=response.Total or an empty page terminates
  the loop — fetching 100-issue batches across as many pages as needed.
  When a positive maxResults cap is supplied the hard-cap exit fires as
  before and surfaces a jiraTruncatedError.  The empty-page guard remains
  so a misbehaving server can never cause an infinite loop.
- Remove dead jiraTeamIdentifier() — confirmed unreferenced after the N+1
  fix that inlined the team-key lookup into importJiraIssues().
- Add TestJiraIssuesPaginatesAllPages: mock server returns total=150 across
  two pages (100+50); asserts all 150 issues are returned and exactly 2
  HTTP calls are made when maxResults=0.
- Add TestNormalizeJiraBaseURLRejectsHTTP: asserts http:// base URLs are
  rejected with an HTTPS error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add Figma issue previews

* fix(figma): code review fixes for PR #686

- Rename migration 0009_figma_sources.sql → 0010_figma_sources.sql to
  avoid collision with 0009_gitlab_merge_request_links.sql on staging
- Change timestamp columns to timestamptz in figma_source table
- Add format: uuid to the id path param on the refresh endpoint in OpenAPI
- Update OpenAPI summary to be truthful: endpoint stamps refreshed_at only,
  does not call the Figma API
- Regenerate SDK from updated OpenAPI spec
- Fix RefreshFigmaSource auth check: use p, ok := auth.FromContext and
  return 401 if not ok (was silently discarding the ok bool)
- Use SDK client (createBrowserApiClient) for handleFigmaRefresh instead
  of hardcoded fetch URL; remove local FigmaSource interface redeclaration
  and use components["schemas"]["FigmaSource"] from the SDK
- Validate thumbnailUrl in FigmaPreviewCard: only render <img> when URL
  starts with https:// to prevent XSS via untrusted URLs
- Update UI button label from "Refresh" to "Mark seen" to match reality

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: enrich triage routing metadata

* fix(triage): address code review correctness issues

- [CRITICAL] applyTriageDecision no longer writes to http.ResponseWriter;
  server errors return (nil, 500) so BulkTriage can abort the batch
  safely before the response is sent, preventing double-write and
  nil-map dereference panics on mid-batch 500-path failures.
  DecideTriage checks status >= 500 and writes a generic title so DB
  internals are never surfaced in the response.

- [HIGH] triageIssues now batch-loads labels (triageIssueLabelsBatch)
  and source context (triageIssueSourceContextBatch) with
  WHERE issue_id = ANY($1::uuid[]) instead of one query per issue,
  eliminating the N+1 on the triage list endpoint.

- [HIGH] optionProjects adds "completed_at is null and canceled_at is
  null" to filter dead projects for both triage routing and issue
  creation — offering them as targets only produced validation errors.

- [HIGH] settings_members.go (~253) now checks errors.As(*triageValidationError)
  before using the message as a 400 title; raw DB errors become 500s
  instead of leaking DB internals into the response. validate* helpers
  return *triageValidationError for user-visible messages.

- [MEDIUM] triageSourceContext: renamed local var "context" to "out"
  to stop shadowing the "context" package import.

- [LOW] validateTriageAssignee: added ::uuid cast on $2 for consistency
  with other UUID parameters in the package.

Deferred (as noted in review): OpenAPI schema tightening MEDIUM,
debounce/E2E LOWs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ntax

- Remove incorrect jira/zendesk fallback config block: both configure
  via their own admin setup forms, not env var checks
- Fix missing closing braces in handler_test.go caused by naive union
  merge of figma and intercom test blocks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Squash merge PR #711 after controller validation.
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: port google sheets scheduled sync

* fix: record google sheets oauth failures

* fix: trim google sheets worker whitespace

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add guided GitHub import foundation

* fix: use shared bool parser for GitHub imports

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Brings the released TTY terminal login redesign (#695) and the
insights-overlay opacity fix into staging, plus the rest of current main.

Conflict resolutions:
- comments/issues/labels handlers: keep both realtime sync publish (#699)
  and outbound webhook enqueue (main).
- integrations/handler.go: keep required (non-omitempty) details field and
  the HealthSummary row column; drop duplicate Metadata field.
- teams/triage.go: keep staging's no-writer (map,int) decision contract and
  due_date support; graft in main's issueauthz relationship validation and
  drop the duplicate tx.Begin.
- teams/handler_test.go: keep both added tests.
- settings/integrations page: use the named IntegrationDetails type, extended
  with the Google Sheets fields.

Merge fixups:
- repoint 6 integration disconnect handlers from the removed revokeProvider
  to main's disconnectProvider (arg order swapped).

Migration reconciliation:
- allowlist the parallel-branch 0010/0011 duplicate prefixes in
  check-migrations.mjs (same convention used for 0006-0008).
- add 0012_customer_requests_reconcile.sql to unify the customer /
  customer_request schema introduced independently by the Gong ingestion
  (0010) and the customer-requests CRM (0011): make Gong-only provider/excerpt
  columns nullable so CRM inserts succeed, and guarantee the superset columns
  and upsert indexes regardless of apply order.

make check passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ity (#726)

- fix hardcoded /home/jaeyunha path -> $HOME/wt (was broken on macOS)
- fix npm install -> pnpm install (this is a pnpm monorepo)
- add remote-branch support (origin/foo: fetch + upstream tracking)
- flatten '/' in branch names for the worktree dir
- resolve repo root via git rev-parse (works from any cwd)
- copy .codex/.agents alongside .claude; clean up + bail on install failure
- add EXPONENTIAL_WORKTREE_OVERRIDE_BASE override

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Focused controller QA passed: apps/api go test ./..., import/export Vitest 5/5, diff-check clean.
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Hermes Controller <hermes-controller@users.noreply.github.com>
Controller-validated squash merge to staging for #588.
Squash merge PR #738 to staging after controller validation.

Evidence: make check && make test passed; focused Go agentruns/http and OpenAPI/migration/API-boundary guards passed; current-architecture diff only.

Closes #591.
Co-authored-by: Hermes Conductor <conductor@local>
* fix: revert web drizzle runtime

Remove the web-side Drizzle/Better Auth runtime path reintroduced by the staging release and restore the last successfully deployed web architecture. Verified with web build, make check, make test, and infra/docker/web.Dockerfile build.

* Revert "fix: revert web drizzle runtime"

This reverts commit c1246a4.

* fix: repair stripe billing release path

* fix: remove web auth drizzle runtime

* fix: enforce relationship authz

* docs: refresh codebase documentation

* feat: create SNS alarm topic and wire ALARM_TOPIC_ARN into autoscaling alarms

- prepare-ecs-deploy-env.sh: when ALARM_EMAIL is set, idempotently create
  an SNS topic (${APP_NAME}-alarms) and subscribe the operator email; store
  the resulting ARN as ALARM_TOPIC_ARN in .env and print a confirmation
  reminder about the required subscription email. Falls back gracefully when
  neither ALARM_EMAIL nor ALARM_TOPIC_ARN is provided.
- deploy-ecs.sh: export ALARM_TOPIC_ARN before invoking
  configure-ecs-autoscaling.sh so the variable is available in the
  subprocess even when it was not set in the calling shell.
- check-deploy-scripts.mjs: add guard assertions verifying that the SNS
  wiring (sns create-topic, sns subscribe, ALARM_EMAIL, ALARM_TOPIC_ARN) is
  present in prepare-ecs-deploy-env.sh and that deploy-ecs.sh exports
  ALARM_TOPIC_ARN.

Closes #638

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: overhaul README as product landing page

Add quickstart one-liner, comparison table vs Linear/Jira/Plane,
workflow GIF placeholder with instructions, MCP and CLI sections,
ELv2 license statement, and set GitHub repo topics via API.

Closes #643

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: automate HTTPS listener, ACM cert wiring, and HTTP-to-HTTPS redirect in ECS preflight

Extends scripts/preflight.sh to consume ACM_CERT_ARN: when set, creates an
HTTPS:443 ALB listener with TLS 1.3 policy wired to the existing target groups,
converts HTTP:80 to a permanent 301 redirect, and routes /api/* on the HTTPS
listener to the Go API. Runs are idempotent — existing listeners are detected
and updated. Documents the full ACM certificate request, DNS validation (manual
and Route 53-automated), and app URL configuration steps in docs/self-hosting.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: publish pre-built GHCR images and image-based compose path

Adds a GitHub Actions workflow that builds and pushes multi-arch
(linux/amd64 + linux/arm64) exponential-api and exponential-web images
to GHCR on every release, tagged with the release version and `latest`.
Introduces docker-compose.images.yml as the recommended self-hosting
path (no build step, <5 min to running instance), and updates
docs/self-hosting.md to present the image-based quick start first.
Adds IMAGE_TAG to .env.example for version pinning.

Closes #634

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: make Stripe secrets optional for non-billing ECS deployments

Self-hosted community deployments are blocked mid-script by Stripe secrets
they don't have and don't need. This change makes STRIPE_WEBHOOK_SIGNING_SECRET
and STRIPE_SECRET_KEY (plus the price-ID vars) optional throughout the ECS
deploy path:

- prepare-ecs-deploy-env.sh: guard Stripe secret upserts behind presence
  checks so the script succeeds without any Stripe configuration
- render-ecs-task-definitions.mjs: treat Stripe ARN and price-ID vars as
  optional; prune their task-definition entries when absent rather than
  rendering empty ARNs that ECS would reject
- render-ecs-task-definitions.test.mjs: cover both billing-enabled and
  non-billing rendering modes
- docs/self-hosting.md: document that Stripe is billing-only for ECS

Closes #636

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add semver release workflow, expn --version, changelog, and tag script

- Add .github/workflows/release.yml: tag-triggered GitHub Release pipeline
  that validates package.json versions match the tag, runs tests, builds
  tarballs, publishes both SDK and CLI to npm (with provenance), generates
  per-release changelog notes, and creates a GitHub Release with tarballs.
- Add scripts/tag-release.sh: guards (main branch, clean tree, version match)
  then creates an annotated tag and pushes it to trigger the release workflow.
- Add expn --version / expn version command that reads the installed version
  from package.json at runtime, no token required.
- Add CHANGELOG.md documenting the v0.1.0 initial release and future release
  cadence, following Keep a Changelog format.
- Update .github/workflows/publish-cli.yml header to note it is the manual/
  emergency path; normal releases use tag-release.sh + release.yml.
- Update .github/workflows/README.md with full release runbook.
- Add 3 new tests for --version flag (with EXPN_VERSION test seam, without
  token requirement, semver format assertion).

Closes #639

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add generic SMTP email provider for self-hosted magic-link sign-in

Adds a plain SMTP sender to apps/api/internal/email alongside SES and
Opensend so self-hosters can use any SMTP relay (Mailhog, Mailgun,
Postmark, Gmail app password, etc.) without an AWS account or Opensend
key.

- New smtpSender: STARTTLS (port 587 default) and implicit-TLS (port
  465, SMTP_TLS=true) transports; optional SMTP_USERNAME/SMTP_PASSWORD
  AUTH PLAIN; graceful no-auth path for loopback relays (Mailhog).
- Auto-selection order: smtp (SMTP_HOST set) > opensend
  (OPENSEND_API_KEY) > ses (SENDER_EMAIL) > Disabled.
- EMAIL_PROVIDER=smtp|ses|opensend explicit override still works.
- docker-compose.dev.yml: wire api to Mailhog via SMTP envs so the
  magic-link flow works out-of-the-box in the dev stack.
- .env.example: document SMTP_HOST/PORT/USERNAME/PASSWORD/TLS vars.
- docs/self-hosting.md: document provider selection order, SMTP
  quick-start, and Mailhog dev recipe.
- 11 new Go table tests covering provider selection, Mailhog delivery,
  raw-message building (multipart and HTML-only paths), and edge cases.

Closes #635

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): add write tools to local MCP server (create/update issue, comment, triage)

Extends packages/mcp-server beyond read-only with four write tools:
- create_issue: POST /issues with title, team_id and optional fields
- update_issue: PATCH /issues/{id} for status/priority/assignee/labels
- add_comment: POST /issues/{id}/comments
- triage_issue: PATCH /teams/{key}/triage/{issueID} with accept/decline action

All write tools are annotated readOnlyHint:false; triage_issue also sets
destructiveHint:true. Input schemas use strict Zod validation. Errors map
API problem JSON to MCP tool errors while redacting auth fields.

Read-only tool annotations and registration are unchanged; the tool
registration loop now reads annotations from each ToolDefinition.

Tests updated: removed read-only-only assertion, added per-annotation check
and happy-path tests for all four write tools (12 tests total, all passing).

docs/mcp.md updated to document write tools, scope, and follow-up work.

Closes #640

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add integration platform lifecycle, health, secrets, and job substrate

Implements the integration runtime substrate (issue #568):
- Migration 0007: adds lifecycle_state, health timestamps, credential_ref,
  disconnected_at, and integration_job table with backoff/retry fields
- Go: lifecycle helpers (BackoffDuration, TransitionJobStatus, IsTerminal),
  HealthInfo struct serialized without any secret/credential fields,
  disconnectProvider that revokes credentials and disables jobs while
  preserving historical links (replaces hard-delete)
- OpenAPI: adds IntegrationHealth schema; Integration gains nullable health field
- Web: settings/integrations page shows lifecycle badge, last event/success/
  failure timestamps, health summary, and reconnect/disconnect CTAs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: address PR #655 review findings on README security, build time, and ECS command

- Move security callout above the quickstart one-liner and strengthen it
  to warn before network connections, not only before sharing
- Add inline comment and build-time note (~15 min, ~8 GiB RAM) with
  reference to #634 for the image-based path
- Add bash prefix to RUN_PROD_SMOKE=true scripts/deploy-ecs.sh for
  consistency with the two preceding lines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: implement outbound webhook delivery with signing, retry, and admin visibility

- Add webhook_delivery table (migration 0007) tracking delivery attempts,
  status, response codes, retry schedule, and dead-letter state
- New apps/api/internal/webhooks package: HMAC-SHA256 signing, outbound HTTP
  delivery, exponential backoff retry (5 max attempts), dead-letter state,
  and EnqueueEvent/ProcessPending helpers
- Emit webhook events fire-and-forget after issue, comment, and label mutations
  (created/updated/deleted) so they never block the user response path
- Add /webhook-deliveries admin endpoint (list + per-delivery retry) requiring
  manager role
- Validate webhook event types against known list on createWebhook
- Expose supportedWebhookEvents in workspace API payload
- Background delivery processor ticker (10s interval) wired into main.go
- 11 unit tests covering signing, verification, backoff, HTTP delivery (success,
  4xx, 5xx, network error), event type validation, and helpers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: harden HTTPS preflight against unvalidated certs and downgrade idempotency

- Exit 1 (was WARNING+continue) when ACM cert status is not ISSUED; message now
  explicitly warns that HTTP:80 would also stop working via the 301 redirect.
- Guard create-listener return value against 'None' before passing ARN to
  ensure_listener_rule, preventing a broken partial-state after HTTP is already
  converted to a redirect.
- In HTTP-only mode, delete any orphaned HTTPS:443 ALB listener left from a
  previous HTTPS run and remove stale ALB_HTTPS_LISTENER_ARN from .env via a
  new del_env_file helper, restoring true downgrade idempotency.
- docs/self-hosting.md: replace the append-only 'echo >> .env' with a
  grep+sed in-place edit pattern consistent with set_env_file, preventing
  duplicate ACM_CERT_ARN keys on re-runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address PR #657 review findings — scoped GHA cache, workflow concurrency, pinned .env ref

- Add scope=api / scope=web to cache-from and cache-to in publish-images.yml
  so parallel build-api and build-web jobs use isolated GHA cache buckets and
  cannot evict each other's layers.
- Add top-level concurrency group (publish-images, cancel-in-progress: false)
  to prevent simultaneous workflow runs from racing the same GHCR tags.
- Update docs/self-hosting.md quick-start curl to fetch .env.example from the
  pinned IMAGE_TAG ref rather than main, so variable sets stay aligned with the
  image being deployed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address review findings on release engineering PR

- Scope --version flag check to rawArgs[0] only, preventing `expn issues
  watch --version N` from being intercepted and printing the CLI version
  instead of running the watch command.
- Fix GitHub Actions tag glob patterns from POSIX-regex `+` quantifiers
  (invalid in minimatch) to `*` wildcards so the release workflow
  actually fires on real semver tags like v1.2.3.
- Strengthen tag-release.sh clean-tree guard from `git diff --quiet HEAD`
  (misses untracked files) to `git status --porcelain` so untracked
  secrets or generated files are caught before a release tag is pushed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): address review findings on triage_issue handler and write-tool tests

- Replace optionalStringField cast with stringField for required `action` field (finding #1)
- Replace unsafe priority cast with new optionalNullableIssuePriority helper (finding #2)
- Use WRITE_ANNOTATIONS instead of destructiveHint:true for triage_issue (finding #3)
- Add request body assertions to all four write-tool happy-path tests (finding #4)
- Add rejection test for triage_issue missing required action field (finding #5)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: correct SMTP quoted-printable encoding, LOGIN auth fallback, misconfiguration error, and test races

- Use mime/quotedprintable.NewWriter to properly encode text/plain and
  text/html body parts; fixes protocol violation for non-ASCII content
  and lines >998 bytes that would produce corrupt messages.
- Replace smtp.PlainAuth-only auth with smtpAuth() helper that inspects
  the server's AUTH advertisement and falls back to AUTH LOGIN for
  shared-hosting relays (cPanel, some Gmail SMTP configs) that only
  advertise LOGIN; update the doc comment to reflect actual behaviour.
- Return a descriptive error (not Disabled{}) when EMAIL_PROVIDER=smtp
  is explicit but SMTP_HOST or SENDER_EMAIL is missing, so the warning
  path in router.go is actually reached.
- Fix TestSMTPSendAgainstMailhog race: send to delivered channel before
  writing "221 Bye" so the channel is populated before smtp.Client.Quit()
  returns; replace non-blocking default select with a 5s timeout.
- Replace conn.Read(buf) in mock SMTP server with bufio.Scanner for
  per-line reading, preventing TCP-coalesced multi-command reads from
  silently dropping commands on fast machines.
- buildRawMessage now returns ([]byte, error) to propagate QP encoder
  failures up the call stack.
- Add TestSMTPBuildRawMessageNonASCII to catch the QP encoding regression.
- Add TestNewSMTPExplicitMissingHostReturnsError and
  TestNewSMTPExplicitMissingFromReturnsError for the new error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address PR #663 review findings for outbound webhooks

- CRITICAL: wrap FOR UPDATE SKIP LOCKED and status='delivering' UPDATE in
  an explicit pgx transaction in ProcessPending so row locks are held across
  both statements, preventing duplicate deliveries on concurrent workers
- CRITICAL: add OpenAPI spec entries for GET /workspaces/current/webhook-deliveries
  and POST /workspaces/current/webhook-deliveries/{id}/retry plus WebhookDelivery
  schema so check-openapi-coverage passes
- HIGH: rename migrations to fix 0006 prefix collision — 0006_stripe_webhook_event
  becomes 0007, and 0007_webhook_delivery becomes 0008
- HIGH: add validateWebhookURL with SSRF protection blocking loopback, private,
  link-local, and IMDS (169.254.169.254) addresses; add SSRF test coverage
- MEDIUM: move /webhook-deliveries mount to /workspaces/current/webhook-deliveries
  matching established workspace-management URL pattern
- MEDIUM: extract isManager to auth.IsManager shared helper, removing the
  duplicate definition in webhooks/handler.go and delegating workspaces/handler.go

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: renumber integration lifecycle migration from 0007 to 0008

The outbound webhooks branch (PR #663) introduced a 0007_stripe_webhook_event.sql
rename from the existing 0006_stripe_webhook_event.sql (which is already applied
on all deployed databases). Even after that branch is corrected to restore 0006 and
use 0007 for webhook_delivery, the integration lifecycle migration needs to be 0008
to sit above the webhook delivery migration and avoid a version-key collision in the
migration runner (which uses filepath.Base as the unique key).

- Rename packages/proto/migrations/0007_integration_lifecycle.sql
  to packages/proto/migrations/0008_integration_lifecycle.sql
- All 13 integrations package tests pass; go build ./... clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address remaining PR #663 review findings (migration rename + data race)

DESTRUCTIVE MIGRATION RENAME (finding #2):
- Restore packages/proto/migrations/0006_stripe_webhook_event.sql to its
  original name. The previous fix commit incorrectly renamed it to
  0007_stripe_webhook_event.sql, which would cause the migration runner
  (filepath.Base as version key) to re-execute it on any DB that already has
  "0006_stripe_webhook_event.sql" applied, creating a ghost version record and
  breaking migration history.
- Rename 0008_webhook_delivery.sql back to 0007_webhook_delivery.sql since
  0006_stripe_webhook_event.sql is now restored to its correct slot.

DATA RACE on skipSSRFValidation (finding #3):
- Remove the package-level `var skipSSRFValidation atomic.Bool` global.
- Add `skipSSRFCheck bool` field to the Deliverer struct, a
  `WithSSRFCheckDisabled()` functional option, and a `NewDeliverer` constructor.
- Change `sendWebhookRequest` to accept an explicit `skipSSRFCheck bool`
  parameter, eliminating all shared mutable state between parallel tests.
- Update deliver_test.go: remove `setupSSRFBypass` helper and pass `true`
  directly to sendWebhookRequest in tests that use httptest 127.0.0.1 servers.
- All 12 webhooks tests pass under `go test -race`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: make Stripe ECS deploy vars optional with all-or-none coherence check

- deploy-ecs.sh: move Stripe vars out of unconditional require_env loop;
  add all-or-none coherence check treating empty strings as absent so
  non-billing operators can deploy without Stripe vars set
- prepare-ecs-deploy-env.sh: guard existing_or_synced_secret_arn against
  syncing an empty value over a pre-existing secret ARN (SYNC_DEPLOY_SECRET_VALUES=true)
- render-ecs-task-definitions.test.mjs: add environment assertions for
  STRIPE_CLOUD_*_PRICE_ID pruning in the fully-absent-Stripe test; replace
  delete statements with destructuring rest to satisfy Biome noDelete rule

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): satisfy Biome useLiteralKeys and formatting in server.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: apply Biome formatting to integrations settings page

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): apply Biome formatting to server.test.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(integrations): update view tests to match new page UI contract

The new integrations page shows configuration_required integrations as
active cards on the main page (not behind an empty state), so update
the test fixtures and assertions to match the actual rendered contract.

- Give Slack canConnect:true so the Connect button renders (tests real
  actionability, not just presence of a card)
- Assert both provider cards appear on the main page after load instead
  of checking for the old "No active integrations" empty state text
- Assert setup requirement messages surface inline (anti-placeholder
  check preserved with equivalent strength)
- Keep the Slack connect API error test intact: only update the load
  anchor (findByText("Slack") instead of the removed empty-state text)
  and remove the now-unnecessary dialog-open step; error surfacing via
  role="alert" is unchanged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: remove accidentally committed .release tarballs and ignore the directory

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: align webhook event names after release merge

* test: stabilize auth diagnostics during e2e

* fix: keep insights overlay opaque in light theme

* fix: account for applied migration prefixes

* style: format migration guard

* fix(web): restore TTY terminal login design (#695)

* fix(web): restore TTY terminal login design

The faithful TTY/terminal auth surface shipped in 0b723a6 was silently
overwritten by the staging merge 94e8a7f, which resurrected origin/main's
stale rounded-pill "chooser" auth-page during a path relocation. Follow-up
commits then realigned tests around the regressed chooser, cementing it.

Re-skin auth-page.tsx back to the redesign.html `TtyLogin` mock — two-column
terminal layout, top chrome bar, sharp bordered method tiles, preflight
doctor panel (real data), recent-sessions table, CLI pairing block, and the
vim-style command bar — WITHOUT regressing auth behavior. All current logic
is preserved: Google OAuth, email magic-link, SAML SSO, passkey, Turnstile,
signup wizard, workspace slug-check, recent-sessions/preflight fetches.

The right column no longer collapses to an empty void (preflight + CLI
pairing always render). Uses --auth-* tokens, works in dark and light.

Validation:
- make check, make test
- web vitest: 1069 passed
- e2e (unauth project, live stack): 20 passed
- visual: dark, light, and email step verified

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(web): make TTY auth surface fill the viewport height

The root used h-full inside a min-h-screen flex-col layout, so it
collapsed to content height — leaving a black void below and the
command bar floating mid-screen. Use flex-1 so it grows to fill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(web): gate login to Google only, mark other methods coming soon

Disable email magic-link, SAML SSO, and passkey buttons on the auth
chooser and add a "coming soon" caption to each; Google OAuth remains
the only active sign-in method. Underlying handlers are unchanged — only
the UI entry points are gated.

Update tests to the new contract: assert Google stays enabled while
email/SAML/passkey are disabled and show "coming soon"; remove e2e flows
that drove the now-disabled email step.

Validation: make check, make test, web vitest (1068), e2e unauth (18).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): don't block deploy when runner status is unreadable (#697)

The runner precheck added in #670 calls the GitHub self-hosted runners
API, which requires repository Administration access. With no
GH_RUNNER_STATUS_TOKEN configured it falls back to the default
GITHUB_TOKEN and gets 401/403, hard-failing every deploy since Jun 15 —
even though the Mac mini runner is online.

Treat an auth failure (401/403) as "cannot verify" and skip the precheck
with a warning instead of failing the deploy. The gate stays strict when
a capable token is present, and the deploy job still fails safely if no
runner picks it up. Set GH_RUNNER_STATUS_TOKEN to re-enable the gate.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat: add GitHub App integration ingress (#696)

* feat: add github app integration ingress

* fix(integrations): correct GitHub connect redirect and webhook 202 for no-installation events

- Web: read installationUrl (not authorizationUrl) when provider is
  github so Connect/Reconnect actually redirects to the GitHub App
  installation page
- Web: handle ?github=connected / ?github=canceled query params on
  return from GitHub so a success/error notice is shown and the URL
  is cleaned up
- API: return HTTP 202 with ignored=no_installation_context (instead
  of 400) for webhook events that carry no installation field (e.g.
  ping, github_app_authorization), matching the existing
  unknown_installation path so GitHub marks deliveries as succeeded

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: reconcile GitHub integration release batch

* test: scope integration connect action

* ci: add PR validation workflow (build + Go tests + contract guards)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
#743)

* perf(web): seed page data on the server to cut the hydration waterfall

Authenticated content pages fetched their own data in the browser via a
post-hydration useEffect, so content-visible = TTFB + full-shell hydration
+ a client round-trip. Convert the data pages to the RSC server-seeding
pattern: an async server component fetches via the server SDK and seeds the
client component's initial state, so content ships in the first paint.

Pages converted (server helper + seeded client + async page):
- team/[key]/all and board (getTeamIssues)
- inbox (getInboxNotifications)
- my-issues/[tab] (getMyIssues)
- initiatives (getInitiatives)
- projects + projects/all (getProjects)
- settings/members (getWorkspaceMembers)

Also fixes the projects slug-variant routes, which were their own
non-seeded files rather than re-exports of the converted canonical page.

Supporting hydration/latency cuts:
- app-shell: dynamic-import the always-mounted overlays (ask-assistant,
  command-palette, create-issue-modal) with ssr:false to shrink the
  hydration-blocking bundle.
- layout: overlap the /workspaces/current and /teams round-trips.
- web-session: short-circuit anonymous visitors (no session cookie) instead
  of paying an /auth/session round-trip.
- auth pages: drop force-dynamic.

Result (local expperf stack): every page now emits fully data-seeded HTML
in ~13-20ms server-render (curl TTFB), with the client-fetch waterfall
eliminated. Server helpers use runtime type guards (no casts on API data);
a server miss returns null and degrades gracefully to the client fetch.

Adds perf measurement scripts under apps/web/scripts. Typecheck, biome, and
the full Vitest suite (1085 tests) pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(web): biome-format perf measurement scripts

Fix Biome lint/format errors (noUnusedTemplateLiteral, useNumberNamespace,
formatting) in the perf-measure scripts that broke the Node CI check on #743.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): point members SDK-usage guard at the post-seeding file layout

The members settings surface was split into an RSC page.tsx (server-SDK
seeding) plus members-client.tsx (browser SDK client). Update the guard to
require createBrowserApiClient in the client file and ban direct member/invite
fetches across both files, preserving the guard's intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d191ed354

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export async function getWorkspaceMembers(
userId: string,
): Promise<WorkspaceMembersResponse | null> {
const workspaceId = await resolveActiveWorkspaceId(userId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor the URL workspace when seeding members

When a user opens a workspace-prefixed route such as /acme/settings/members while their activeWorkspaceId cookie still points at another workspace, this resolves the cookie-selected workspace and later sends it as X-Workspace-Id. The API's requestedWorkspace() path gives X-Workspace-Id precedence over the forwarded X-Workspace-Slug, and MembersClient skips its browser fetch when initialData is present, so the page can display and manage the wrong workspace's members/invites under the requested workspace URL.

Useful? React with 👍 / 👎.

Comment on lines +182 to +184
if (seededRef.current) {
seededRef.current = false;
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Still load project labels and templates after seeding

When initialProjects is provided by the new server page, this early return prevents fetchProjects() from running, so the follow-up /api/project-labels and /api/project-templates requests never happen on mount. As a result the label filter and the create-project form's label/template selectors remain empty until a later sync event or create action happens to trigger a full client fetch.

Useful? React with 👍 / 👎.

@jaeyunha

Copy link
Copy Markdown
Member Author

Controller release validation update for head 4d191ed3:

  • make check passed from /home/jaeyunha/wt/exponential/staging-main.
  • make test passed from the same release worktree.
  • GitHub CI is green for Go API, Node, and non-blocking Web typecheck.
  • make test-e2e is not clean yet. First run confirmed a harness/env issue: Playwright web preflight inherited a stale whetline database URL. Rerun with Exponential .env, EXPONENTIAL_API_URL=http://localhost:7016/v1, and system Chrome progressed into the suite but had to be stopped after ~12 minutes with failing app assertions/artifacts already present: command palette dialog not opening, issue mention insertion order mismatch, inbox snooze row still visible, issue relation action timeout, plus one browser channel-closed failure.

Disposition: release remains blocked; not merge-ready until the E2E failures are reduced to accepted flakes or fixed and the full suite passes. This is not active implementation capacity (capacityDisposition=not-active-worker-capacity).

@jaeyunha jaeyunha merged commit 177dba5 into main Jun 26, 2026
6 checks passed
@jaeyunha

Copy link
Copy Markdown
Member Author

Post-merge deploy blocker for merge commit 177dba5f:

Evidence:

Blocker: production deploy is not healthy until the RED metrics smoke path is restored or the deploy smoke contract is updated intentionally.

Next: controller should route a focused deploy-smoke fix/retry before treating the release as production-complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant