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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .agents/skills/test-t3-mobile/scripts/pair-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ const query = new URLSearchParams({
pairingUrl: process.env.PAIRING_URL,
autoConnect: "1",
});
process.stdout.write(`t3code-dev://connections/new?${query}`);
process.stdout.write(`ottercode-dev://connections/new?${query}`);
NODE
)"

if ! "$agent_device_command" open com.t3tools.t3code.dev "$deep_link" "$@" \
if ! "$agent_device_command" open dev.otterware.code.dev "$deep_link" "$@" \
>/dev/null 2>&1; then
echo "AgentDevice could not open the pairing route. Check the Device panel and retry with a fresh credential." >&2
exit 1
Expand Down
39 changes: 10 additions & 29 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
# Optional: T3 Connect source builds
# `cp .env.example .env` enables T3 Connect against the production deployment.
# These are the same public identifiers baked into official release builds, not
# secrets. Remove or comment them out to build with cloud features disabled.
# Do not add server-side secrets to this file.

# Production Clerk instance. To use your own, get these from the Clerk Dashboard
# under API keys, JWT templates, and OAuth applications.
T3CODE_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsudDMuY29kZXMk
T3CODE_CLERK_JWT_TEMPLATE=t3-relay
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=hzxSgY2cH10sDU2r

# Optional: signed macOS passkey builds. The RP domain defaults to the Frontend API
# hostname encoded in T3CODE_CLERK_PUBLISHABLE_KEY. Set the override only when Clerk
# returns a different RP ID or when multiple domains must be entitled.
# T3CODE_APPLE_TEAM_ID=ABC1234567
# T3CODE_MACOS_PROVISIONING_PROFILE=/absolute/path/to/t3code.provisionprofile
# T3CODE_CLERK_PASSKEY_RP_DOMAINS=example.clerk.accounts.dev,clerk.example.com

# Production relay. For a self-hosted relay, `infra/relay` deploys update it
# automatically.
T3CODE_RELAY_URL=https://relay.t3.codes

# Optional: hosted app origin used by the CLI's out-of-band OAuth flow.
# Defaults to https://app.t3.codes; override to test against a staging deployment.
# T3CODE_HOSTED_APP_URL=https://nightly.app.t3.codes

# Public, ingest-only mobile OpenTelemetry configuration.
# Public Otter Accounts and Connect defaults are built into Otter Code.
# Override these together only for a separately registered staging deployment.
# Server-side credentials do not belong in this file.
T3CODE_ACCOUNTS_URL=https://accounts.otterware.app/v1/auth
T3CODE_ACCOUNTS_CLI_CLIENT_ID=otter-code-cli
T3CODE_RELAY_URL=https://relay.code.otterware.app
T3CODE_HOSTED_APP_URL=https://code.otterware.app

# Optional public, ingest-only mobile tracing configuration.
# T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces
# T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev
# T3CODE_MOBILE_OTLP_TRACES_DATASET=otter-code-mobile-traces-dev
# T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-...
156 changes: 156 additions & 0 deletions .github/scripts/otter-adapt-upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# Adapts upstream files to the fork. Rerun this after every sync rebase and
# commit the result last in the fork stack (see OTTER.md), so upstream edits to
# these lines never cause rebase conflicts. Each edit is a no-op if upstream
# renames what it matches.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"

# Blacksmith runners -> GitHub-hosted runners.
# macOS jobs use macos-15: GitHub's macos-26 image rejects electron-builder <26.16.1's
# keychain unlock (electron-userland/electron-builder#10066). Move to macos-26 once
# upstream pins electron-builder 26.16.1 or later.
find .github -name '*.yml' -print0 | xargs -0 sed -i.bak -E \
-e 's/blacksmith-[0-9]+vcpu-ubuntu-2404/ubuntu-24.04/g' \
-e 's/blacksmith-[0-9]+vcpu-macos-26/macos-15/g' \
-e 's/blacksmith-[0-9]+vcpu-windows-2025/windows-2025/g'
find .github -name '*.yml.bak' -delete

# Upstream sizes release job timeouts for 8-vCPU Blacksmith runners; the 4-vCPU
# GitHub-hosted runners need about twice as long (the release test suite alone
# takes ~10 minutes there).
perl -pi -e 's/^( timeout-minutes: )10$/${1}20/' .github/workflows/release.yml

# Otter Code ships macOS and Linux only: skip the Windows desktop jobs, let the
# release proceed without them, and stop expecting .exe assets.
perl -0pi -e '
s/(\n desktop_win_(?:x64|arm64):\n(?: .*\n)*? if: )\$\{\{.*?\}\}/$1\${{ false }}/g;
s/needs\.desktop_win_(x64|arm64)\.result == \x27success\x27/needs.desktop_win_$1.result != \x27failure\x27/g;
s/\n[ \t]*echo \x27release-assets\/\*\.exe\x27//g;
' .github/workflows/release.yml

# Keep Cloudflare hosting at the fork's deployment boundary. Upstream retains
# its Vercel implementation; Otter stages a Worker version and promotes it only
# after the desktop/CLI release exists.
python3 - <<'PY'
from pathlib import Path
import re

def fork_block(action):
return f'''\n if [[ "${{GITHUB_REPOSITORY}}" == "otterware-app/otter-code" ]]; then
node scripts/otter/web-hosting.mjs {action}
exit 0
fi
'''

def adapt_job(text, name, action, entries):
pattern = rf"(^ {name}:\n.*?)(?=^ [a-zA-Z0-9_]+:\n|\Z)"
match = re.search(pattern, text, re.M | re.S)
if not match:
raise SystemExit(f"Missing upstream hosting job: {name}")
job = match[0]
# Replace the previous Otter-only Vercel promotion guard during migration.
job = re.sub(r'\n if \[\[ "\$\{GITHUB_REPOSITORY\}" == "otterware-app/otter-code" \]\]; then\n node scripts/otter/promote-web.mjs\n exit 0\n fi\n', '', job)
if f"node scripts/otter/web-hosting.mjs {action}\n" not in job:
anchor = " set -euo pipefail\n"
if anchor not in job:
raise SystemExit(f"Missing upstream hosting step: {name}")
job = job.replace(anchor, anchor + fork_block(action), 1)
for section, key, value in entries:
if f" {key}:" not in job:
job = job.replace(f" {section}:\n", f" {section}:\n {key}: {value}\n", 1)
return text[:match.start()] + job + text[match.end():]

release_path = Path('.github/workflows/release.yml')
release = release_path.read_text()
release = adapt_job(release, 'build_web', 'stage', [
('env', 'CLOUDFLARE_API_TOKEN', '${{ secrets.CLOUDFLARE_API_TOKEN }}'),
('env', 'APP_VERSION', '${{ needs.preflight.outputs.version }}'),
('env', 'VITE_HOSTED_APP_CHANNEL', "${{ needs.preflight.outputs.release_channel == 'stable' && 'latest' || 'nightly' }}"),
('outputs', 'version_id', '${{ steps.deploy.outputs.version_id }}'),
])
release = adapt_job(release, 'deploy_web', 'promote', [
('env', 'CLOUDFLARE_API_TOKEN', '${{ secrets.CLOUDFLARE_API_TOKEN }}'),
('env', 'WEB_VERSION_ID', '${{ needs.build_web.outputs.version_id }}'),
])
release_path.write_text(release)
preview_path = Path('.github/workflows/web-preview.yml')
preview_path.write_text(adapt_job(preview_path.read_text(), 'deploy', 'stage-preview', [
('env', 'CLOUDFLARE_API_TOKEN', '${{ secrets.CLOUDFLARE_API_TOKEN }}'),
]))
PY

# Keep upstream's provisioning-profile path, but make it optional for Otter's
# existing Developer ID releases. Otter Accounts has no Clerk RP domain.
python3 - <<'PY'
from pathlib import Path

path = Path('.github/workflows/release-desktop.yml')
text = path.read_text()
if ' MACOS_PROVISIONING_PROFILE:\n' not in text:
text = text.replace(' AZURE_TENANT_ID:\n',
' MACOS_PROVISIONING_PROFILE:\n required: false\n AZURE_TENANT_ID:\n', 1)
if ' MACOS_PROVISIONING_PROFILE:' not in text:
text = text.replace(' APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}\n',
' APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}\n'
' MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}\n', 1)
text = text.replace(' T3CODE_CLERK_PASSKEY_RP_DOMAINS: ${{ vars.CLERK_PASSKEY_RP_DOMAINS }}\n', '')
if ' T3CODE_MACOS_PASSKEY_RP_DOMAINS:' not in text:
text = text.replace(' MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}\n',
' MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }}\n'
' T3CODE_MACOS_PASSKEY_RP_DOMAINS: ${{ vars.MACOS_PASSKEY_RP_DOMAINS }}\n', 1)
start_anchor = ' if has_all "$CSC_LINK" "$CSC_KEY_PASSWORD" "$APPLE_API_KEY" "$APPLE_API_KEY_ID" "$APPLE_API_ISSUER"; then\n'
end_anchor = ' echo "macOS signing enabled."\n'
start = text.index(start_anchor) + len(start_anchor)
end = text.index(end_anchor, start)
text = text[:start] + ''' key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY" > "$key_path"
export APPLE_API_KEY="$key_path"

if [[ -n "$MACOS_PROVISIONING_PROFILE" ]]; then
if [[ -z "$APPLE_TEAM_ID" ]]; then
echo "MACOS_PROVISIONING_PROFILE requires APPLE_TEAM_ID." >&2
exit 1
fi
profile_path="$RUNNER_TEMP/t3code.provisionprofile"
printf '%s' "$MACOS_PROVISIONING_PROFILE" | base64 -D > "$profile_path"
security cms -D -i "$profile_path" >/dev/null
export T3CODE_APPLE_TEAM_ID="$APPLE_TEAM_ID"
export T3CODE_MACOS_PROVISIONING_PROFILE="$profile_path"
echo "macOS provisioning profile validated; entitlements selected by the packager."
else
echo "No macOS provisioning profile; signing and notarization continue without passkey entitlements."
fi

''' + text[end:]
path.write_text(text)
PY

# Point agents at the fork guide from AGENTS.md, which every agent reads first.
perl -0pi -e '
s/\A(# [^\n]*\n)/$1\n> **This is the Otter Code fork.** Read [OTTER.md](OTTER.md) first: it covers how the fork\n> stays in sync with upstream, its identities and infrastructure, releases, and moving machines.\n/
unless /OTTER\.md/;
' AGENTS.md

# Otter Code publishes its CLI with an npm token (no npm trusted publisher exists
# for new packages), and tags each nightly as latest so plain `npx otter-code` works.
OTTER_NPM_AUTH_STEP=' - name: Configure npm token (Otter Code)
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -n "$NPM_TOKEN" ]; then
printf "//registry.npmjs.org/:_authToken=%s\n" "$NPM_TOKEN" >> ~/.npmrc
fi

' OTTER_NPM_TAG_STEP='
- name: Tag nightly as latest (Otter Code)
if: needs.preflight.outputs.release_channel == '"'"'nightly'"'"'
run: npm dist-tag add "otter-code@${{ needs.preflight.outputs.version }}" latest
' perl -0pi -e '
s/( - name: Build npm packages from CLI archives\n)/$ENV{OTTER_NPM_AUTH_STEP}$1/ unless /Configure npm token \(Otter Code\)/;
s/( - name: Publish CLI packages\n run: [^\n]*\n)/$1$ENV{OTTER_NPM_TAG_STEP}/ unless /Tag nightly as latest \(Otter Code\)/;
' .github/workflows/release.yml
perl -0pi -e '
s/(node scripts\/build-npm-platform-packages\.ts [^\n]*--output-dir npm-packages)\n/$1 --allow-missing\n/
unless /--output-dir npm-packages --allow-missing/;
' .github/workflows/release.yml
24 changes: 12 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# passes only when every other job does.
lint:
name: Lint
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -57,7 +57,7 @@ jobs:

typecheck:
name: Typecheck
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand All @@ -83,7 +83,7 @@ jobs:

build:
name: Build
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
# per runner is unchanged.
test:
name: Test
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -201,7 +201,7 @@ jobs:
# started last and ran alone for about 40s after everything else finished.
test_web:
name: Test Web
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -230,7 +230,7 @@ jobs:
# split files by recorded duration (apps/server/src/testUtils/weightedShardSequencer.ts).
test_server:
name: Test Server ${{ matrix.shard }}
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
Expand Down Expand Up @@ -302,7 +302,7 @@ jobs:
# On Blacksmith like every other job here: the GitHub-hosted queue is shared
# with the PR bots and can leave this job waiting until it is abandoned,
# which fails `Check` on an otherwise green run.
runs-on: blacksmith-2vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require transfer measurements
Expand All @@ -315,7 +315,7 @@ jobs:
# for checks that take under 3s, on the critical path of every PR.
rust:
name: Rust
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -349,7 +349,7 @@ jobs:
# the diff cannot be resolved, the lint runs.
mobile_native_changes:
name: Mobile Native Changes
runs-on: blacksmith-2vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
Expand Down Expand Up @@ -427,7 +427,7 @@ jobs:
# Skip only on an explicit "no": a gate job that failed or errored leaves the
# output empty, and that must run the lint rather than silently skip it.
if: ${{ !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }}
runs-on: blacksmith-6vcpu-macos-26
runs-on: macos-15
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -455,7 +455,7 @@ jobs:

release_smoke:
name: Release Smoke
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
Expand Down Expand Up @@ -500,7 +500,7 @@ jobs:
mobile_native_static_analysis,
release_smoke,
]
runs-on: blacksmith-2vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every job to pass
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/deploy-relay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
name: Deploy production relay
# A manual run from another branch would deploy that branch to production.
if: github.ref == 'refs/heads/main'
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: ubuntu-24.04
timeout-minutes: 15
environment:
name: production
Expand All @@ -41,12 +41,13 @@ jobs:
RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }}
RELAY_TUNNEL_CLEANUP_MODE: ${{ vars.RELAY_TUNNEL_CLEANUP_MODE }}
RELAY_LEGACY_TUNNEL_CLEANUP_MODE: ${{ vars.RELAY_LEGACY_TUNNEL_CLEANUP_MODE }}
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }}
OTTER_ACCOUNTS_URL: ${{ vars.ACCOUNTS_URL }}
APNS_ENVIRONMENT: ${{ vars.APNS_ENVIRONMENT }}
APNS_TEAM_ID: ${{ vars.APNS_TEAM_ID }}
APNS_KEY_ID: ${{ vars.APNS_KEY_ID }}
APNS_BUNDLE_ID: ${{ vars.APNS_BUNDLE_ID }}
LINEAR_CLIENT_ID: ${{ vars.LINEAR_CLIENT_ID }}
HOSTED_APP_URL: ${{ vars.HOSTED_APP_URL }}
ALCHEMY_TELEMETRY_DISABLED: "1"
steps:
- name: Checkout
Expand Down Expand Up @@ -76,9 +77,10 @@ jobs:
PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }}
PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }}
AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }}
FCM_SERVICE_ACCOUNT: ${{ secrets.FCM_SERVICE_ACCOUNT }}
LINEAR_CLIENT_SECRET: ${{ secrets.LINEAR_CLIENT_SECRET }}
LINEAR_WEBHOOK_SECRET: ${{ secrets.LINEAR_WEBHOOK_SECRET }}

- name: Publish relay deploy commit status
uses: actions/github-script@v8
Expand Down
Loading
Loading