Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 108 additions & 9 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ on:
- master
tags:
- "v*"
paths:
- "Dockerfile"
- "docker-entrypoint.sh"
- "config.docker.json"
- "go.mod"
- "go.sum"
- "cmd/**"
- "internal/**"
- "static/**"
- "frontend/**"
- ".dockerignore"
- ".github/workflows/docker-image.yml"
workflow_dispatch:

permissions:
Expand All @@ -19,15 +31,39 @@ concurrency:

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
prep:
runs-on: ubuntu-latest
outputs:
image_name_lc: ${{ steps.norm.outputs.image_name_lc }}
steps:
- name: Normalize image name
id: norm
run: |
echo "image_name_lc=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

build:
runs-on: ubuntu-latest
needs: prep
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
- platform: linux/arm64
arch: arm64

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand All @@ -42,21 +78,84 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
images: ${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push image
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=image,name=${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }},push-by-digest=true,name-canonical=true,push=true
cache-from: |
type=gha,scope=notion2api-${{ matrix.arch }}
type=registry,ref=${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }}:buildcache-${{ matrix.arch }}
cache-to: |
type=gha,mode=max,scope=notion2api-${{ matrix.arch }}
type=registry,ref=${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }}:buildcache-${{ matrix.arch }},mode=max,oci-mediatypes=true,image-manifest=true

- name: Export digest
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"

- name: Upload digest artifact
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

merge:
runs-on: ubuntu-latest
needs:
- prep
- build

steps:
- name: Download digest artifacts
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}

- name: Create and push manifest list
working-directory: ${{ runner.temp }}/digests
run: |
tags=$(jq -r '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
sources=$(printf '${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }}@sha256:%s ' *)
docker buildx imagetools create $tags $sources

- name: Inspect image
run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ needs.prep.outputs.image_name_lc }}:${{ steps.meta.outputs.version }}
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,4 @@ frontend/out/
*.spec.tsx
__tests__/
WEBUI_DEVELOPMENT_GUIDE.md

# Rust FFI build artifacts (v2 wreq-ffi)
wreq-ffi/target/
wreq-ffi/include/wreq_ffi.h
wreq-ffi/Cargo.lock
.serena/
117 changes: 10 additions & 107 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,17 @@ FROM --platform=$BUILDPLATFORM node:22-bookworm AS frontend-builder

WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci
COPY frontend ./
RUN npm run build

FROM --platform=$BUILDPLATFORM rust:1.86-bookworm AS rust-builder
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG TARGETARCH
ARG TARGETOS=linux
WORKDIR /src

RUN apt-get update -o Acquire::Retries=5 \
&& apt-get install -y -o Acquire::Retries=5 --no-install-recommends \
cmake perl build-essential libclang-dev clang lld file \
gcc-x86-64-linux-gnu g++-x86-64-linux-gnu \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/*

RUN set -eux; \
case "${TARGETARCH}" in \
amd64) RUST_TARGET=x86_64-unknown-linux-gnu ;; \
arm64) RUST_TARGET=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
rustup target add "${RUST_TARGET}"; \
echo "${RUST_TARGET}" > /tmp/rust_target

ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc \
CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc \
CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++ \
AR_x86_64_unknown_linux_gnu=x86_64-linux-gnu-ar \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \
AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar

ENV CARGO_TARGET_DIR=/cargo-target

COPY wreq-ffi ./wreq-ffi

RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/cargo-target,id=cargo-target-${TARGETARCH},sharing=locked \
set -eux; \
RUST_TARGET=$(cat /tmp/rust_target); \
case "${TARGETARCH}" in \
amd64) CC=x86_64-linux-gnu-gcc; CXX=x86_64-linux-gnu-g++; AR=x86_64-linux-gnu-ar ;; \
arm64) CC=aarch64-linux-gnu-gcc; CXX=aarch64-linux-gnu-g++; AR=aarch64-linux-gnu-ar ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
export CC CXX AR; \
echo "rust-builder toolchain: TARGETARCH=${TARGETARCH} RUST_TARGET=${RUST_TARGET} CC=${CC} CXX=${CXX} AR=${AR}"; \
echo "rust-builder diag: BUILDPLATFORM=${BUILDPLATFORM} TARGETPLATFORM=${TARGETPLATFORM} TARGETARCH=${TARGETARCH} RUST_TARGET=${RUST_TARGET} host=$(uname -m)"; \
cd wreq-ffi; \
mkdir -p include; \
touch src/lib.rs; \
cargo build --release --target "${RUST_TARGET}"; \
test -f include/wreq_ffi.h; \
mkdir -p /out; \
cp "${CARGO_TARGET_DIR}/${RUST_TARGET}/release/libwreq_ffi.a" /out/; \
cp include/wreq_ffi.h /out/; \
FIRST_MEMBER=$(ar t /out/libwreq_ffi.a | head -1); \
AFILE=$(ar p /out/libwreq_ffi.a "$FIRST_MEMBER" | file -); \
echo "rust-builder: first member ($FIRST_MEMBER) of /out/libwreq_ffi.a => ${AFILE}"; \
case "${TARGETARCH}" in \
amd64) echo "${AFILE}" | grep -q 'x86-64' || { echo "FATAL: /out/libwreq_ffi.a is not x86-64 (TARGETARCH=amd64). This usually means a cache mount got mixed up; try: docker buildx prune -af" >&2; exit 1; } ;; \
arm64) echo "${AFILE}" | grep -q 'aarch64' || { echo "FATAL: /out/libwreq_ffi.a is not aarch64 (TARGETARCH=arm64). This usually means a cache mount got mixed up; try: docker buildx prune -af" >&2; exit 1; } ;; \
esac; \
echo "rust-builder: arch verified for TARGETARCH=${TARGETARCH}"

FROM --platform=$BUILDPLATFORM golang:1.22-bookworm AS builder
FROM --platform=$BUILDPLATFORM golang:1.25.0-bookworm AS builder
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH

RUN apt-get update -o Acquire::Retries=5 \
&& apt-get install -y -o Acquire::Retries=5 --no-install-recommends \
file \
gcc-x86-64-linux-gnu g++-x86-64-linux-gnu \
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
Expand All @@ -95,48 +23,23 @@ COPY cmd ./cmd
COPY internal ./internal
COPY static ./static
COPY --from=frontend-builder /frontend/out /src/static/admin
COPY --from=rust-builder /out/libwreq_ffi.a /src/wreq-ffi/target/release/libwreq_ffi.a
COPY --from=rust-builder /out/wreq_ffi.h /src/wreq-ffi/include/wreq_ffi.h

RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
case "${TARGETARCH}" in \
amd64) CC=x86_64-linux-gnu-gcc; CXX=x86_64-linux-gnu-g++ ;; \
arm64) CC=aarch64-linux-gnu-gcc; CXX=aarch64-linux-gnu-g++ ;; \
*) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
echo "go-builder diag: BUILDPLATFORM=${BUILDPLATFORM} TARGETPLATFORM=${TARGETPLATFORM} TARGETARCH=${TARGETARCH} CC=${CC} host=$(uname -m)"; \
FIRST_MEMBER=$(ar t /src/wreq-ffi/target/release/libwreq_ffi.a | head -1); \
AFILE=$(ar p /src/wreq-ffi/target/release/libwreq_ffi.a "$FIRST_MEMBER" | file -); \
echo "go-builder: first member ($FIRST_MEMBER) of libwreq_ffi.a => ${AFILE}"; \
case "${TARGETARCH}" in \
amd64) echo "${AFILE}" | grep -q 'x86-64' || { echo "FATAL: libwreq_ffi.a in builder stage is not x86-64; rust-builder produced wrong arch or COPY layer is stale. Run: docker buildx prune -af" >&2; exit 1; } ;; \
arm64) echo "${AFILE}" | grep -q 'aarch64' || { echo "FATAL: libwreq_ffi.a in builder stage is not aarch64; rust-builder produced wrong arch or COPY layer is stale. Run: docker buildx prune -af" >&2; exit 1; } ;; \
esac; \
test -f ./cmd/notion2api/main.go; \
CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} CC=${CC} CXX=${CXX} \
go build -v -trimpath -tags wreq_ffi \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -v -trimpath \
-ldflags="-s -w" \
-o /out/notion2api ./cmd/notion2api

FROM node:22-bookworm-slim
FROM alpine:3.22

ARG TARGETARCH
ENV TZ=Asia/Shanghai
ENV NODE_PATH=/opt/notion2api-helper/node_modules
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata curl tini \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb \
&& mkdir -p /opt/notion2api-helper /app/config /app/data/notion_accounts /app/static

RUN cd /opt/notion2api-helper \
&& npm init -y >/dev/null 2>&1 \
&& npm install --omit=dev --no-package-lock node-wreq@2.2.1 \
&& test -d "$NODE_PATH/node-wreq" \
&& npm cache clean --force >/dev/null 2>&1
RUN apk add --no-cache ca-certificates tzdata curl tini \
&& mkdir -p /app/config /app/data/notion_accounts /app/static

COPY --from=builder /out/notion2api /app/notion2api
COPY --from=builder /src/static /app/static
Expand All @@ -150,5 +53,5 @@ EXPOSE 8787

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD curl -fsS http://127.0.0.1:8787/healthz || exit 1

ENTRYPOINT ["tini", "--", "docker-entrypoint.sh"]
ENTRYPOINT ["/sbin/tini", "--", "docker-entrypoint.sh"]
CMD ["./notion2api", "--config", "/app/config/config.json"]
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ docker compose up -d --build
docker compose -f docker-compose.prod.yml up -d --build
```

本地从源码开发需 Go `1.25.0+`(`go.mod` 已声明)。

## 默认入口

- API:`http://127.0.0.1:8787/v1/*`
Expand Down
4 changes: 0 additions & 4 deletions cmd/notion2api/main.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package main

import (
"log"

"notion2api/internal/app"
"notion2api/internal/wreq"
)

func main() {
log.Printf("notion2api: wreq backend = %s", wreq.Version())
app.Main()
}
13 changes: 13 additions & 0 deletions config.docker.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"persist_continuation_sessions": true,
"persist_sillytavern_bindings": true
},
"limits": {
"max_request_body_bytes": 4194304
},
"admin": {
"enabled": true,
"password": "change-me-admin-password",
Expand All @@ -46,6 +49,16 @@
"retry_on_auth_error": true,
"auto_switch_account": true
},
"dispatch": {
"probe_cache_ttl_seconds": 45
},
"browser": {
"helper_pool_size": 0
},
"debug": {
"pprof_enabled": false,
"pprof_addr": "127.0.0.1:6060"
},
"features": {
"use_web_search": true,
"use_read_only_mode": false,
Expand Down
13 changes: 13 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
"persist_continuation_sessions": true,
"persist_sillytavern_bindings": true
},
"limits": {
"max_request_body_bytes": 4194304
},
"features": {
"use_web_search": true,
"use_read_only_mode": false,
Expand All @@ -69,6 +72,16 @@
"retry_on_auth_error": true,
"auto_switch_account": true
},
"dispatch": {
"probe_cache_ttl_seconds": 45
},
"browser": {
"helper_pool_size": 0
},
"debug": {
"pprof_enabled": false,
"pprof_addr": "127.0.0.1:6060"
},
"accounts": [
{
"email": "alice@example.com",
Expand Down
Loading
Loading