diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 6fc9f6f1a1f7..32d907240b3c 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,4 +1,4 @@ reviews: review_status: false auto_review: - enabled: false + enabled: true diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000000..69dc53dc4393 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,8 @@ +# Security policy + +Report security vulnerabilities affecting T3 Code or T3 Tools-operated infrastructure to +[security@ping.gg](mailto:security@ping.gg). Please do not disclose them publicly until we have had +a reasonable opportunity to investigate and remediate them. + +See the [full security policy](https://t3.codes/security-policy) for reporting details, scope, +and safe harbor terms for good-faith research. diff --git a/.github/scripts/check-nightly-release.cjs b/.github/scripts/check-nightly-release.cjs index dc4b55bc6517..82ee40da1aff 100644 --- a/.github/scripts/check-nightly-release.cjs +++ b/.github/scripts/check-nightly-release.cjs @@ -1,19 +1,21 @@ const MINIMUM_RELEASE_GAP_MS = 6 * 60 * 60 * 1000; -// Runs after the workflow acquires the nightly concurrency lock. -async function shouldReleaseNightly({ github, context, core, now = Date.now() }) { +const isNightlyTag = (tag) => /^v.*-nightly\./.test(tag) || tag.startsWith("nightly-v"); + +// Newest published nightly by publication time, or undefined when none exists. +async function findLatestNightly({ github, context }) { const releases = await github.paginate(github.rest.repos.listReleases, { ...context.repo, per_page: 100, }); - const lastNightly = releases - .filter( - (release) => - !release.draft && - release.published_at && - (/^v.*-nightly\./.test(release.tag_name) || release.tag_name.startsWith("nightly-v")), - ) + return releases + .filter((release) => !release.draft && release.published_at && isNightlyTag(release.tag_name)) .sort((a, b) => Date.parse(b.published_at) - Date.parse(a.published_at))[0]; +} + +// Runs after the workflow acquires the nightly concurrency lock. +async function shouldReleaseNightly({ github, context, core, now = Date.now() }) { + const lastNightly = await findLatestNightly({ github, context }); if (!lastNightly) { core.info("No published nightly found. Proceeding with release."); @@ -41,4 +43,25 @@ async function shouldReleaseNightly({ github, context, core, now = Date.now() }) return true; } -module.exports = { shouldReleaseNightly }; +// Stable releases build the commit the latest nightly shipped, so the stable +// build is one nightly users already ran. Returns the nightly tag, its commit, +// and the stable version that nightly was a preview of. +async function resolveLatestNightlyCommit({ github, context, core }) { + const lastNightly = await findLatestNightly({ github, context }); + if (!lastNightly) { + throw new Error("No published nightly found. Stable releases build the latest nightly commit."); + } + + const tag = lastNightly.tag_name; + // repos.getCommit dereferences annotated tags, so this is the commit either way. + const { data: commit } = await github.rest.repos.getCommit({ ...context.repo, ref: tag }); + const version = /^(?:nightly-)?v(\d+\.\d+\.\d+)-nightly\./.exec(tag)?.[1]; + if (!version) { + throw new Error(`Cannot derive a stable version from nightly tag ${tag}.`); + } + + core.info(`Latest nightly ${tag} shipped ${commit.sha} as a preview of ${version}.`); + return { tag, sha: commit.sha, version }; +} + +module.exports = { shouldReleaseNightly, resolveLatestNightlyCommit }; diff --git a/.github/scripts/check-nightly-release.test.cjs b/.github/scripts/check-nightly-release.test.cjs index 476773bc4e5a..49ade68aeef7 100644 --- a/.github/scripts/check-nightly-release.test.cjs +++ b/.github/scripts/check-nightly-release.test.cjs @@ -99,3 +99,45 @@ for (const status of ["behind", "diverged"]) { assert.equal(await shouldReleaseNightly(options), false); }); } + +const { resolveLatestNightlyCommit } = require("./check-nightly-release.cjs"); + +function nightlyCommitFixture({ releases, commitSha = "abc123" }) { + const refs = []; + const { options } = fixture({ releases }); + options.github.rest.repos.getCommit = async ({ ref }) => { + refs.push(ref); + return { data: { sha: commitSha } }; + }; + return { options, refs }; +} + +test("stable releases resolve the commit of the newest published nightly", async () => { + const { options, refs } = nightlyCommitFixture({ + releases: [ + nightly(10, { tag_name: "v1.0.1-nightly.20260905.100" }), + nightly(1, { tag_name: "v1.0.1-nightly.20260905.123" }), + nightly(0, { tag_name: "v1.0.0" }), + nightly(0, { draft: true, tag_name: "v1.0.1-nightly.20260905.999" }), + ], + commitSha: "deadbeef", + }); + assert.deepEqual(await resolveLatestNightlyCommit(options), { + tag: "v1.0.1-nightly.20260905.123", + sha: "deadbeef", + version: "1.0.1", + }); + assert.deepEqual(refs, ["v1.0.1-nightly.20260905.123"]); +}); + +test("stable releases derive the version from legacy nightly tags", async () => { + const { options } = nightlyCommitFixture({ + releases: [nightly(1, { tag_name: "nightly-v0.9.0-nightly.20260905.5" })], + }); + assert.equal((await resolveLatestNightlyCommit(options)).version, "0.9.0"); +}); + +test("stable releases fail without a published nightly", async () => { + const { options } = nightlyCommitFixture({ releases: [nightly(0, { tag_name: "v1.0.0" })] }); + await assert.rejects(resolveLatestNightlyCommit(options), /No published nightly/); +}); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 938efdf51c39..7f167aa70bf0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ on: - stable - nightly version: - description: "Release version (for example 1.2.3 or v1.2.3)" + description: "Stable version override (for example 1.2.3). Defaults to the version the latest nightly previewed." required: false type: string artifacts_only: @@ -44,33 +44,54 @@ permissions: id-token: none jobs: - check_changes: - name: Check automatic nightly release - if: github.event_name == 'schedule' + # Picks the commit every later job builds. Nightlies and tag pushes build the + # triggering commit. Manual stable releases build the commit of the latest + # published nightly, so stable only ever ships a build that nightly users + # have already run. Scheduled runs also decide here whether a nightly is due. + resolve_commit: + name: Resolve release commit runs-on: ubuntu-24.04 timeout-minutes: 5 outputs: - has_changes: ${{ steps.check.outputs.result }} + ref: ${{ steps.resolve.outputs.ref }} + nightly_version: ${{ steps.resolve.outputs.nightly_version }} + has_changes: ${{ steps.resolve.outputs.has_changes }} steps: - name: Checkout uses: actions/checkout@v6 with: sparse-checkout: .github/scripts - - id: check - name: Check release gap and new commits + - id: resolve + name: Resolve release commit uses: actions/github-script@v8 + env: + DISPATCH_CHANNEL: ${{ inputs.channel }} with: script: | - const { shouldReleaseNightly } = require('./.github/scripts/check-nightly-release.cjs'); - return await shouldReleaseNightly({ github, context, core }); + const { + shouldReleaseNightly, + resolveLatestNightlyCommit, + } = require('./.github/scripts/check-nightly-release.cjs'); + + if (context.eventName === 'schedule') { + core.setOutput('has_changes', await shouldReleaseNightly({ github, context, core })); + core.setOutput('ref', context.sha); + } else if (context.eventName === 'workflow_dispatch' && process.env.DISPATCH_CHANNEL !== 'nightly') { + const { tag, sha, version } = await resolveLatestNightlyCommit({ github, context, core }); + core.notice(`Stable release builds ${sha}, the commit shipped by ${tag}.`); + core.setOutput('ref', sha); + core.setOutput('nightly_version', version); + } else { + core.setOutput('ref', context.sha); + } preflight: name: Preflight - needs: [check_changes] + needs: [resolve_commit] if: | - !failure() && !cancelled() && - (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') + needs.resolve_commit.result == 'success' && + (github.event_name != 'schedule' || needs.resolve_commit.outputs.has_changes == 'true') runs-on: ubuntu-24.04 timeout-minutes: 10 outputs: @@ -83,11 +104,12 @@ jobs: cli_dist_tag: ${{ steps.release_meta.outputs.cli_dist_tag }} is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }} make_latest: ${{ steps.release_meta.outputs.make_latest }} - ref: ${{ github.sha }} + ref: ${{ needs.resolve_commit.outputs.ref }} steps: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ needs.resolve_commit.outputs.ref }} fetch-depth: 0 sparse-checkout: | /* @@ -109,8 +131,9 @@ jobs: env: DISPATCH_CHANNEL: ${{ github.event.inputs.channel }} DISPATCH_VERSION: ${{ github.event.inputs.version }} + NIGHTLY_VERSION: ${{ needs.resolve_commit.outputs.nightly_version }} NIGHTLY_DATE: ${{ github.run_started_at }} - NIGHTLY_SHA: ${{ github.sha }} + NIGHTLY_SHA: ${{ needs.resolve_commit.outputs.ref }} NIGHTLY_RUN_NUMBER: ${{ github.run_number }} run: | if [[ "${GITHUB_EVENT_NAME}" == "schedule" || ( "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-stable}" == "nightly" ) ]]; then @@ -128,9 +151,9 @@ jobs: echo "make_latest=false" >> "$GITHUB_OUTPUT" else if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - raw="${DISPATCH_VERSION}" + raw="${DISPATCH_VERSION:-$NIGHTLY_VERSION}" if [[ -z "$raw" ]]; then - echo "workflow_dispatch stable releases require the version input." >&2 + echo "workflow_dispatch stable releases need a version input or a published nightly." >&2 exit 1 fi else @@ -215,14 +238,12 @@ jobs: relay_public_config: name: Resolve T3 Connect public config - # Consumes only the commit SHA, not preflight's resolved version, so it runs - # alongside preflight instead of after it. The condition mirrors preflight's: - # check_changes is skipped on manual and tag releases (skipped is neither failure - # nor success, so success() would be wrong here). - needs: [check_changes] + # Consumes only the release commit, not preflight's resolved version, so it + # runs alongside preflight instead of after it. The condition mirrors preflight's. + needs: [resolve_commit] if: | - !failure() && !cancelled() && - (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') + needs.resolve_commit.result == 'success' && + (github.event_name != 'schedule' || needs.resolve_commit.outputs.has_changes == 'true') runs-on: ubuntu-24.04 timeout-minutes: 5 environment: @@ -244,7 +265,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - ref: ${{ github.sha }} + ref: ${{ needs.resolve_commit.outputs.ref }} sparse-checkout: | /* !/.repos/ @@ -317,19 +338,19 @@ jobs: # machine. node-pty is N-API, so one binary works across all WSL Node versions. build_wsl_node_pty: name: Build WSL node-pty (linux-x64) - # Same gating as relay_public_config: only the commit SHA is needed, so this - # runs alongside preflight. See the condition comment there. - needs: [check_changes] + # Same gating as relay_public_config: only the release commit is needed, so + # this runs alongside preflight. See the condition comment there. + needs: [resolve_commit] if: | - !failure() && !cancelled() && - (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') + needs.resolve_commit.result == 'success' && + (github.event_name != 'schedule' || needs.resolve_commit.outputs.has_changes == 'true') runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 with: - ref: ${{ github.sha }} + ref: ${{ needs.resolve_commit.outputs.ref }} sparse-checkout: | /* !/.repos/ @@ -1042,6 +1063,60 @@ jobs: "${vercel_scope_args[@]}" fi + deploy_marketing: + name: Deploy marketing site + needs: [preflight, release] + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'nightly' }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + node-version-file: package.json + cache: true + run-install: | + args: + - --filter=@t3tools/marketing... + + - name: Deploy marketing site to Vercel + shell: bash + run: | + set -euo pipefail + + if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" ]]; then + echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID." >&2 + exit 1 + fi + + VERCEL_PROJECT_ID="$( + curl --fail --silent --show-error \ + --header "Authorization: Bearer $VERCEL_TOKEN" \ + "https://api.vercel.com/v9/projects/t3code-marketing?teamId=$VERCEL_ORG_ID" \ + | jq --exit-status --raw-output '.id' + )" + export VERCEL_PROJECT_ID + + vp dlx vercel@53.1.1 deploy \ + --archive=tgz \ + --prod \ + --yes \ + --token "$VERCEL_TOKEN" \ + --scope "${VERCEL_TEAM_SLUG:-$VERCEL_ORG_ID}" + finalize: name: Finalize release if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} diff --git a/.macroscope/check-run-agents/effect-service-conventions.md b/.macroscope/check-run-agents/effect-service-conventions.md index 316cf3509bf5..cfb810e37b45 100644 --- a/.macroscope/check-run-agents/effect-service-conventions.md +++ b/.macroscope/check-run-agents/effect-service-conventions.md @@ -1,7 +1,7 @@ --- title: Effect Service Conventions -model: claude-opus-5 -effort: high +model: gpt-5-6-sol +effort: medium input: full_diff tools: - browse_code @@ -16,6 +16,7 @@ labels: - vouch:trusted requires: - Check +maxBudgetPerRun: 5 maxBudgetPerPR: 25 conclusion: failure showToolCalls: true diff --git a/.macroscope/check-run-agents/ui-consistency.md b/.macroscope/check-run-agents/ui-consistency.md index 32df265d3313..dd88c891ed9b 100644 --- a/.macroscope/check-run-agents/ui-consistency.md +++ b/.macroscope/check-run-agents/ui-consistency.md @@ -1,6 +1,6 @@ --- title: UI Consistency -model: claude-opus-5 +model: gpt-5-6-terra effort: medium input: full_diff tools: @@ -15,9 +15,9 @@ labels: - vouch:trusted requires: - Check -maxBudgetPerPR: 25 +maxBudgetPerRun: 2 +maxBudgetPerPR: 10 conclusion: failure -maxBudgetPerRun: 10 --- # UI consistency review diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 1cb32c858509..9eb5a9625eaa 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -28,7 +28,6 @@ const clientSettings: ClientSettings = { confirmWorktreeRemoval: true, confirmThreadUnpin: false, contextWindowMeterEnabled: false, - composerCollapseOnBlur: false, composerCollapseOnScroll: true, dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, diff --git a/apps/marketing/public/nightly-sky.svg b/apps/marketing/public/nightly-sky.svg new file mode 100644 index 000000000000..3165b211b287 --- /dev/null +++ b/apps/marketing/public/nightly-sky.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/marketing/src/assets/icon-nightly.webp b/apps/marketing/src/assets/icon-nightly.webp new file mode 100644 index 000000000000..8a00067e43b8 Binary files /dev/null and b/apps/marketing/src/assets/icon-nightly.webp differ diff --git a/apps/marketing/src/layouts/Layout.astro b/apps/marketing/src/layouts/Layout.astro index 4c95bc86560e..a06521986ea0 100644 --- a/apps/marketing/src/layouts/Layout.astro +++ b/apps/marketing/src/layouts/Layout.astro @@ -80,6 +80,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site); T3 Code