ci(release): publish versioned artifacts
CI / validate (push) Successful in 6m40s
Release / release (push) Successful in 11m55s

This commit is contained in:
dmg
2026-08-01 15:02:31 -04:00
parent 326cc60688
commit 420389d601
14 changed files with 515 additions and 5 deletions
+13
View File
@@ -0,0 +1,13 @@
node_modules
**/node_modules
.next
**/.next
plugins/velocity/.gradle
plugins/velocity/build
.git
.gitea
.env
.env.*
!.env.example
*.log
.DS_Store
+71
View File
@@ -0,0 +1,71 @@
name: CI
on:
push:
branches:
- "**"
pull_request:
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: gradle
cache-dependency-path: plugins/velocity/*.gradle.kts
- name: Install dependencies
run: npm ci
- name: Validate conventional commits
if: github.event_name == 'pull_request'
run: |
npx -y \
-p @commitlint/cli@19 \
-p @commitlint/config-conventional@19 \
commitlint --from "${{ github.event.pull_request.base.sha }}" --to "${{ github.sha }}" \
--extends @commitlint/config-conventional
- name: Validate OKF design
run: npm run design:validate
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Build applications
run: npm run build
- name: Build and test Velocity plugin
run: ./gradlew clean test shadowJar
working-directory: plugins/velocity
- name: Upload development Velocity JAR
uses: actions/upload-artifact@v3
with:
name: minecraft-account-manager-velocity-${{ github.sha }}
path: plugins/velocity/build/libs/*.jar
if-no-files-found: error
+201
View File
@@ -0,0 +1,201 @@
name: Release
on:
push:
branches:
- main
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITEA_TOKEN }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: gradle
cache-dependency-path: plugins/velocity/*.gradle.kts
- name: Install dependencies
run: npm ci
- name: Validate release source
run: |
npm run design:validate
npm run lint
npm run typecheck
npm test
npm run build
(cd plugins/velocity && ./gradlew clean test)
- name: Capture previous tag
id: previous_tag
run: |
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
echo "value=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
else
echo "value=" >> "$GITHUB_OUTPUT"
fi
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
npx -y \
-p semantic-release@24.2.9 \
-p @semantic-release/commit-analyzer@13.0.1 \
-p @semantic-release/release-notes-generator@14.1.0 \
semantic-release
- name: Capture current tag
id: current_tag
run: |
if git describe --tags --exact-match HEAD >/dev/null 2>&1; then
echo "value=$(git describe --tags --exact-match HEAD)" >> "$GITHUB_OUTPUT"
else
echo "value=" >> "$GITHUB_OUTPUT"
fi
- name: Resolve release
id: release
env:
PREVIOUS_TAG: ${{ steps.previous_tag.outputs.value }}
CURRENT_TAG: ${{ steps.current_tag.outputs.value }}
run: |
if [ -n "$CURRENT_TAG" ] && [ "$CURRENT_TAG" != "$PREVIOUS_TAG" ]; then
echo "created=true" >> "$GITHUB_OUTPUT"
echo "tag=$CURRENT_TAG" >> "$GITHUB_OUTPUT"
echo "version=${CURRENT_TAG#v}" >> "$GITHUB_OUTPUT"
else
echo "created=false" >> "$GITHUB_OUTPUT"
fi
- name: Build versioned Velocity JAR
if: steps.release.outputs.created == 'true'
env:
VERSION: ${{ steps.release.outputs.version }}
run: ./gradlew clean test shadowJar -PreleaseVersion="$VERSION"
working-directory: plugins/velocity
- name: Upload release workflow artifact
if: steps.release.outputs.created == 'true'
uses: actions/upload-artifact@v3
with:
name: minecraft-account-manager-velocity-${{ steps.release.outputs.version }}
path: plugins/velocity/build/libs/minecraft-account-manager-velocity-${{ steps.release.outputs.version }}.jar
if-no-files-found: error
- name: Install Docker CLI
if: steps.release.outputs.created == 'true'
run: |
if ! command -v docker >/dev/null 2>&1; then
apt-get update
apt-get install -y ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce-cli
fi
- name: Log in to Gitea container registry
if: steps.release.outputs.created == 'true'
run: echo "${{ secrets.CONTAINER_REGISTRY_TOKEN }}" | docker login git.garvis.dev -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Build and push web image
if: steps.release.outputs.created == 'true'
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
docker build \
--platform linux/amd64 \
--target runner \
--build-arg VERSION="$VERSION" \
-t "git.garvis.dev/dmg/minecraft-account-manager:${VERSION}" \
-t git.garvis.dev/dmg/minecraft-account-manager:latest \
.
docker push "git.garvis.dev/dmg/minecraft-account-manager:${VERSION}"
docker push git.garvis.dev/dmg/minecraft-account-manager:latest
- name: Build and push migration image
if: steps.release.outputs.created == 'true'
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
docker build \
--platform linux/amd64 \
--target migrate \
--build-arg VERSION="$VERSION" \
-t "git.garvis.dev/dmg/minecraft-account-manager-migrate:${VERSION}" \
-t git.garvis.dev/dmg/minecraft-account-manager-migrate:latest \
.
docker push "git.garvis.dev/dmg/minecraft-account-manager-migrate:${VERSION}"
docker push git.garvis.dev/dmg/minecraft-account-manager-migrate:latest
- name: Create Gitea release and upload Velocity JAR
if: steps.release.outputs.created == 'true'
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
VERSION: ${{ steps.release.outputs.version }}
PREVIOUS_TAG: ${{ steps.previous_tag.outputs.value }}
run: |
api_url="${GITEA_SERVER_URL}/api/v1"
jar="plugins/velocity/build/libs/minecraft-account-manager-velocity-${VERSION}.jar"
export RELEASE_BODY
if [ -n "$PREVIOUS_TAG" ]; then
RELEASE_BODY=$(git log --pretty='format:- %s (%h)' "${PREVIOUS_TAG}..HEAD")
else
RELEASE_BODY=$(git log --pretty='format:- %s (%h)' HEAD)
fi
payload=$(node -e '
const payload = {
tag_name: process.env.TAG,
name: process.env.TAG,
body: process.env.RELEASE_BODY,
draft: false,
prerelease: false
};
process.stdout.write(JSON.stringify(payload));
')
response=$(curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${api_url}/repos/${REPOSITORY}/releases" \
--data "$payload")
release_id=$(printf '%s' "$response" | node -e '
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => {
const response = JSON.parse(input);
if (!response.id) process.exit(1);
process.stdout.write(String(response.id));
});
')
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${jar}" \
"${api_url}/repos/${REPOSITORY}/releases/${release_id}/assets?name=minecraft-account-manager-velocity-${VERSION}.jar"
+12
View File
@@ -0,0 +1,12 @@
{
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
["@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "chore", "scope": "deps", "release": "patch" }
]
}],
"@semantic-release/release-notes-generator"
]
}
+48
View File
@@ -0,0 +1,48 @@
# syntax=docker/dockerfile:1
FROM node:22-alpine AS dependencies
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
COPY apps/web/package.json ./apps/web/package.json
COPY apps/discord-bot/package.json ./apps/discord-bot/package.json
COPY packages/auth/package.json ./packages/auth/package.json
COPY packages/contracts/package.json ./packages/contracts/package.json
COPY packages/database/package.json ./packages/database/package.json
COPY packages/minecraft/package.json ./packages/minecraft/package.json
COPY packages/network/package.json ./packages/network/package.json
RUN npm ci
FROM dependencies AS builder
COPY . .
RUN npm run build --workspace @minecraft-account-manager/web
FROM node:22-alpine AS runner
ARG VERSION=development
LABEL org.opencontainers.image.title="Minecraft Account Manager" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://git.garvis.dev/dmg/minecraft-account-manager"
WORKDIR /app
ENV NODE_ENV=production \
HOSTNAME=0.0.0.0 \
PORT=3000
RUN addgroup --system app && adduser --system --ingroup app app
COPY --from=builder --chown=app:app /app/apps/web/.next/standalone ./
COPY --from=builder --chown=app:app /app/apps/web/.next/static ./apps/web/.next/static
USER app
EXPOSE 3000
CMD ["node", "apps/web/server.js"]
FROM dependencies AS migrate
ARG VERSION=development
LABEL org.opencontainers.image.title="Minecraft Account Manager Migrations" \
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 packages ./packages
USER app
CMD ["npm", "run", "db:migrate"]
+6
View File
@@ -47,6 +47,12 @@ npm run build
npm run velocity:build npm run velocity:build
``` ```
## Releases and containers
Gitea Actions validate every change and use conventional commits to produce semantic releases from `main`. Releases include a public, versioned Velocity JAR plus separate web runtime and Drizzle migration images.
See [`docs/releases.md`](docs/releases.md) for artifact names, required repository secrets, and deployment order.
## Database workflow ## Database workflow
Always create and apply versioned migrations: Always create and apply versioned migrations:
+1
View File
@@ -29,6 +29,7 @@ This OKF bundle is the product record for implemented and proposed behavior. Sto
* [US-013 — Manage users as an administrator](us-013-admin-user-management.md) - Administrators search users and manage names and Minecraft accounts. * [US-013 — Manage users as an administrator](us-013-admin-user-management.md) - Administrators search users and manage names and Minecraft accounts.
* [US-014 — Receive standardized API errors](us-014-problem-details.md) - Application APIs return RFC 9457 Problem Details. * [US-014 — Receive standardized API errors](us-014-problem-details.md) - Application APIs return RFC 9457 Problem Details.
* [US-015 — Deploy and operate securely](us-015-platform-operations.md) - Operators have reproducible builds, migrations, credentials, and security controls. * [US-015 — Deploy and operate securely](us-015-platform-operations.md) - Operators have reproducible builds, migrations, credentials, and security controls.
* [US-016 — Build and publish versioned releases](us-016-automated-releases.md) - Gitea Actions publish the Velocity JAR and web and migration images.
# Tracking # Tracking
+2
View File
@@ -2,6 +2,8 @@
## 2026-08-01 ## 2026-08-01
* **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.
* **Refine**: Corrected the Velocity Java and Gradle namespace to the repository owner's `games.dmg` reverse domain. * **Refine**: Corrected the Velocity Java and Gradle namespace to the repository owner's `games.dmg` reverse domain.
* **Create**: Established the OKF v0.1 [user-story index](index.md). * **Create**: Established the OKF v0.1 [user-story index](index.md).
* **Document**: Captured the implemented player portal, Discord authentication, onboarding, account management, network intelligence, Velocity admission, auditing, administration, API error, and operational stories. * **Document**: Captured the implemented player portal, Discord authentication, onboarding, account management, network intelligence, Velocity admission, auditing, administration, API error, and operational stories.
+2 -1
View File
@@ -3,7 +3,7 @@ type: User Story
title: Deploy and operate the platform securely title: Deploy and operate the platform securely
description: Operators have repeatable builds, migrations, credential provisioning, configuration, and security checks. description: Operators have repeatable builds, migrations, credential provisioning, configuration, and security checks.
tags: [operations, security, database, deployment] tags: [operations, security, database, deployment]
timestamp: 2026-08-01T18:43:58Z timestamp: 2026-08-01T19:01:47Z
story_id: US-015 story_id: US-015
status: verified status: verified
--- ---
@@ -41,3 +41,4 @@ Use `npm test`, `npm run typecheck`, `npm run lint`, `npm run build`, `npm run v
- [Administrator SSO](us-011-admin-sso.md) - [Administrator SSO](us-011-admin-sso.md)
- [Standardize API errors](us-014-problem-details.md) - [Standardize API errors](us-014-problem-details.md)
- [Build and publish versioned releases](us-016-automated-releases.md)
+44
View File
@@ -0,0 +1,44 @@
---
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:01:47Z
story_id: US-016
status: implemented
---
# User Story
As a platform operator, I want automated validation and semantic releases, so that deployable web, migration, and Velocity artifacts are reproducible and downloadable.
# Acceptance Criteria
- [ ] Pushes and pull requests run OKF validation, linting, type checks, tests, the web build, and the Velocity build.
- [ ] Pull requests validate conventional commit messages.
- [ ] CI uploads the development Velocity JAR as a workflow artifact.
- [ ] Main-branch conventional commits determine the next semantic version and create a `vMAJOR.MINOR.PATCH` tag.
- [x] A release build embeds the semantic version in the Velocity plugin and JAR filename.
- [ ] A public Gitea release exposes the versioned Velocity JAR as a downloadable asset.
- [ ] Releases publish versioned and `latest` web runtime images to the Gitea registry.
- [ ] 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.
# Implementation
- [CI workflow](../.gitea/workflows/ci.yml)
- [Release workflow](../.gitea/workflows/release.yml)
- [Semantic Release configuration](../.releaserc)
- [Web and migration Docker targets](../Dockerfile)
- [Velocity Gradle build](../plugins/velocity/build.gradle.kts)
- [Release and deployment guide](../docs/releases.md)
# Validation
Local OKF, lint, typecheck, test, Next.js build, and versioned Velocity JAR checks pass. A test `1.2.3` JAR was generated with matching Velocity metadata. Workflow YAML parses successfully. Container builds and remote publication remain pending because the local Docker daemon is unavailable and the first push is intentionally paused until repository secrets are configured.
# Related Stories
- [Deploy and operate securely](us-015-platform-operations.md)
- [Velocity game admission](us-009-velocity-admission.md)
+67
View File
@@ -0,0 +1,67 @@
# Releases and container deployment
The public Gitea repository is hosted at `https://git.garvis.dev/dmg/minecraft-account-manager`. Public release assets, including the Velocity plugin JAR, can be downloaded without repository authentication.
## Semantic versioning
Releases are calculated from conventional commits on `main`:
- `fix` creates a patch release.
- `feat` creates a minor release.
- `BREAKING CHANGE` or a breaking `!` creates a major release.
- `chore(deps)` creates a patch release.
- Other documentation, test, CI, and maintenance commits do not release by default.
Release tags use `vMAJOR.MINOR.PATCH`. With no existing tags, the first release is `v1.0.0`. The Git tag is the authoritative platform version.
## Required repository secrets
Configure these under **Repository settings → Actions → Secrets** before the first push:
| Secret | Purpose |
| --- | --- |
| `GITEA_TOKEN` | Checkout authentication, semantic-version tag publication, and Gitea release/asset creation. Use a token with write access to this repository. |
| `REGISTRY_USERNAME` | Gitea container-registry username, normally `dmg`. |
| `CONTAINER_REGISTRY_TOKEN` | Token used by Docker to publish packages for the `dmg` owner. |
Never commit these values or place them in image build arguments.
## Published artifacts
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-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.
## Database migration
Run the migration image for the same version before starting or replacing the web container:
```bash
docker run --rm \
-e DATABASE_URL='postgresql://user:password@postgres:5432/minecraft_accounts' \
git.garvis.dev/dmg/minecraft-account-manager-migrate:1.0.0
```
The image runs `drizzle-kit migrate` against the versioned SQL files. It never runs `drizzle push`. Back up production PostgreSQL and test migrations against a representative staging database before deployment.
## Web runtime
The web image contains the Next.js standalone server and runs as an unprivileged user:
```bash
docker run --rm -p 3000:3000 \
--env-file /path/to/minecraft-account-manager.env \
git.garvis.dev/dmg/minecraft-account-manager:1.0.0
```
Provide all deployment settings described by [`.env.example`](../.env.example). Only set `TRUST_PROXY=true` behind a proxy that overwrites forwarding headers.
## Velocity JAR
Download the JAR from the matching public Gitea release, copy it to Velocity's `plugins/` directory, and retain the existing `plugins/minecraft-account-manager/config.properties` during upgrades.
+12 -2
View File
@@ -2,14 +2,24 @@
The plugin checks every online-mode Java login against the account-manager API. It fails closed: unavailable, unauthorized, stale, replayed, malformed, and unknown requests are denied. The plugin checks every online-mode Java login against the account-manager API. It fails closed: unavailable, unauthorized, stale, replayed, malformed, and unknown requests are denied.
## Build ## Download or build
Versioned JARs are attached to public releases at `https://git.garvis.dev/dmg/minecraft-account-manager/releases`.
To build a development JAR locally:
```bash ```bash
cd plugins/velocity cd plugins/velocity
./gradlew clean test shadowJar ./gradlew clean test shadowJar
``` ```
Copy `build/libs/minecraft-account-manager-velocity-0.1.0.jar` to Velocity's `plugins/` directory and start Velocity once to create `plugins/minecraft-account-manager/config.properties`. To embed a release version in both the plugin metadata and filename:
```bash
./gradlew clean test shadowJar -PreleaseVersion=1.0.0
```
Copy `build/libs/minecraft-account-manager-velocity-VERSION.jar` to Velocity's `plugins/` directory and start Velocity once to create `plugins/minecraft-account-manager/config.properties`.
## Provision a credential ## Provision a credential
+35 -1
View File
@@ -4,7 +4,9 @@ plugins {
} }
group = "games.dmg" group = "games.dmg"
version = "0.1.0" version = providers.gradleProperty("releaseVersion")
.orElse("0.1.0-SNAPSHOT")
.get()
repositories { repositories {
mavenCentral() mavenCentral()
@@ -24,6 +26,38 @@ java {
toolchain.languageVersion.set(JavaLanguageVersion.of(17)) toolchain.languageVersion.set(JavaLanguageVersion.of(17))
} }
val generatedVersionDirectory = layout.buildDirectory.dir("generated/sources/version/java/main")
val generateVersionSource = tasks.register("generateVersionSource") {
val pluginVersion = providers.provider { project.version.toString() }
inputs.property("pluginVersion", pluginVersion)
outputs.dir(generatedVersionDirectory)
doLast {
val output = generatedVersionDirectory.get()
.file("games/dmg/accountmanager/BuildMetadata.java")
.asFile
output.parentFile.mkdirs()
output.writeText(
"""package games.dmg.accountmanager;
public final class BuildMetadata {
public static final String VERSION = "${pluginVersion.get()}";
private BuildMetadata() {}
}
"""
)
}
}
sourceSets.main {
java.srcDir(generatedVersionDirectory)
}
tasks.compileJava {
dependsOn(generateVersionSource)
}
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
@@ -16,7 +16,7 @@ import org.slf4j.Logger;
@Plugin( @Plugin(
id = "minecraft-account-manager", id = "minecraft-account-manager",
name = "Minecraft Account Manager", name = "Minecraft Account Manager",
version = "0.1.0", version = BuildMetadata.VERSION,
description = "Fail-closed admission checks for registered Java accounts" description = "Fail-closed admission checks for registered Java accounts"
) )
public final class MinecraftAccountManagerPlugin { public final class MinecraftAccountManagerPlugin {