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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
**/node_modules
**/dist
**/*.tsbuildinfo
**/.react-router
.git
.diffity
.claude
plans
tmp
snapshot-*
131 changes: 131 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: deploy

on:
workflow_dispatch:

concurrency:
group: deploy
cancel-in-progress: false

env:
PROJECT: nc-innovation-496314
REGION: europe-west1
IMAGE: europe-west1-docker.pkg.dev/nc-innovation-496314/diffity/diffity

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
with:
context: .
push: false
load: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke test
run: |
docker run -d --name smoke -p 127.0.0.1:5390:5390 ${{ env.IMAGE }}:${{ github.sha }}
for _ in $(seq 1 30); do
if curl -fsS http://127.0.0.1:5390/healthz; then
docker rm -f smoke
exit 0
fi
sleep 1
done
docker logs smoke
exit 1

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: prod
deployment: false
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5

- uses: docker/setup-buildx-action@v3

- uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT }}

- uses: google-github-actions/setup-gcloud@v3

- run: gcloud auth configure-docker europe-west1-docker.pkg.dev --quiet

# rebuilt rather than carried over from `build`: the layers are all in the buildx gha cache,
# so this is a cache hit and only the push costs anything
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE }}:${{ github.sha }}
${{ env.IMAGE }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Deploy to Cloud Run
id: deploy
env:
GITHUB_APP_CLIENT_ID: ${{ vars.DIFFITY_GITHUB_APP_CLIENT_ID }}
GITHUB_APP_SLUG: ${{ vars.DIFFITY_GITHUB_APP_SLUG }}
# /projects/<project number>/global/backendServices/<id of the `diffity` backend service>
IAP_AUDIENCE: ${{ vars.DIFFITY_IAP_AUDIENCE }}
run: |
if [ -z "$IAP_AUDIENCE" ]; then
echo "Set the DIFFITY_IAP_AUDIENCE repository variable" >&2
exit 1
fi
env_vars="DIFFITY_PUBLIC_URL=https://diffity.prod.naturalcycles.net"
env_vars+=",DIFFITY_TRUST_PROXY=1"
env_vars+=",DIFFITY_IAP_AUDIENCE=${IAP_AUDIENCE}"
secrets="DATABASE_URL=diffity-database-url:latest,DIFFITY_PG_CA=diffity-postgres-ca:latest,DIFFITY_SECRET_KEY=diffity-secret-key:latest"
# Without the App, users paste a GitHub token on Settings instead.
if [ -n "$GITHUB_APP_CLIENT_ID" ]; then
env_vars+=",DIFFITY_GITHUB_APP_CLIENT_ID=${GITHUB_APP_CLIENT_ID}"
secrets+=",DIFFITY_GITHUB_APP_CLIENT_SECRET=diffity-github-app-client-secret:latest"
fi
if [ -n "$GITHUB_APP_SLUG" ]; then
env_vars+=",DIFFITY_GITHUB_APP_SLUG=${GITHUB_APP_SLUG}"
fi

gcloud run deploy diffity \
--project "$PROJECT" \
--image ${{ env.IMAGE }}:${{ github.sha }} \
--region "$REGION" \
--service-account diffity-runtime@nc-innovation-496314.iam.gserviceaccount.com \
--vpc-connector diffity-vpc-connector \
--vpc-egress private-ranges-only \
--allow-unauthenticated \
--ingress internal-and-cloud-load-balancing \
--no-default-url \
--execution-environment gen2 \
--memory 2Gi \
--cpu 1 \
--min-instances 1 \
--max-instances 1 \
--set-env-vars "$env_vars" \
--set-secrets "$secrets"

- name: announce deploy outcome in Slack
if: always() && env.SLACK_API_TOKEN != ''
env:
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
uses: NaturalCycles/ci-typescript/actions/slack-deployment-outcome@v2
with:
PROJECT_NAME: diffity
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
OUTCOME: ${{ steps.deploy.outcome }}
FAILURE_MSG: 'diffity deployment failed — check the Actions run for the failing step.'
90 changes: 0 additions & 90 deletions .github/workflows/release.yml

This file was deleted.

18 changes: 5 additions & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,17 @@ name: test
on:
pull_request:
push:
branches: [develop]
branches: [hosted-server]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
# The actual engines floor and the version releases build with.
node: ['22.13', 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- uses: actions/checkout@v5
- uses: NaturalCycles/ci-typescript/actions/setup-node-pnpm@v2
# Typecheck resolves workspace imports through built dists, so the build comes first.
- run: npm run build
- run: npm test
- run: pnpm build
- run: pnpm test
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,5 @@ dist/
*.tsbuildinfo
.DS_Store
*.local
.bin/
.claude/skills/
packages/cli/README.md
packages/cli/src/generated/
.react-router/
plans/
packages/cli/LICENSE
53 changes: 30 additions & 23 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
# Working on diffity

## Bump the version in every pull request
diffity is one hosted server: `packages/server` serves the review UI (`packages/ui`) and the MCP
endpoint. See the README for what it does, its environment and the deploy.

A running instance replaces itself only when the binary's version differs from the one it
registered (`packages/cli/src/index.ts`, `isStale`). Merge a behaviour change without a bump and
every instance already running keeps serving the old build, silently, until someone restarts it by
hand. So the version is not a release ceremony here — it is how a change reaches the people who
have diffity open.
## Layout

Bump inside the pull request, as part of the branch:
| Package | |
|---|---|
| `packages/parser` | Unified diff parsing, shared by the server and the UI |
| `packages/api` | The wire types and request parsers of the UI's `/s/:sid/api/*` routes |
| `packages/ui` | The review page (React Router in SPA mode, Vite, Tailwind); builds into `packages/server/dist/ui` |
| `packages/server` | Express: IAP or dev login, OAuth for MCP clients, the MCP tools and the `review` prompt (`src/review-prompt.md`), the UI's API, GitHub App connect and posting, Postgres (PGlite without `DATABASE_URL`) |

The server's esbuild bundle takes `api` and `parser` from source; the UI and the typecheck read
them from their built `dist`, so build before typechecking.

## Commands

From the repository root:

```bash
npx tsx scripts/release.ts patch --no-git # or: minor
pnpm install
pnpm build # parser → api → ui → server; the server build copies in the UI
pnpm test # typecheck, then every package's tests
pnpm dev # build once, then the server from source, restarted on change (needs the README's localhost env)
```

`--no-git` is what makes it safe in a branch. Without it the script commits and tags, and since
`develop` takes squash merges the commit is rewritten on the way in and the tag is left pointing at
a commit that never lands.

There are no npm publish scripts: every merge to develop whose version is not yet on npm is
published by `.github/workflows/release.yml`, with a `v<version>` tag and a GitHub release. The
workflow needs the `NPM_TOKEN` secret (publish rights on the `@naturalcycles` scope).

Bump in every pull request that touches anything the artifact is built from — `packages/`,
`scripts/`, `skills/`, `README.md`, `LICENSE`, or the root
`package.json`/`package-lock.json`/`tsconfig.json`; tests are exempt. The release workflow enforces exactly that set: a merge to develop whose version is
already on npm and whose diff (since that version's tag) touched it fails, which is what catches
two parallel branches bumping to the same number — the second to land would otherwise ship
nothing, silently. A comment-only source change trips the same wire; bump anyway, a patch number
costs nothing.
One package: `pnpm -F @diffity/server test`, `pnpm -F @diffity/ui typecheck`. Server tests run
on in-memory PGlite; nothing needs a database or network.

## Conventions

- Node 24. The version lives only in the root `package.json`; the server build embeds it.
- Every session, thread, comment and tour belongs to one user: a store query takes the user id, and
another user's id answers as not found. Keep the isolation tests passing.
- Schema changes are new numbered migrations in `packages/server/src/db.ts`; never edit an applied one.
- Comments explain a non-obvious current state only; no change history in code or docs.
44 changes: 44 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Build from the repository root: docker build -t diffity .

FROM node:24-slim AS base
ENV PNPM_HOME=/pnpm
ENV PATH=/pnpm:$PATH
RUN corepack enable && corepack prepare pnpm@12.3.1 --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/api/package.json packages/api/
COPY packages/parser/package.json packages/parser/
COPY packages/server/package.json packages/server/
COPY packages/ui/package.json packages/ui/

FROM base AS build
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# The workspace packages are bundled into dist; only the server's own dependencies are installed.
FROM base AS prod-deps
RUN pnpm install --frozen-lockfile --prod --filter @diffity/server

FROM node:24-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --home-dir /data --shell /usr/sbin/nologin diffity \
&& mkdir -p /data \
&& chown diffity:diffity /data
# The same layout as the install: packages/server/node_modules links into /app/node_modules/.pnpm.
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=prod-deps /app/packages/server/node_modules /app/packages/server/node_modules
COPY --from=build /app/packages/server/package.json /app/packages/server/package.json
COPY --from=build /app/packages/server/dist /app/packages/server/dist
WORKDIR /app/packages/server
ENV NODE_ENV=production \
DIFFITY_DATA_DIR=/data \
DIFFITY_BIND=0.0.0.0 \
PORT=5390
USER diffity
EXPOSE 5390
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || 5390) + '/healthz').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
CMD ["node", "dist/index.js"]
Loading
Loading