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
31 changes: 0 additions & 31 deletions .env.enc

This file was deleted.

81 changes: 7 additions & 74 deletions .githooks/post-checkout
Original file line number Diff line number Diff line change
@@ -1,78 +1,11 @@
#!/bin/bash
set -euo pipefail

# Install sops if not exists
# Detect platform
OS="$(uname -s)"
ARCH="$(uname -m)"

# Only run if sops not installed
if ! command -v sops >/dev/null 2>&1; then
echo "[INFO] sops not found, installing..."

case "$OS" in
Linux)
if [ "$ARCH" = "x86_64" ]; then
curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64
sudo mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
echo "[INFO] sops installed at /usr/local/bin/sops"
else
echo "[ERROR] Unsupported Linux architecture: $ARCH"
exit 1
fi
;;
Darwin) # macOS
echo "[INFO] Detected macOS"
if command -v brew >/dev/null 2>&1; then
brew install sops
else
echo "[ERROR] Homebrew not installed. Please install sops manually:"
echo " https://github.com/getsops/sops#installation"
exit 1
fi
;;
MINGW*|MSYS*|CYGWIN*) # Git Bash on Windows
echo "[ERROR] Windows detected. Please install sops manually:"
echo " choco install sops # (if using Chocolatey)"
echo " scoop install sops # (if using Scoop)"
exit 1
;;
*)
echo "[ERROR] Unsupported OS: $OS"
exit 1
;;
esac
fi

# Only run if age not installed
if ! command -v age >/dev/null 2>&1; then
echo "[INFO] age not found, installing..."

case "$OS" in
Linux)
sudo apt install age -y
;;
Darwin) # macOS
echo "[INFO] Detected macOS"
if command -v brew >/dev/null 2>&1; then
brew install age
else
echo "[ERROR] Homebrew not installed. Please install age manually:"
exit 1
fi
;;
*)
echo "[ERROR] Unsupported OS: $OS"
exit 1
;;
esac
# Secrets come from Doppler (project api-server). Nothing to decrypt on checkout —
# just nudge if the CLI is missing or the repo hasn't been bound yet.
if ! command -v doppler >/dev/null 2>&1; then
echo "[INFO] doppler CLI not found. Install it (https://docs.doppler.com/docs/install-cli),"
echo " then run: doppler login && doppler setup --no-interactive"
elif ! doppler configure get config --plain >/dev/null 2>&1; then
echo "[INFO] Doppler not configured for this repo. Run: doppler setup --no-interactive"
fi


# Auto-decrypt after checkout
if [ -f .env.enc ]; then
echo "Decrypting secrets after checkout..."
sops -d --input-type dotenv --output-type dotenv .env.enc > .env
fi

44 changes: 1 addition & 43 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,50 +1,8 @@
#!/bin/bash
set -euo pipefail

if [ -f .env ]; then
echo "Checking if .env has changed since last encryption..."

tmp_decrypted=$(mktemp)
if [ -f .env.enc ]; then
# Decrypt the existing .env.enc for comparison
if ! sops --decrypt --input-type dotenv --output-type dotenv .env.enc > "$tmp_decrypted"; then
echo "Warning: Failed to decrypt existing .env.enc, re-encrypting..."
tmp_decrypted="/dev/null"
fi
else
# No .env.enc exists yet
tmp_decrypted="/dev/null"
fi

# Normalize both files (strip CRLF, trailing spaces, and blank line differences) before comparing
norm_env=$(mktemp)
norm_dec=$(mktemp)

# Create normalized versions
awk 'NF{print $0}' .env | sed 's/\r$//' | sed 's/[[:space:]]*$//' | sort > "$norm_env"
if [ "$tmp_decrypted" != "/dev/null" ]; then
awk 'NF{print $0}' "$tmp_decrypted" | sed 's/\r$//' | sed 's/[[:space:]]*$//' | sort > "$norm_dec"
else
# force re-encrypt when there is no existing enc file
: > "$norm_dec"
fi

# Compare normalized content
if ! cmp -s "$norm_dec" "$norm_env"; then
echo "Encrypting updated .env before commit..."
sops --encrypt --input-type dotenv --output-type dotenv .env > .env.enc
git add .env.enc
else
echo ".env unchanged, skipping encryption."
fi

# Clean up temp files
[ "$tmp_decrypted" != "/dev/null" ] && rm -f "$tmp_decrypted"
rm -f "$norm_env" "$norm_dec"
fi

echo "[pre-commit] Running ansible-lint --fix (non-blocking)..."
ansible-lint --fix || {
echo "[pre-commit] ansible-lint failed or could not fix all issues, but continuing anyway."
}
git add -u
git add -u
18 changes: 4 additions & 14 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ jobs:
sparse-checkout: |
Dockerfile
entrypoint.sh
.env.enc
sparse-checkout-cone-mode: false

- name: Download JAR artifact
Expand All @@ -115,8 +114,6 @@ jobs:
run: |
echo "REPO=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
echo "IMAGE_TAG=${{ needs.ci-test.outputs.app-version || '0.0.1-SNAPSHOT' }}" >> $GITHUB_ENV
SOPS_VERSION=$(curl -s https://api.github.com/repos/getsops/sops/releases/latest | jq -r .tag_name)
echo "SOPS_VERSION=$SOPS_VERSION" >> $GITHUB_ENV

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Expand All @@ -125,8 +122,6 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
build-args: |
SOPS_VERSION=${{ env.SOPS_VERSION }}
push: true
platforms: linux/amd64
tags: |
Expand Down Expand Up @@ -158,8 +153,6 @@ jobs:
hosts.ini
requirements.yml
docker-compose.yml
.env.enc
.sops.yaml
sparse-checkout-cone-mode: false

- name: Ship ansible payload to server
Expand All @@ -169,15 +162,14 @@ jobs:
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
# Pass `templates` as a directory (not `templates/*`) so subpaths are preserved on the remote.
source: "playbook.yml,templates,hosts.ini,requirements.yml,docker-compose.yml,.env.enc,.sops.yaml"
source: "playbook.yml,templates,hosts.ini,requirements.yml,docker-compose.yml"
target: "/home/${{ secrets.SERVER_USER }}/api-server-deploy"
rm: true

- name: Run ansible + docker compose on server
uses: appleboy/ssh-action@v1.2.5
env:
AGE_PRIVATE_KEY: ${{ secrets.AGE_PRIVATE_KEY }}
TLS_KEYSTORE_PASSWORD: ${{ secrets.TLS_KEYSTORE_PASSWORD }}
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
GHCR_PAT_RO: ${{ secrets.GHCR_PAT_RO }}
# GHCR_USER must match the account that owns GHCR_PAT_RO — use the repo owner (stable)
# rather than github.actor (which varies by who triggers workflow_dispatch / tag push).
Expand All @@ -186,7 +178,7 @@ jobs:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
envs: AGE_PRIVATE_KEY,TLS_KEYSTORE_PASSWORD,GHCR_PAT_RO,GHCR_USER
envs: DOPPLER_TOKEN,GHCR_PAT_RO,GHCR_USER
script: |
set -euo pipefail
cd ~/api-server-deploy
Expand All @@ -195,9 +187,7 @@ jobs:
SECRETS_FILE="$(mktemp /tmp/ansible-secrets.XXXXXX.yml)"
trap 'shred -u "$SECRETS_FILE" 2>/dev/null || rm -f "$SECRETS_FILE"' EXIT
{
printf 'age_private_key: |\n'
printf '%s\n' "$AGE_PRIVATE_KEY" | sed 's/^/ /'
printf 'tls_keystore_password: "%s"\n' "${TLS_KEYSTORE_PASSWORD//\"/\\\"}"
printf 'doppler_token: "%s"\n' "${DOPPLER_TOKEN//\"/\\\"}"
printf 'ghcr_user: "%s"\n' "${GHCR_USER//\"/\\\"}"
printf 'ghcr_pat_ro: "%s"\n' "${GHCR_PAT_RO//\"/\\\"}"
printf 'docker_deploy: true\n'
Expand Down
5 changes: 0 additions & 5 deletions .sops.yaml

This file was deleted.

8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ mvn -P ci clean verify
# Build without tests
mvn -P ci -DskipTests=true clean package

# Run locally with dev profile
SPRING_PROFILES_ACTIVE=dev mvn spring-boot:run
# Run locally (Doppler injects dev secrets + SPRING_PROFILES_ACTIVE=dev)
doppler run -- mvn spring-boot:run

# Run a single test class
mvn -P ci -Dtest=com.chencraft.common.service.HashServiceTest test
Expand Down Expand Up @@ -56,7 +56,7 @@ mvn -P ci -Dtest=com.chencraft.common.service.HashServiceTest#testValidateHash t
### Configuration
- `application.properties` — prod config (port 8080, Prometheus metrics on 8957)
- `application-dev.properties` — dev overrides (port 8085, debug logging)
- Environment variables loaded from `.env` file (encrypted as `.env.enc` in Docker, decrypted via sops + age key)
- Secrets arrive as env vars from Doppler (project `api-server`, configs `dev`/`prd` — per-env Mongo URI/database, Cloudflare R2 bucket/keys, mTLS proxy secret). Local binding via committed `doppler.yaml`; prod container runs `doppler run` with a service token from `/opt/api-server/doppler.env`. Tests need no secrets.

### Test Infrastructure (`src/test/`)
- Mock/local service implementations: `LocalFileService`, `MockMailService`, `MockCertificateService`, `ImmediateTaskExecutor`
Expand All @@ -66,7 +66,7 @@ mvn -P ci -Dtest=com.chencraft.common.service.HashServiceTest#testValidateHash t

## Deployment

Tag push (`v*.*.*`) triggers `.github/workflows/maven.yml`: build & test → push image to GHCR → SCP the ansible payload (`playbook.yml`, `templates/`, `docker-compose.yml`, `.env.enc`, etc.) to the server → SSH as `githubdeploy` and run `ansible-playbook` unattended. The playbook converges nginx + PKCS#12 + deploy user, then (when `docker_deploy=true` is passed from CI) runs `docker compose pull && up -d` against `/opt/api-server/docker-compose.yml`. Manual `workflow_dispatch` re-runs the deploy job without rebuilding the image (useful for nginx-only changes). Laptop bootstrap is still `./install.sh`.
Tag push (`v*.*.*`) triggers `.github/workflows/maven.yml`: build & test → push image to GHCR → SCP the ansible payload (`playbook.yml`, `templates/`, `docker-compose.yml`, etc.) to the server → SSH as `githubdeploy` and run `ansible-playbook` unattended with `doppler_token` (the `DOPPLER_TOKEN` repo secret, a read-only `api-server/prd` service token). The playbook fetches the keystore password + nginx proxy secret from Doppler, converges nginx + PKCS#12 + deploy user, writes `/opt/api-server/doppler.env`, then (when `docker_deploy=true` is passed from CI) runs `docker compose pull && up -d` against `/opt/api-server/docker-compose.yml`. Manual `workflow_dispatch` re-runs the deploy job without rebuilding the image (useful for nginx-only changes). Laptop bootstrap is still `./install.sh`.

## Skill maintenance — `api-chencraft`

Expand Down
16 changes: 2 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
# ---- Stage 1: Download sops ----
FROM alpine:3.24 AS sops-downloader

ARG SOPS_VERSION=v3.11.0
WORKDIR /tmp

RUN apk add --no-cache curl \
&& curl -sSL -o sops https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64 \
&& chmod +x sops

# ---- Stage 2: Runtime ----
FROM eclipse-temurin:25-jre-alpine

LABEL org.opencontainers.image.source=https://github.com/SibeiC/api-server/
Expand All @@ -18,15 +7,14 @@ LABEL org.opencontainers.image.licenses=GPL-3.0-only
# Set working directory
WORKDIR /app

# Copy sops binary from downloader stage
COPY --from=sops-downloader /tmp/sops /usr/local/bin/sops
# Doppler CLI injects runtime secrets (DOPPLER_TOKEN supplied via docker-compose env_file)
COPY --from=dopplerhq/cli:3 /bin/doppler /usr/local/bin/doppler

# Copy built JAR from CI
COPY app/api-server.jar ./api-server.jar

# Copy relevant files
COPY entrypoint.sh .
COPY .env.enc .

# Make entrypoint executable
RUN chmod +x entrypoint.sh
Expand Down
Loading
Loading