Skip to content

Commit 9d49cce

Browse files
authored
Merge pull request #193 from wevm/sync-upstream-8
Sync rocicorp/mono main (3933b71)
2 parents ec06579 + 548c3e4 commit 9d49cce

42 files changed

Lines changed: 820 additions & 3219 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/release.yml‎

Lines changed: 149 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ jobs:
125125
fi
126126
127127
build:
128-
name: Build artifacts
128+
name: Build package
129129
runs-on: ubuntu-latest
130130
needs: plan
131131
permissions:
@@ -134,7 +134,6 @@ jobs:
134134
tarball_name: ${{ steps.pack.outputs.filename }}
135135
env:
136136
SCRIPTS: ${{ github.workspace }}/ci/scripts/src
137-
SOURCE_SHA: ${{ needs.plan.outputs.source_sha }}
138137
VERSION: ${{ needs.plan.outputs.version }}
139138
steps:
140139
- name: Checkout workflow scripts
@@ -190,14 +189,59 @@ jobs:
190189
TARBALL: ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
191190
run: node "$SCRIPTS/release-verify-tarball.ts" "$TARBALL" "$VERSION"
192191

192+
- name: Upload npm package artifact
193+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
194+
with:
195+
name: zero-npm-${{ needs.plan.outputs.version }}
196+
if-no-files-found: error
197+
retention-days: 7
198+
path: ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
199+
200+
build-docker:
201+
name: Build Docker image (${{ matrix.arch }})
202+
runs-on: ${{ matrix.runner }}
203+
needs: [plan, build]
204+
strategy:
205+
fail-fast: false
206+
matrix:
207+
include:
208+
- arch: amd64
209+
platform: linux/amd64
210+
runner: ubuntu-24.04
211+
- arch: arm64
212+
platform: linux/arm64
213+
runner: ubuntu-24.04-arm
214+
permissions:
215+
contents: read
216+
env:
217+
ARCH: ${{ matrix.arch }}
218+
PLATFORM: ${{ matrix.platform }}
219+
SOURCE_SHA: ${{ needs.plan.outputs.source_sha }}
220+
TARBALL: ${{ needs.build.outputs.tarball_name }}
221+
VERSION: ${{ needs.plan.outputs.version }}
222+
steps:
223+
- name: Checkout release source
224+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
225+
with:
226+
path: source
227+
ref: ${{ needs.plan.outputs.source_sha }}
228+
persist-credentials: false
229+
230+
- name: Download npm package artifact
231+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
232+
with:
233+
name: zero-npm-${{ needs.plan.outputs.version }}
234+
path: ${{ runner.temp }}/npm
235+
193236
- name: Set up Docker Buildx
194237
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
195238

196239
- name: Build Docker image archive
197240
env:
198-
TARBALL: ${{ steps.pack.outputs.filename }}
199-
TARBALL_PATH: ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
200-
IMAGE_ARCHIVE: ${{ runner.temp }}/docker/zero-image.tar
241+
IMAGE_ARCHIVE: ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.tar
242+
IMAGE_DIGEST: ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.digest
243+
IMAGE_METADATA: ${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.json
244+
TARBALL_PATH: ${{ runner.temp }}/npm/${{ needs.build.outputs.tarball_name }}
201245
run: |
202246
set -euo pipefail
203247
mkdir -p source/packages/zero/pkgs "$(dirname "$IMAGE_ARCHIVE")"
@@ -209,37 +253,38 @@ jobs:
209253
build_contexts+=(--build-context monogo=source/go)
210254
fi
211255
docker buildx build \
212-
--platform linux/amd64,linux/arm64 \
256+
--platform "$PLATFORM" \
213257
"${build_contexts[@]}" \
214258
--build-arg ZERO_VERSION="$VERSION" \
215259
--build-arg ZERO_PACKAGE="$TARBALL" \
216260
--label org.opencontainers.image.revision="$SOURCE_SHA" \
217261
--label org.opencontainers.image.version="$VERSION" \
262+
--metadata-file "$IMAGE_METADATA" \
218263
--sbom=true \
219264
--provenance=mode=max \
220265
--output=type=oci,dest="$IMAGE_ARCHIVE" \
221266
source/packages/zero
222-
223-
- name: Upload npm package artifact
224-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
225-
with:
226-
name: zero-npm-${{ needs.plan.outputs.version }}
227-
if-no-files-found: error
228-
retention-days: 7
229-
path: ${{ runner.temp }}/npm/${{ steps.pack.outputs.filename }}
267+
digest=$(jq -r '."containerimage.digest"' "$IMAGE_METADATA")
268+
if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
269+
echo "Invalid $ARCH image digest: $digest" >&2
270+
exit 1
271+
fi
272+
printf '%s\n' "$digest" > "$IMAGE_DIGEST"
230273
231274
- name: Upload Docker image artifact
232275
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
233276
with:
234-
name: zero-docker-${{ needs.plan.outputs.version }}
277+
name: zero-docker-${{ needs.plan.outputs.version }}-${{ matrix.arch }}
235278
if-no-files-found: error
236279
retention-days: 7
237-
path: ${{ runner.temp }}/docker/zero-image.tar
280+
path: |
281+
${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.tar
282+
${{ runner.temp }}/docker/zero-image-${{ matrix.arch }}.digest
238283
239284
publish-npm:
240285
name: Publish npm package
241286
runs-on: ubuntu-latest
242-
needs: [plan, build]
287+
needs: [plan, build, build-docker]
243288
environment:
244289
name: zero-release-npm
245290
permissions:
@@ -305,7 +350,7 @@ jobs:
305350
publish-docker:
306351
name: Publish Docker images
307352
runs-on: ubuntu-latest
308-
needs: [plan, build, archive-cloudzero-source]
353+
needs: [plan, build-docker, archive-cloudzero-source]
309354
environment:
310355
name: zero-release-docker
311356
permissions:
@@ -315,31 +360,33 @@ jobs:
315360
MODE: ${{ needs.plan.outputs.mode }}
316361
VERSION: ${{ needs.plan.outputs.version }}
317362
steps:
318-
- name: Download Docker image artifact
363+
- name: Download amd64 Docker image artifact
319364
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
320365
with:
321-
name: zero-docker-${{ needs.plan.outputs.version }}
366+
name: zero-docker-${{ needs.plan.outputs.version }}-amd64
322367
path: dist/docker
323368

324-
- name: Push GHCR Docker image
325-
uses: docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
326-
with:
327-
args: >-
328-
copy --all
329-
--dest-creds ${{ github.actor }}:${{ github.token }}
330-
oci-archive:dist/docker/zero-image.tar
331-
docker://ghcr.io/rocicorp/zero:${{ needs.plan.outputs.version }}
332-
333-
- name: Push GHCR head tag
334-
# The version signature covers this tag because both resolve to one digest.
335-
if: needs.plan.outputs.mode == 'head'
336-
uses: docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
369+
- name: Download arm64 Docker image artifact
370+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
337371
with:
338-
args: >-
339-
copy --all
340-
--dest-creds ${{ github.actor }}:${{ github.token }}
341-
oci-archive:dist/docker/zero-image.tar
342-
docker://ghcr.io/rocicorp/zero:head
372+
name: zero-docker-${{ needs.plan.outputs.version }}-arm64
373+
path: dist/docker
374+
375+
- name: Read Docker image digests
376+
id: digests
377+
run: |
378+
set -euo pipefail
379+
for arch in amd64 arm64; do
380+
digest=$(<"dist/docker/zero-image-$arch.digest")
381+
if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
382+
echo "Invalid $arch image digest: $digest" >&2
383+
exit 1
384+
fi
385+
echo "$arch=$digest" >> "$GITHUB_OUTPUT"
386+
done
387+
388+
- name: Set up Docker Buildx
389+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
343390

344391
- name: Install cosign
345392
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
@@ -362,27 +409,81 @@ jobs:
362409
with:
363410
username: rocicorp
364411

365-
- name: Push Docker Hub image
412+
- name: Push GHCR amd64 image by digest
366413
uses: docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
367414
env:
368415
REGISTRY_AUTH_FILE: ${{ runner.temp }}/docker-auth/config.json
369416
with:
370417
args: >-
371-
copy --all
372-
oci-archive:dist/docker/zero-image.tar
373-
docker://docker.io/rocicorp/zero:${{ needs.plan.outputs.version }}
418+
copy --all --preserve-digests
419+
oci-archive:dist/docker/zero-image-amd64.tar
420+
docker://ghcr.io/rocicorp/zero@${{ steps.digests.outputs.amd64 }}
374421
375-
- name: Push Docker Hub head tag
376-
# The version signature covers this tag because both resolve to one digest.
377-
if: needs.plan.outputs.mode == 'head'
422+
- name: Push GHCR arm64 image by digest
378423
uses: docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
379424
env:
380425
REGISTRY_AUTH_FILE: ${{ runner.temp }}/docker-auth/config.json
381426
with:
382427
args: >-
383-
copy --all
384-
oci-archive:dist/docker/zero-image.tar
385-
docker://docker.io/rocicorp/zero:head
428+
copy --all --preserve-digests
429+
oci-archive:dist/docker/zero-image-arm64.tar
430+
docker://ghcr.io/rocicorp/zero@${{ steps.digests.outputs.arm64 }}
431+
432+
- name: Push Docker Hub amd64 image by digest
433+
uses: docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
434+
env:
435+
REGISTRY_AUTH_FILE: ${{ runner.temp }}/docker-auth/config.json
436+
with:
437+
args: >-
438+
copy --all --preserve-digests
439+
oci-archive:dist/docker/zero-image-amd64.tar
440+
docker://docker.io/rocicorp/zero@${{ steps.digests.outputs.amd64 }}
441+
442+
- name: Push Docker Hub arm64 image by digest
443+
uses: docker://quay.io/containers/skopeo:v1.22.2-immutable@sha256:ca4fd94dba8cab15cf79c4c156bfc26d28e2265411294e9bba87756942e739ad
444+
env:
445+
REGISTRY_AUTH_FILE: ${{ runner.temp }}/docker-auth/config.json
446+
with:
447+
args: >-
448+
copy --all --preserve-digests
449+
oci-archive:dist/docker/zero-image-arm64.tar
450+
docker://docker.io/rocicorp/zero@${{ steps.digests.outputs.arm64 }}
451+
452+
- name: Create multi-platform Docker image tags
453+
env:
454+
AMD64_DIGEST: ${{ steps.digests.outputs.amd64 }}
455+
ARM64_DIGEST: ${{ steps.digests.outputs.arm64 }}
456+
DOCKER_CONFIG: ${{ runner.temp }}/docker-auth
457+
run: |
458+
set -euo pipefail
459+
for image in ghcr.io/rocicorp/zero docker.io/rocicorp/zero; do
460+
tags=(--tag "$image:$VERSION")
461+
if [ "$MODE" = 'head' ]; then
462+
# The version signature covers head because both use one digest.
463+
tags+=(--tag "$image:head")
464+
fi
465+
docker buildx imagetools create \
466+
"${tags[@]}" \
467+
"$image@$AMD64_DIGEST" \
468+
"$image@$ARM64_DIGEST"
469+
done
470+
471+
- name: Verify multi-platform Docker image tags
472+
env:
473+
DOCKER_CONFIG: ${{ runner.temp }}/docker-auth
474+
run: |
475+
set -euo pipefail
476+
for image in \
477+
"ghcr.io/rocicorp/zero:$VERSION" \
478+
"docker.io/rocicorp/zero:$VERSION"; do
479+
platforms=$(docker buildx imagetools inspect "$image" --raw |
480+
jq -r '.manifests[].platform | select(.os == "linux") | .architecture' |
481+
sort -u | paste -sd, -)
482+
if [ "$platforms" != 'amd64,arm64' ]; then
483+
echo "Expected $image to contain linux/amd64 and linux/arm64, got: $platforms" >&2
484+
exit 1
485+
fi
486+
done
386487
387488
- name: Sign Docker images
388489
env:

‎apps/zbugs/api/index.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {dbProvider, sql} from '../server/db.ts';
2222
import {registerMachineRoutes} from '../server/machine-routes.ts';
2323
import {isLocalHostname, nativeAppRedirect} from '../server/native-login.ts';
2424
import {createServerMutators} from '../server/server-mutators.ts';
25+
import {verifyUnsubscribeToken} from '../server/unsubscribe-token.ts';
2526
import {jwtDataSchema, type JWTData} from '../shared/auth.ts';
2627
import {queries} from '../shared/queries.ts';
2728
import {schema} from '../shared/schema.ts';
@@ -258,7 +259,7 @@ fastify.post<{
258259
});
259260

260261
fastify.get<{
261-
Querystring: {id: string; email: string};
262+
Querystring: {id: string; email: string; token?: string | undefined};
262263
}>('/api/unsubscribe', async (request, reply) => {
263264
if (!request.query.email) {
264265
reply.status(400).send('Email is required');
@@ -273,6 +274,21 @@ fastify.get<{
273274
return;
274275
}
275276

277+
// The token is only in the link we emailed to this address, so knowing
278+
// someone's email is not enough to unsubscribe them.
279+
const {token} = request.query;
280+
if (
281+
typeof token !== 'string' ||
282+
!verifyUnsubscribeToken(token, request.query.email, shortID)
283+
) {
284+
reply
285+
.status(403)
286+
.send(
287+
'This unsubscribe link is not valid. Use the link in a more recent email, or unsubscribe on the issue page.',
288+
);
289+
return;
290+
}
291+
276292
const existingUserResult =
277293
await sql`SELECT id, email FROM "user" WHERE "email" = ${request.query.email}`;
278294

@@ -284,7 +300,7 @@ fastify.get<{
284300
}
285301

286302
const issueResult =
287-
await sql`SELECT id, title FROM "issue" WHERE "shortID" = ${shortID}`;
303+
await sql`SELECT id FROM "issue" WHERE "shortID" = ${shortID}`;
288304

289305
const issue = issueResult[0];
290306

@@ -297,10 +313,12 @@ fastify.get<{
297313
VALUES (${existingUser.id}, ${issue.id}, false)
298314
ON CONFLICT ("userID", "issueID")
299315
DO UPDATE SET "subscribed" = false`;
316+
// No stored content, such as the issue title, goes in this response: it is
317+
// HTML on the app origin, and the endpoint is unauthenticated.
300318
reply
301319
.type('text/html')
302320
.send(
303-
`OK! You are unsubscribed from <a href="https://bugs.rocicorp.dev/issue/${shortID}">${issue.title}</a>.`,
321+
`OK! You are unsubscribed from <a href="https://bugs.rocicorp.dev/issue/${shortID}">issue #${shortID}</a>.`,
304322
);
305323
});
306324

‎apps/zbugs/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"lint": "oxlint --quiet --config ../../oxlint.config.ts src/ scripts/",
2626
"preview": "vite preview",
2727
"zero": "node --trace-warnings ../../packages/zero-cache/src/server/runner/main.ts",
28-
"zero-cache-compare": "ZERO_LOG_LEVEL=debug ZERO_CHANGE_STREAMER_SQLITE_CHANGE_LOG_MODE=compare ZERO_CHANGE_STREAMER_SQLITE_CHANGE_LOG_COMPARE_PERCENT=100 ZERO_CHANGE_STREAMER_SQLITE_CHANGE_LOG_RETENTION_MS=60000 node ../../packages/zero-cache/src/server/runner/main.ts",
2928
"zero-cache-dev": "node ../../packages/zero/src/zero-cache-dev.ts",
3029
"zero-brk": "node --inspect-brk ../../packages/zero-cache/src/server/debug/single.ts",
3130
"transform-query": "node ../../packages/zero-cache/src/scripts/transform-query.ts",

0 commit comments

Comments
 (0)