Skip to content

fix: support arm64 deploy storage config#746

Open
jaeyunha wants to merge 2 commits into
stagingfrom
deploy-opensend-r2-arm64
Open

fix: support arm64 deploy storage config#746
jaeyunha wants to merge 2 commits into
stagingfrom
deploy-opensend-r2-arm64

Conversation

@jaeyunha

@jaeyunha jaeyunha commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • switch ECS task definitions and deploy builds to ARM64
  • add optional opensend email and R2/S3-compatible storage deploy wiring
  • ignore generated local GJC runtime state for tooling

Verification

  • make check
  • make test (fails: existing web unit expectations for exponential.app/ prefix; rendered app shows localhost:7015/ in tests)

jaeyunha and others added 2 commits June 26, 2026 14:52
* feat: connect Gong call findings to issues (#690)

* 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 (#694)

* 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 (#693)

* 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 (#692)

* 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 (#691)

* 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 (#689)

* 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>

* Merge remote-tracking branch 'origin/staging' into issue-580-p1-jira-jira-cloud-server-sync-plu

* feat: add Figma issue previews (#686)

* 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 (#698)

* 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>

* fix(integrations): correct setupRequirement union and handler_test syntax

- 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>

* fix: persist workspace agent runs (#711)

Squash merge PR #711 after controller validation.

* feat: wire realtime sync subscriptions (#699)

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

* feat: add customer request model (#709)

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

* P3 Google Sheets scheduled export sync (#704)

* 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 provider imports (#706)

* 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>

* chore(hack): upgrade create_worktree.sh to opensend/forever-agent parity (#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>

* fix: harden Jira sync import flow (#714)

Focused controller QA passed: apps/api go test ./..., import/export Vitest 5/5, diff-check clean.

* fix: encrypt provider credentials (#729)

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>

* feat: add SCIM provisioning endpoints (#730)

Closes #529

* feat: add SAML SSO support (#731)

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>

* fix: wire GitHub OAuth provider (#734)

Co-authored-by: Hermes Controller <hermes-controller@users.noreply.github.com>

* Rebuild Airbyte read-only source connector (#736)

Controller-validated squash merge to staging for #588.

* P2 AI Agents integration surfaces (#738)

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.

* feat: add Zapier app contract (#739)

Co-authored-by: Hermes Conductor <conductor@local>

* ci: add PR validation workflow (#723)

* 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>

* fix: stabilize staging QA validation

* perf(web): seed page data on the server to cut the hydration waterfall (#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>

---------

Co-authored-by: jaeyunha <jaeyunha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Hermes Controller <hermes-controller@users.noreply.github.com>
Co-authored-by: Hermes Conductor <conductor@local>
Co-authored-by: Conductor <conductor@example.invalid>
Co-authored-by: Hermes Conductor <hermes-conductor@users.noreply.github.com>
@jaeyunha

jaeyunha commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Controller disposition for current head 257cf67e: blocked before autonomous merge.

Evidence: PR targets staging and CI is green (Go API, Node, web typecheck); focused static deploy-script checks passed (git diff --check, bash -n for deploy/preflight/env scripts, node --check for render/SDK guard scripts). Diff against origin/main is the narrow ARM64/R2 deploy-config change.

Blocker: this branch is based on current main/release commit, while origin/staging is still at an older release point, so the PR diff against its actual base replays the release batch plus production deploy configuration. Because it changes production deploy/ECS config, Conductor is routing this to human QA/release-base confirmation instead of autonomous staging merge.

Next: Jaeyun should confirm whether to retarget/merge this as a main hotfix or first reconcile staging to the current release base, then rerun the focused deploy checks before merge.

@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: 257cf67eca

ℹ️ 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 getProjects(
userId: string,
): Promise<ProjectsResponse | 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 Seed SSR data from the requested workspace

The proxy sets x-workspace-slug for workspace-prefixed routes and AppLayout honors it, but this new SSR seed path calls resolveActiveWorkspaceId(), which ignores that header and chooses the activeWorkspace* cookies, then sends that cookie-derived ID as X-Workspace-Id. When a user opens a workspace-prefixed page such as /other-workspace/projects while their cookies still point at a different workspace, the shell renders the requested workspace but the seeded client skips its fetch and displays the other workspace's data; the sibling seeded inbox/initiatives/members/team helpers use the same pattern.

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 after SSR seeding

When initialProjects is non-null, this early return skips fetchProjects(). The server seed only populates the projects array, while fetchProjects() is the only code that loads project labels and project templates, so the SSR-seeded projects pages render with an empty label filter and no template selector until a later sync/create happens to refetch them.

Useful? React with 👍 / 👎.

Comment on lines +252 to +254
if [ -n "${R2_ACCESS_KEY_ID:-}${R2_ACCESS_KEY_ID_SECRET_ARN:-}" ]; then
set_env R2_ACCESS_KEY_ID_SECRET_ARN "$(existing_or_synced_secret_arn R2_ACCESS_KEY_ID_SECRET_ARN "${APP_NAME}/r2-access-key-id" "${R2_ACCESS_KEY_ID:-}")"
set_env R2_SECRET_ACCESS_KEY_SECRET_ARN "$(existing_or_synced_secret_arn R2_SECRET_ACCESS_KEY_SECRET_ARN "${APP_NAME}/r2-secret-access-key" "${R2_SECRET_ACCESS_KEY:-}")"

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 Reject partial R2 credentials before syncing

If an operator provides only R2_ACCESS_KEY_ID/R2_ACCESS_KEY_ID_SECRET_ARN and forgets R2_SECRET_ACCESS_KEY/R2_SECRET_ACCESS_KEY_SECRET_ARN, this block still calls existing_or_synced_secret_arn for the secret access key with an empty raw value and then writes an ARN. That can bypass deploy-ecs.sh's all-or-none validation and leave the ECS task with an empty AWS_SECRET_ACCESS_KEY (or fail here with an opaque Secrets Manager error), so partial R2 config should be rejected before syncing either secret.

Useful? React with 👍 / 👎.

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