Skip to content

Commit dd14c98

Browse files
only21milclaude
andcommitted
Merge main (d53873b) into r13/b122-watch-pull-request-reports-removal-from-
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2 parents 86f6522 + d53873b commit dd14c98

681 files changed

Lines changed: 29495 additions & 10606 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
name: Clean up relay preview
2+
3+
# Tears down stage `pr-<n>` when `relay:preview` is removed or the PR closes,
4+
# and retries teardown for closed labeled PRs on a schedule. A label stays on a
5+
# PR only while its teardown is outstanding, so the scheduled retry lists
6+
# exactly the failures.
7+
#
8+
# This runs under pull_request_target, so the workflow and the Alchemy program
9+
# come from the default branch. No PR code runs here, which is why it needs no
10+
# reviewer approval and why a PR cannot edit its own teardown to reach secrets.
11+
on:
12+
pull_request_target:
13+
types: [unlabeled, closed]
14+
schedule:
15+
- cron: "17 7 * * *"
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
issues: read
21+
pull-requests: read
22+
23+
jobs:
24+
cleanup_pr:
25+
name: Tear down relay preview
26+
if: >-
27+
github.ref == 'refs/heads/main' &&
28+
github.event_name == 'pull_request_target' &&
29+
(
30+
(github.event.action == 'unlabeled' && github.event.label.name == 'relay:preview') ||
31+
(github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'relay:preview'))
32+
)
33+
# Job-level so that events which skip this job, such as a closed PR without
34+
# the label, never take the group's pending slot. GitHub replaces a pending
35+
# run in a group with a newer one, which would drop a queued teardown.
36+
concurrency:
37+
group: relay-preview-${{ github.event.pull_request.number }}
38+
cancel-in-progress: false
39+
permissions:
40+
contents: read
41+
issues: write
42+
pull-requests: write
43+
runs-on: blacksmith-8vcpu-ubuntu-2404
44+
timeout-minutes: 30
45+
environment:
46+
name: relay-preview-cleanup
47+
env:
48+
GH_TOKEN: ${{ github.token }}
49+
STAGE: pr-${{ github.event.pull_request.number }}
50+
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
51+
PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }}
52+
AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }}
53+
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
54+
RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }}
55+
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
56+
CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }}
57+
APNS_ENABLED: "false"
58+
ALCHEMY_TELEMETRY_DISABLED: "1"
59+
steps:
60+
- name: Checkout default branch
61+
uses: actions/checkout@v6
62+
with:
63+
ref: ${{ github.event.repository.default_branch }}
64+
sparse-checkout: |
65+
/*
66+
!/.repos/
67+
sparse-checkout-cone-mode: false
68+
69+
- name: Setup Vite+
70+
uses: voidzero-dev/setup-vp@v1
71+
with:
72+
node-version-file: package.json
73+
cache: true
74+
run-install: |
75+
args:
76+
- --filter=t3code-relay...
77+
78+
# Destroy also runs the preview tunnel sweep, which is part of the stack.
79+
- name: Destroy preview stage
80+
run: vp run --filter t3code-relay destroy --stage "$STAGE" --yes --no-input
81+
env:
82+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
83+
PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }}
84+
PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }}
85+
AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }}
86+
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
87+
88+
# Only reached after a successful destroy. A failed one keeps the label,
89+
# so the scheduled retry picks the PR up again.
90+
- name: Clear the teardown marker
91+
if: github.event.action == 'closed'
92+
run: gh pr edit "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" --remove-label relay:preview
93+
94+
# An unlabeled event has already removed the label, so a failed teardown
95+
# must put it back or no retry will ever see the stage. Labels added with
96+
# GITHUB_TOKEN do not start new runs, so this does not redeploy.
97+
- name: Restore the teardown marker after a failed destroy
98+
if: failure() && github.event.action == 'unlabeled'
99+
run: gh pr edit "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" --add-label relay:preview
100+
101+
list_closed_previews:
102+
name: List closed relay previews
103+
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request_target'
104+
runs-on: blacksmith-8vcpu-ubuntu-2404
105+
timeout-minutes: 10
106+
outputs:
107+
numbers: ${{ steps.list.outputs.numbers }}
108+
steps:
109+
# The issues endpoint is paginated by gh, so there is no listing cap. Pull
110+
# requests appear there with a `pull_request` key.
111+
- name: List closed PRs labeled relay:preview
112+
id: list
113+
env:
114+
GH_TOKEN: ${{ github.token }}
115+
run: |
116+
numbers=$(gh api --paginate --method GET "repos/$GITHUB_REPOSITORY/issues" \
117+
-f labels=relay:preview -f state=closed -f per_page=100 \
118+
--jq '.[] | select(.pull_request) | .number') \
119+
|| { echo "Could not list closed relay:preview PRs" >&2; exit 1; }
120+
json=$(printf '%s\n' "$numbers" | jq -Rsc 'split("\n") | map(select(length > 0) | tonumber)')
121+
echo "Closed relay:preview PRs: $json"
122+
echo "numbers=$json" >> "$GITHUB_OUTPUT"
123+
124+
sweep_pr:
125+
name: Retry teardown for PR ${{ matrix.number }}
126+
needs: list_closed_previews
127+
if: github.ref == 'refs/heads/main' && needs.list_closed_previews.outputs.numbers != '[]'
128+
runs-on: blacksmith-8vcpu-ubuntu-2404
129+
timeout-minutes: 30
130+
strategy:
131+
fail-fast: false
132+
max-parallel: 4
133+
matrix:
134+
number: ${{ fromJSON(needs.list_closed_previews.outputs.numbers) }}
135+
# Same group as the deploy and teardown for this PR, so a reopened PR's
136+
# deploy cannot run concurrently with a teardown of its previous stage.
137+
concurrency:
138+
group: relay-preview-${{ matrix.number }}
139+
cancel-in-progress: false
140+
permissions:
141+
contents: read
142+
issues: write
143+
pull-requests: write
144+
environment:
145+
name: relay-preview-cleanup
146+
env:
147+
GH_TOKEN: ${{ github.token }}
148+
STAGE: pr-${{ matrix.number }}
149+
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
150+
PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }}
151+
AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }}
152+
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
153+
RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }}
154+
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
155+
CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }}
156+
APNS_ENABLED: "false"
157+
ALCHEMY_TELEMETRY_DISABLED: "1"
158+
steps:
159+
# The lock above is taken before this runs, so the state read here is
160+
# current. An open PR, including one reopened since the listing, is left
161+
# alone. GitHub reports merged PRs as MERGED, so anything not OPEN counts.
162+
- name: Confirm the PR is not open
163+
id: eligible
164+
run: |
165+
state=$(gh pr view "${{ matrix.number }}" --repo "$GITHUB_REPOSITORY" \
166+
--json state --jq 'if .state == "OPEN" then "no" else "yes" end') \
167+
|| { echo "Could not read PR ${{ matrix.number }}" >&2; exit 1; }
168+
echo "eligible=$state" >> "$GITHUB_OUTPUT"
169+
[ "$state" = yes ] || echo "PR ${{ matrix.number }} is open again; leaving $STAGE alone."
170+
171+
- name: Checkout default branch
172+
if: steps.eligible.outputs.eligible == 'yes'
173+
uses: actions/checkout@v6
174+
with:
175+
ref: ${{ github.event.repository.default_branch }}
176+
sparse-checkout: |
177+
/*
178+
!/.repos/
179+
sparse-checkout-cone-mode: false
180+
181+
- name: Setup Vite+
182+
if: steps.eligible.outputs.eligible == 'yes'
183+
uses: voidzero-dev/setup-vp@v1
184+
with:
185+
node-version-file: package.json
186+
cache: true
187+
run-install: |
188+
args:
189+
- --filter=t3code-relay...
190+
191+
- name: Destroy preview stage
192+
if: steps.eligible.outputs.eligible == 'yes'
193+
run: vp run --filter t3code-relay destroy --stage "$STAGE" --yes --no-input
194+
env:
195+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
196+
PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }}
197+
PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }}
198+
AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }}
199+
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
200+
201+
- name: Clear the teardown marker
202+
if: steps.eligible.outputs.eligible == 'yes'
203+
run: gh pr edit "${{ matrix.number }}" --repo "$GITHUB_REPOSITORY" --remove-label relay:preview
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Deploy relay preview
2+
3+
# Labeling a same-repository PR `relay:preview` deploys Alchemy stage `pr-<n>`.
4+
# This job runs the PR's own code with account credentials, so it waits for an
5+
# approval in the `relay-preview` environment on every run. Teardown is in
6+
# relay-preview-cleanup.yml and never runs PR code.
7+
on:
8+
pull_request:
9+
types: [labeled, synchronize, reopened]
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
15+
concurrency:
16+
group: relay-preview-${{ github.event.pull_request.number }}
17+
cancel-in-progress: false
18+
19+
jobs:
20+
deploy_preview:
21+
name: Deploy relay preview
22+
if: >-
23+
github.event.pull_request.head.repo.full_name == github.repository &&
24+
contains(github.event.pull_request.labels.*.name, 'relay:preview')
25+
runs-on: blacksmith-8vcpu-ubuntu-2404
26+
timeout-minutes: 20
27+
environment:
28+
name: relay-preview
29+
env:
30+
STAGE: pr-${{ github.event.pull_request.number }}
31+
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
32+
PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }}
33+
AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }}
34+
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
35+
RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }}
36+
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
37+
CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }}
38+
# Previews take no alert notifier, no tunnel cleanup, and no custom
39+
# domain, so they stay silent and reap nothing outside their own stage.
40+
APNS_ENABLED: "false"
41+
ALCHEMY_TELEMETRY_DISABLED: "1"
42+
steps:
43+
- name: Confirm the PR is still open and labeled
44+
env:
45+
GH_TOKEN: ${{ github.token }}
46+
PR_NUMBER: ${{ github.event.pull_request.number }}
47+
run: |
48+
state=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state,labels \
49+
--jq 'if .state == "OPEN" and any(.labels[]; .name == "relay:preview") then "ok" else "stale" end')
50+
if [ "$state" != "ok" ]; then
51+
echo "PR #$PR_NUMBER is no longer open with relay:preview; not deploying." >&2
52+
exit 1
53+
fi
54+
55+
- name: Checkout
56+
uses: actions/checkout@v6
57+
with:
58+
ref: ${{ github.event.pull_request.head.sha }}
59+
sparse-checkout: |
60+
/*
61+
!/.repos/
62+
sparse-checkout-cone-mode: false
63+
64+
- name: Setup Vite+
65+
uses: voidzero-dev/setup-vp@v1
66+
with:
67+
node-version-file: package.json
68+
cache: true
69+
run-install: |
70+
args:
71+
- --filter=t3code-relay...
72+
73+
- name: Deploy preview stage
74+
run: vp run --filter t3code-relay deploy --stage "$STAGE" --yes --no-input
75+
env:
76+
T3CODE_RELAY_CLIENT_CONFIG_ENV: ${{ runner.temp }}/relay-client-config.env
77+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
78+
PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }}
79+
PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }}
80+
AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }}
81+
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Close stale "needs more info" issues
2+
3+
on:
4+
schedule:
5+
- cron: "17 6 * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
actions: write
10+
issues: write
11+
12+
jobs:
13+
stale:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/stale@v9
17+
with:
18+
only-issue-labels: "needs more info"
19+
# The label is applied by the triage flow, so never auto-mark; close 7 days after it was added.
20+
days-before-issue-stale: -1
21+
days-before-issue-close: 7
22+
days-before-pr-stale: -1
23+
days-before-pr-close: -1
24+
remove-issue-stale-when-updated: true
25+
stale-issue-label: "needs more info"
26+
close-issue-reason: not_planned
27+
close-issue-message: >-
28+
Closing: no reply in 7 days after we asked for more information.
29+
Comment with the missing details and we will reopen.
30+
operations-per-run: 200

‎.repos/alchemy-effect/packages/frontend-frameworks/src/core/NodeServe.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export interface NodeServeHandler {
4848
export interface NodeServeEntryOptions {
4949
/**
5050
* JS expression resolving the on-disk client-assets directory
51-
* (typically `fileURLToPath(new URL("../client/", import.meta.url))`).
51+
* (typically `fileURLToPath(new URL("../shared/", import.meta.url))`).
5252
* Omit to skip static-file serving (Next.js handles its own assets).
5353
*/
5454
readonly clientDirExpression?: string | undefined;

‎.repos/alchemy-effect/packages/frontend-frameworks/src/core/test/NodeServe.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ describe("relativeClientDirExpression", () => {
2828
"/project/dist/server/serve-node.mjs",
2929
"/project/dist/client",
3030
),
31-
).toBe(`fileURLToPath(new URL("../client/", import.meta.url))`);
31+
).toBe(`fileURLToPath(new URL("../shared/", import.meta.url))`);
3232
});
3333
});
3434

3535
describe("makeNodeServeEntrySource", () => {
3636
it("listens on PORT, answers /health, serves static files, then the fetch handler", () => {
3737
const source = makeNodeServeEntrySource({
38-
clientDirExpression: `fileURLToPath(new URL("../client/", import.meta.url))`,
38+
clientDirExpression: `fileURLToPath(new URL("../shared/", import.meta.url))`,
3939
handler: {
4040
kind: "fetch",
4141
imports: `import { handler } from "./index.mjs";`,
@@ -131,7 +131,7 @@ describe("makeNodeServeEntrySource", () => {
131131

132132
it("does not map GET / onto public/index.html (SSR home stays on the handler)", () => {
133133
const source = makeNodeServeEntrySource({
134-
clientDirExpression: `fileURLToPath(new URL("../client/", import.meta.url))`,
134+
clientDirExpression: `fileURLToPath(new URL("../shared/", import.meta.url))`,
135135
handler: {
136136
kind: "node",
137137
imports: `import { handler } from "./index.mjs";`,

‎apps/desktop/src/app/DesktopObservability.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
2+
import desktopPackageJson from "../../package.json" with { type: "json" };
23
import * as NodeServices from "@effect/platform-node/NodeServices";
34
import { assert, describe, it } from "@effect/vitest";
45
import * as ConfigProvider from "effect/ConfigProvider";
@@ -312,4 +313,33 @@ describe("DesktopObservability", () => {
312313
),
313314
);
314315
});
316+
317+
it.effect("reports the app's own version for an unpackaged desktop run", () => {
318+
const requests: Array<ExportedRequest> = [];
319+
return Effect.gen(function* () {
320+
const fileSystem = yield* FileSystem.FileSystem;
321+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
322+
prefix: "t3-desktop-observability-test-",
323+
});
324+
const layerEnvironment = layerEnvironmentFor(baseDir);
325+
yield* writeObservabilitySettings(layerEnvironment, {
326+
otlpLogsUrl: "https://settings.example.com/v1/logs",
327+
});
328+
329+
yield* Effect.scoped(
330+
Effect.logInfo("desktop resource under test").pipe(
331+
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(layerEnvironment))),
332+
),
333+
);
334+
335+
const body = requests.map((request) => request.body).join("\n");
336+
assert.include(
337+
body,
338+
`"key":"service.version","value":{"stringValue":"${desktopPackageJson.version}"}`,
339+
);
340+
}).pipe(
341+
Effect.scoped,
342+
Effect.provide(Layer.mergeAll(NodeServices.layer, layerCollector(requests), layerEmptyEnv)),
343+
);
344+
});
315345
});

0 commit comments

Comments
 (0)