Skip to content

Commit 170d0dd

Browse files
committed
Update GitHub Actions workflow and Dockerfile for improved caching and new entry point
1 parent 591de19 commit 170d0dd

3 files changed

Lines changed: 270 additions & 22 deletions

File tree

Lines changed: 118 additions & 18 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,149 @@ 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:
17-
if: github.repository == 'bluesky-social/atproto'
18-
runs-on: ubuntu-22.04
15+
build-platforms:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 60
1918
permissions:
2019
contents: read
2120
packages: write
21+
attestations: write
2222
id-token: write
23-
23+
strategy:
24+
fail-fast: false
25+
matrix:
26+
platform: [linux/amd64, linux/arm64]
2427
steps:
2528
- name: Checkout repository
26-
uses: actions/checkout@v3
29+
uses: actions/checkout@v5
2730

2831
- name: Setup Docker buildx
29-
uses: docker/setup-buildx-action@v2
32+
uses: docker/setup-buildx-action@v3
33+
34+
- name: Sanitize platform name
35+
id: platform
36+
run: |
37+
platform=${{ matrix.platform }}
38+
echo "safe=${platform//\//-}" >> $GITHUB_OUTPUT
3039
3140
- name: Log into registry ${{ env.REGISTRY }}
32-
uses: docker/login-action@v2
41+
uses: docker/login-action@v3
3342
with:
3443
registry: ${{ env.REGISTRY }}
3544
username: ${{ env.USERNAME }}
3645
password: ${{ env.PASSWORD }}
3746

3847
- name: Extract Docker metadata
3948
id: meta
40-
uses: docker/metadata-action@v4
49+
uses: docker/metadata-action@v5
4150
with:
4251
images: |
4352
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
4453
tags: |
45-
type=sha,enable=true,priority=100,prefix=bsky:,suffix=,format=long
54+
type=sha,enable=true,priority=100,prefix=bsky-,suffix=,format=long
55+
type=raw,value=bsky-{{date 'YYYY-MM-DDTHH-mm-ssZ'}}
56+
bsky-latest
4657
47-
- name: Build and push Docker image
48-
id: build-and-push
49-
uses: docker/build-push-action@v4
58+
- name: Build and push by digest
59+
id: build
60+
uses: docker/build-push-action@v6
5061
with:
5162
context: .
52-
push: ${{ github.event_name != 'pull_request' }}
63+
platforms: ${{ matrix.platform }}
5364
file: ./services/bsky/Dockerfile
54-
tags: ${{ steps.meta.outputs.tags }}
5565
labels: ${{ steps.meta.outputs.labels }}
56-
cache-from: type=gha
57-
cache-to: type=gha,mode=max
66+
cache-from: type=gha,scope=bsky-${{ steps.platform.outputs.safe }}
67+
cache-to: type=gha,mode=max,scope=bsky-${{ steps.platform.outputs.safe }}
68+
build-args: |
69+
BUILDKIT_INLINE_CACHE=1
70+
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
71+
72+
- name: Export digest
73+
run: |
74+
mkdir -p /tmp/digests
75+
digest="${{ steps.build.outputs.digest }}"
76+
touch "/tmp/digests/${digest#sha256:}"
77+
78+
- name: Upload digest
79+
uses: actions/upload-artifact@v4
80+
with:
81+
name: digests-${{ steps.platform.outputs.safe }}
82+
path: /tmp/digests/*
83+
if-no-files-found: error
84+
retention-days: 1
85+
86+
merge-manifests:
87+
runs-on: ubuntu-latest
88+
needs: build-platforms
89+
permissions:
90+
contents: write
91+
packages: write
92+
attestations: write
93+
id-token: write
94+
steps:
95+
- name: Download digests
96+
uses: actions/download-artifact@v4
97+
with:
98+
pattern: digests-*
99+
merge-multiple: true
100+
path: /tmp/digests
101+
102+
- name: Setup Docker buildx
103+
uses: docker/setup-buildx-action@v3
104+
with:
105+
driver-opts: |
106+
network=host
107+
108+
- name: Log into registry ${{ env.REGISTRY }}
109+
uses: docker/login-action@v3
110+
with:
111+
registry: ${{ env.REGISTRY }}
112+
username: ${{ env.USERNAME }}
113+
password: ${{ env.PASSWORD }}
114+
115+
- name: Extract Docker metadata
116+
id: meta
117+
uses: docker/metadata-action@v5
118+
with:
119+
images: |
120+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
121+
tags: |
122+
type=sha,enable=true,priority=100,prefix=bsky-,suffix=,format=long
123+
type=raw,value=bsky-{{date 'YYYY-MM-DDTHH-mm-ssZ'}}
124+
bsky-latest
125+
126+
- name: Create manifest list and push
127+
id: merge
128+
working-directory: /tmp/digests
129+
run: |
130+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
131+
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
132+
133+
- name: Inspect image and get digest
134+
id: inspect
135+
run: |
136+
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}')
137+
echo "digest=$digest" >> $GITHUB_OUTPUT
138+
139+
- name: Generate artifact attestation
140+
uses: actions/attest-build-provenance@v1
141+
with:
142+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
143+
subject-digest: ${{ steps.inspect.outputs.digest }}
144+
push-to-registry: true
145+
146+
- name: Checkout repository for tagging
147+
uses: actions/checkout@v5
148+
with:
149+
token: ${{ secrets.GITHUB_TOKEN }}
150+
151+
- name: Tag repository with datetime
152+
run: |
153+
DATETIME_TAG=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
154+
git config user.name "github-actions[bot]"
155+
git config user.email "github-actions[bot]@users.noreply.github.com"
156+
git tag -a "bsky-$DATETIME_TAG" -m "Release bsky-$DATETIME_TAG"
157+
git push origin "bsky-$DATETIME_TAG"

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)