ci: build image and release artifact on push to main
Modeled on kejadlen/ketchup's ci.yml, minus its deploy and
snapshot jobs; deploy waits until where the database lives is decided.
Assisted-by: GLM-5.3 via pi
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..ecf73c9
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,21 @@
+.git
+.jj
+.github
+.claude
+.direnv
+.envrc
+.quire
+.ramekin
+# data/ is the real address book; log/ holds unhandled captures with
+# every header. Neither belongs in an image.
+data
+log
+tmp
+docs
+test
+servers
+clients
+monica
+baikal
+work
+carddav.mobileconfig
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..311cb68
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,90 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read # checkout
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # v1
+ with:
+ bundler-cache: true
+ - run: bundle exec rake
+
+ zizmor:
+ name: Audit workflows
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read # checkout
+ security-events: write # upload SARIF to code scanning
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
+
+ build:
+ name: Build
+ needs: test
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # create GitHub release
+ packages: write # push container image to GHCR
+ outputs:
+ version: ${{ steps.meta.outputs.version }}
+ image: ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
+ - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - id: short-sha
+ run: echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
+ - id: change-id
+ run: echo "id=$(git cat-file -p HEAD | grep '^change-id ' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT"
+ - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
+ id: meta
+ with:
+ images: ghcr.io/${{ github.repository }}
+ tags: |
+ type=raw,value={{date 'YYYYMMDD-HHmm'}}-${{ steps.short-sha.outputs.sha }}
+ - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ COMMIT_SHA=${{ steps.short-sha.outputs.sha }}
+ CHANGE_ID=${{ steps.change-id.outputs.id }}
+ BUILD_DATE=${{ github.event.head_commit.timestamp }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ - env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
+ # --target names the commit explicitly: persist-credentials is
+ # false, so the tag cannot ride the leftover git credentials.
+ run: gh release create "${STEPS_META_OUTPUTS_VERSION}" --target "$GITHUB_SHA" --generate-notes
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..990aaf6
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,31 @@
+FROM ruby:4.0-slim AS build
+
+RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+COPY Gemfile Gemfile.lock ./
+RUN bundle config set without development && bundle install
+
+COPY . .
+
+FROM ruby:4.0-slim
+
+ARG COMMIT_SHA
+ARG CHANGE_ID
+ARG BUILD_DATE
+ENV COMMIT_SHA=${COMMIT_SHA}
+ENV CHANGE_ID=${CHANGE_ID}
+ENV BUILD_DATE=${BUILD_DATE}
+# Set TZ at runtime — Date.today drives the birthday list's rollover.
+
+WORKDIR /app
+
+COPY --from=build /usr/local/bundle /usr/local/bundle
+COPY --from=build /app /app
+
+EXPOSE 9292
+
+# 0.0.0.0 reaches only the container's own network namespace — exposure
+# to clients is tailscale serve's job, in front of the published port.
+CMD ["rackup", "-o", "0.0.0.0"]