Skip to content

Commit f643974

Browse files
committed
Update GitHub Actions workflow and Dockerfile for improved caching and new API entry point
1 parent 7804865 commit f643974

3 files changed

Lines changed: 252 additions & 21 deletions

File tree

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
name: build-and-push-bsky-ghcr
22
on:
3+
workflow_dispatch:
34
push:
45
branches:
56
- main
@@ -8,50 +9,132 @@ env:
89
REGISTRY: ghcr.io
910
USERNAME: ${{ github.actor }}
1011
PASSWORD: ${{ secrets.GITHUB_TOKEN }}
11-
12-
# github.repository as <account>/<repo>
1312
IMAGE_NAME: ${{ github.repository }}
1413

1514
jobs:
16-
bsky-container-ghcr:
15+
build-platforms:
1716
if: github.repository == 'bluesky-social/atproto'
18-
runs-on: ubuntu-22.04
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 60
1919
permissions:
2020
contents: read
2121
packages: write
22+
attestations: write
2223
id-token: write
23-
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
platform: [linux/amd64, linux/arm64, linux/arm/v7]
2428
steps:
2529
- name: Checkout repository
26-
uses: actions/checkout@v3
30+
uses: actions/checkout@v5
2731

2832
- name: Setup Docker buildx
29-
uses: docker/setup-buildx-action@v2
33+
uses: docker/setup-buildx-action@v3
3034

3135
- name: Log into registry ${{ env.REGISTRY }}
32-
uses: docker/login-action@v2
36+
uses: docker/login-action@v3
3337
with:
3438
registry: ${{ env.REGISTRY }}
3539
username: ${{ env.USERNAME }}
3640
password: ${{ env.PASSWORD }}
3741

3842
- name: Extract Docker metadata
3943
id: meta
40-
uses: docker/metadata-action@v4
44+
uses: docker/metadata-action@v5
4145
with:
4246
images: |
4347
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
4448
tags: |
45-
type=sha,enable=true,priority=100,prefix=bsky:,suffix=,format=long
49+
type=sha,enable=true,priority=100,prefix=bsky-,suffix=,format=long
50+
bsky-latest
4651
47-
- name: Build and push Docker image
48-
id: build-and-push
49-
uses: docker/build-push-action@v4
52+
- name: Build and push by digest
53+
id: build
54+
uses: docker/build-push-action@v6
5055
with:
5156
context: .
52-
push: ${{ github.event_name != 'pull_request' }}
57+
platforms: ${{ matrix.platform }}
5358
file: ./services/bsky/Dockerfile
54-
tags: ${{ steps.meta.outputs.tags }}
5559
labels: ${{ steps.meta.outputs.labels }}
56-
cache-from: type=gha
57-
cache-to: type=gha,mode=max
60+
cache-from: |
61+
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:bsky-cache-shared
62+
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:bsky-cache-${{ strategy.job-index }}
63+
cache-to: |
64+
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:bsky-cache-${{ strategy.job-index }},mode=max
65+
build-args: |
66+
BUILDKIT_INLINE_CACHE=1
67+
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
68+
69+
- name: Export digest
70+
run: |
71+
mkdir -p /tmp/digests
72+
digest="${{ steps.build.outputs.digest }}"
73+
touch "/tmp/digests/${digest#sha256:}"
74+
75+
- name: Upload digest
76+
uses: actions/upload-artifact@v4
77+
with:
78+
name: digests-${{ strategy.job-index }}
79+
path: /tmp/digests/*
80+
if-no-files-found: error
81+
retention-days: 1
82+
83+
merge-manifests:
84+
runs-on: ubuntu-latest
85+
needs: build-platforms
86+
permissions:
87+
contents: read
88+
packages: write
89+
attestations: write
90+
id-token: write
91+
steps:
92+
- name: Download digests
93+
uses: actions/download-artifact@v4
94+
with:
95+
pattern: digests-*
96+
merge-multiple: true
97+
path: /tmp/digests
98+
99+
- name: Setup Docker buildx
100+
uses: docker/setup-buildx-action@v3
101+
with:
102+
driver-opts: |
103+
network=host
104+
105+
- name: Log into registry ${{ env.REGISTRY }}
106+
uses: docker/login-action@v3
107+
with:
108+
registry: ${{ env.REGISTRY }}
109+
username: ${{ env.USERNAME }}
110+
password: ${{ env.PASSWORD }}
111+
112+
- name: Extract Docker metadata
113+
id: meta
114+
uses: docker/metadata-action@v5
115+
with:
116+
images: |
117+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
118+
tags: |
119+
type=sha,enable=true,priority=100,prefix=bsky-,suffix=,format=long
120+
bsky-latest
121+
122+
- name: Create manifest list and push
123+
id: merge
124+
working-directory: /tmp/digests
125+
run: |
126+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
127+
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
128+
129+
- name: Inspect image and get digest
130+
id: inspect
131+
run: |
132+
digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$(echo '${{ steps.meta.outputs.tags }}' | head -1 | cut -d':' -f2) | grep "^Digest:" | awk '{print $2}')
133+
echo "digest=$digest" >> $GITHUB_OUTPUT
134+
135+
- name: Generate artifact attestation
136+
uses: actions/attest-build-provenance@v1
137+
with:
138+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
139+
subject-digest: ${{ steps.inspect.outputs.digest }}
140+
push-to-registry: true

services/bsky/Dockerfile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ COPY ./packages/internal/xrpc-utils ./packages/internal/xrpc-utils
3131
COPY ./services/bsky ./services/bsky
3232

3333
# install all deps
34-
RUN pnpm install --frozen-lockfile > /dev/null
34+
RUN --mount=type=cache,target=/root/.pnpm-store \
35+
pnpm install --frozen-lockfile > /dev/null
3536
# build all packages with external node_modules
36-
RUN pnpm build > /dev/null
37+
RUN --mount=type=cache,target=/root/.pnpm-store \
38+
pnpm build > /dev/null
3739
# clean up
3840
RUN rm -rf node_modules
3941
# install only prod deps, hoisted to root node_modules dir
40-
RUN pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline > /dev/null
42+
RUN --mount=type=cache,target=/root/.pnpm-store \
43+
pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline > /dev/null
4144

4245
WORKDIR services/bsky
4346

@@ -61,7 +64,7 @@ ENV NODE_ENV=production
6164

6265
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
6366
USER node
64-
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "api.js"]
67+
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "api-zeppelin.js"]
6568

6669
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto
6770
LABEL org.opencontainers.image.description="Bsky App View"

services/bsky/api-zeppelin.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copied from: https://github.com/zeppelin-social/atproto/blob/main/services/bsky/api.js
2+
3+
// @ts-check
4+
/* eslint-env node */
5+
/* eslint-disable import/order */
6+
7+
'use strict'
8+
9+
const dd = require('dd-trace')
10+
11+
dd.tracer
12+
.init()
13+
.use('http2', {
14+
client: true, // calls into dataplane
15+
server: false,
16+
})
17+
.use('express', {
18+
hooks: {
19+
request: (span, req) => {
20+
maintainXrpcResource(span, req)
21+
},
22+
},
23+
})
24+
25+
// modify tracer in order to track calls to dataplane as a service with proper resource names
26+
const DATAPLANE_PREFIX = '/bsky.Service/'
27+
const origStartSpan = dd.tracer._tracer.startSpan
28+
dd.tracer._tracer.startSpan = function (name, options) {
29+
if (
30+
name !== 'http.request' ||
31+
options?.tags?.component !== 'http2' ||
32+
!options?.tags?.['http.url']
33+
) {
34+
return origStartSpan.call(this, name, options)
35+
}
36+
const uri = new URL(options.tags['http.url'])
37+
if (!uri.pathname.startsWith(DATAPLANE_PREFIX)) {
38+
return origStartSpan.call(this, name, options)
39+
}
40+
options.tags['service.name'] = 'dataplane-bsky'
41+
options.tags['resource.name'] = uri.pathname.slice(DATAPLANE_PREFIX.length)
42+
return origStartSpan.call(this, name, options)
43+
}
44+
45+
// Tracer code above must come before anything else
46+
const path = require('node:path')
47+
const assert = require('node:assert')
48+
const cluster = require('node:cluster')
49+
const { Secp256k1Keypair } = require('@atproto/crypto')
50+
const bsky = require('@atproto/bsky') // import all bsky features
51+
52+
const appview = async () => {
53+
const env = getEnv()
54+
const config = bsky.ServerConfig.readEnv()
55+
assert(env.serviceSigningKey, 'must set BSKY_SERVICE_SIGNING_KEY')
56+
assert(env.dbPostgresUrl, 'must set BSKY_DB_POSTGRES_URL')
57+
const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey)
58+
59+
const db = new bsky.Database({
60+
url: env.dbPostgresUrl,
61+
schema: env.dbPostgresSchema,
62+
poolSize: env.dbPoolSize,
63+
})
64+
65+
// ends: involve logics in packages/dev-env/src/bsky.ts <<<<<<<<<<<<<
66+
67+
assert(env.bsyncPort, 'must set BSKY_BSYNC_PORT')
68+
assert(env.dataplanePort, 'must set BSKY_DATAPLANE_PORT')
69+
70+
71+
const bsync = await bsky.MockBsync.create(db, env.bsyncPort)
72+
73+
const dataplane = await bsky.DataPlaneServer.create(
74+
db,
75+
env.dataplanePort,
76+
config.didPlcUrl,
77+
)
78+
79+
const migrationDb = new bsky.Database({
80+
url: env.dbPostgresUrl,
81+
schema: env.dbPostgresSchema,
82+
})
83+
await migrationDb.migrateToLatestOrThrow()
84+
await migrationDb.close()
85+
86+
const server = bsky.BskyAppView.create({ config, signingKey })
87+
88+
assert(env.repoProvider, 'must set BSKY_REPO_PROVIDER')
89+
90+
const sub = new bsky.RepoSubscription({
91+
service: env.repoProvider,
92+
db,
93+
idResolver: dataplane.idResolver,
94+
})
95+
96+
97+
await server.start()
98+
99+
sub.start()
100+
// Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/)
101+
const shutdown = async () => {
102+
await sub.destroy()
103+
await server.destroy()
104+
await dataplane.destroy()
105+
await bsync.destroy()
106+
await db.close()
107+
}
108+
process.on('SIGTERM', shutdown)
109+
process.on('disconnect', shutdown) // when clustering
110+
}
111+
112+
const getEnv = () => ({
113+
serviceSigningKey: process.env.BSKY_SERVICE_SIGNING_KEY || undefined,
114+
dbPostgresUrl: process.env.BSKY_DB_POSTGRES_URL || undefined,
115+
dbPostgresSchema: process.env.BSKY_DB_POSTGRES_SCHEMA || undefined,
116+
dbPoolSize: maybeParseInt(process.env.BSKY_DB_POOL_SIZE) || undefined,
117+
dataplanePort: maybeParseInt(process.env.BSKY_DATAPLANE_PORT) || undefined,
118+
bsyncPort: maybeParseInt(process.env.BSKY_BSYNC_PORT) || undefined,
119+
migration: process.env.ENABLE_MIGRATIONS === 'true' || undefined,
120+
repoProvider: process.env.BSKY_REPO_PROVIDER || undefined,
121+
})
122+
123+
const maybeParseInt = (str) => {
124+
if (!str) return
125+
const int = parseInt(str, 10)
126+
if (isNaN(int)) return
127+
return int
128+
}
129+
130+
const maintainXrpcResource = (span, req) => {
131+
// Show actual xrpc method as resource rather than the route pattern
132+
if (span && req.originalUrl?.startsWith('/xrpc/')) {
133+
span.setTag(
134+
'resource.name',
135+
[
136+
req.method,
137+
path.posix.join(req.baseUrl || '', req.path || '', '/').slice(0, -1), // Ensures no trailing slash
138+
]
139+
.filter(Boolean)
140+
.join(' '),
141+
)
142+
}
143+
}
144+
145+
appview().catch(console.error)

0 commit comments

Comments
 (0)