diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 4d45a0c..3a88f42 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -135,6 +135,21 @@ jobs: docker push "git.garvis.dev/dmg/minecraft-account-manager:${VERSION}" docker push git.garvis.dev/dmg/minecraft-account-manager:latest + - name: Build and push Discord bot image + if: steps.release.outputs.created == 'true' + env: + VERSION: ${{ steps.release.outputs.version }} + run: | + docker build \ + --platform linux/amd64 \ + --target bot \ + --build-arg VERSION="$VERSION" \ + -t "git.garvis.dev/dmg/minecraft-account-manager-bot:${VERSION}" \ + -t git.garvis.dev/dmg/minecraft-account-manager-bot:latest \ + . + docker push "git.garvis.dev/dmg/minecraft-account-manager-bot:${VERSION}" + docker push git.garvis.dev/dmg/minecraft-account-manager-bot:latest + - name: Build and push migration image if: steps.release.outputs.created == 'true' env: diff --git a/Dockerfile b/Dockerfile index 3729bd5..3e01a44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,20 @@ USER app EXPOSE 3000 CMD ["node", "apps/web/server.js"] +FROM dependencies AS bot +ARG VERSION=development +LABEL org.opencontainers.image.title="Minecraft Account Manager Discord Bot" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.source="https://git.garvis.dev/dmg/minecraft-account-manager" +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system app && adduser --system --ingroup app app +COPY --chown=app:app package.json package-lock.json tsconfig.base.json ./ +COPY --chown=app:app apps/discord-bot ./apps/discord-bot +COPY --chown=app:app packages ./packages +USER app +CMD ["npm", "run", "start", "--workspace", "@minecraft-account-manager/discord-bot"] + FROM dependencies AS migrate ARG VERSION=development LABEL org.opencontainers.image.title="Minecraft Account Manager Migrations" \ diff --git a/apps/web/src/app/healthz/route.test.ts b/apps/web/src/app/healthz/route.test.ts new file mode 100644 index 0000000..782b885 --- /dev/null +++ b/apps/web/src/app/healthz/route.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { GET } from "./route"; + +describe("health endpoint", () => { + it("reports process readiness without requiring external services", async () => { + const response = GET(); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(await response.json()).toEqual({ status: "ok" }); + }); +}); diff --git a/apps/web/src/app/healthz/route.ts b/apps/web/src/app/healthz/route.ts new file mode 100644 index 0000000..7d34b6e --- /dev/null +++ b/apps/web/src/app/healthz/route.ts @@ -0,0 +1,10 @@ +export function GET(): Response { + return Response.json( + { status: "ok" }, + { + headers: { + "Cache-Control": "no-store", + }, + }, + ); +} diff --git a/design/log.md b/design/log.md index cfdfa81..55a7969 100644 --- a/design/log.md +++ b/design/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +* **Extend**: Added a releasable Discord bot image and a dependency-free web health endpoint for Kubernetes deployment. * **Verify**: Confirmed the initial `v1.0.0` release, public Velocity JAR, and versioned and `latest` web and migration image manifests. * **Create**: Added Gitea CI and semantic-release pipelines for downloadable Velocity JARs and versioned web and migration images. * **Document**: Added container deployment order, artifact names, and required repository secrets. diff --git a/design/us-015-platform-operations.md b/design/us-015-platform-operations.md index 726d411..7d03be1 100644 --- a/design/us-015-platform-operations.md +++ b/design/us-015-platform-operations.md @@ -3,7 +3,7 @@ type: User Story title: Deploy and operate the platform securely description: Operators have repeatable builds, migrations, credential provisioning, configuration, and security checks. tags: [operations, security, database, deployment] -timestamp: 2026-08-01T19:01:47Z +timestamp: 2026-08-01T19:46:09Z story_id: US-015 status: verified --- @@ -21,6 +21,7 @@ As a platform operator, I want reproducible deployment and security controls, so - [x] The Velocity Gradle wrapper produces a tested shaded JAR. - [x] Environment examples document database, Keycloak, Discord, trusted proxy, and ProxyCheck settings without secrets. - [x] The web application sets CSP, framing, MIME, referrer, and permissions headers. +- [x] The web runtime provides a dependency-free health endpoint for orchestration probes. - [x] npm dependency audit and Semgrep security review complete without findings at the last verified change. - [x] Architecture, Keycloak, API error, security, bot, and Velocity operating documentation is available. diff --git a/design/us-016-automated-releases.md b/design/us-016-automated-releases.md index 03d4b59..f599018 100644 --- a/design/us-016-automated-releases.md +++ b/design/us-016-automated-releases.md @@ -3,9 +3,9 @@ type: User Story title: Build and publish versioned releases description: Gitea Actions validate every change and publish semantically versioned Velocity and container artifacts. tags: [operations, ci, release, velocity, docker] -timestamp: 2026-08-01T19:18:42Z +timestamp: 2026-08-01T19:46:09Z story_id: US-016 -status: verified +status: implemented --- # User Story @@ -21,6 +21,7 @@ As a platform operator, I want automated validation and semantic releases, so th - [x] A release build embeds the semantic version in the Velocity plugin and JAR filename. - [x] A public Gitea release exposes the versioned Velocity JAR as a downloadable asset. - [x] Releases publish versioned and `latest` web runtime images to the Gitea registry. +- [ ] Releases publish versioned and `latest` Discord bot images to the Gitea registry. - [x] Releases publish versioned and `latest` migration images that run versioned Drizzle migrations. - [x] Runtime containers use unprivileged users and exclude development source and secrets where practical. - [x] Operators are told which repository secrets must be configured before the first push. @@ -36,7 +37,7 @@ As a platform operator, I want automated validation and semantic releases, so th # Validation -Local OKF, lint, typecheck, test, Next.js build, and versioned Velocity JAR checks pass. Initial Gitea CI and release runs succeeded. Release `v1.0.0` provides a publicly downloadable JAR whose Velocity metadata reports `1.0.0`. Registry manifests were resolved for versioned and `latest` web and migration images. Pull-request commitlint configuration is present; its conditional execution will be exercised by the first pull request. +Local OKF, lint, typecheck, test, Next.js build, and versioned Velocity JAR checks pass. Initial Gitea CI and release runs succeeded. Release `v1.0.0` provides a publicly downloadable JAR whose Velocity metadata reports `1.0.0`. Registry manifests were resolved for versioned and `latest` web and migration images. Discord bot image publication is implemented for the next feature release. Pull-request commitlint configuration is present; its conditional execution will be exercised by the first pull request. # Related Stories diff --git a/docs/releases.md b/docs/releases.md index 34adc8a..b23116e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -33,11 +33,17 @@ Each release creates: - Gitea release asset `minecraft-account-manager-velocity-VERSION.jar` - `git.garvis.dev/dmg/minecraft-account-manager:VERSION` - `git.garvis.dev/dmg/minecraft-account-manager:latest` +- `git.garvis.dev/dmg/minecraft-account-manager-bot:VERSION` +- `git.garvis.dev/dmg/minecraft-account-manager-bot:latest` - `git.garvis.dev/dmg/minecraft-account-manager-migrate:VERSION` - `git.garvis.dev/dmg/minecraft-account-manager-migrate:latest` Use immutable version tags for deployments. `latest` is a convenience pointer to the newest release. +## Discord bot + +Run exactly one bot replica with the same immutable release version as the web application. It requires `DATABASE_URL`, `APP_URL`, `DISCORD_BOT_TOKEN`, and `DISCORD_GUILD_ID`. Deploy slash commands separately with the release image when command definitions change. + ## Database migration Run the migration image for the same version before starting or replacing the web container: