ci: add semantic-release workflow and docs

This commit is contained in:
neon
2026-04-26 19:03:03 -06:00
parent c85379e4ff
commit 8ce315a7d0
9 changed files with 2862 additions and 80 deletions
+24 -37
View File
@@ -1,21 +1,19 @@
name: Container Image
run-name: ${{ github.event_name == 'workflow_run' && format('Image - {0} ({1})', github.event.workflow_run.head_commit.message, github.event.workflow_run.head_branch) || format('Image - {0}', github.ref_name) }}
run-name: >-
${{ github.event_name == 'workflow_run' && format('Image - {0} ({1})', github.event.workflow_run.head_commit.message, github.event.workflow_run.head_branch) || github.event_name == 'pull_request' && format('Image check - PR #{0}', github.event.pull_request.number) || format('Image - {0}', github.ref_name) }}
on:
pull_request:
branches: [main]
# Break-glass: bypasses CI/E2E gate. Use only when CI is broken and a hotfix image is urgently needed.
workflow_dispatch:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
push:
tags:
- "v*"
concurrency:
# Use the commit SHA across all trigger types so a workflow_run completion and a
# simultaneous tag push for the same commit share one concurrency slot and cancel correctly.
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.sha }}
cancel-in-progress: true
permissions:
@@ -28,10 +26,11 @@ env:
jobs:
image:
name: Build and Push Image
name: ${{ github.event_name == 'pull_request' && 'Build and Scan Image' || 'Build and Push Image' }}
if: >
github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push')
github.event_name == 'pull_request' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push')
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
@@ -48,7 +47,6 @@ jobs:
GH_SHA: ${{ github.sha }}
WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# SHA resolution mirrors the concurrency group logic at the top of this workflow.
if [[ "$EVENT_NAME" == "workflow_dispatch" && "$GH_REF" != "refs/heads/main" ]]; then
echo "workflow_dispatch is only supported from main."
exit 1
@@ -63,41 +61,28 @@ jobs:
SHORT_SHA="${SHA:0:12}"
IMAGE="$REGISTRY/$IMAGE_NAME"
PRIMARY_TAG="$GH_REF_NAME"
echo "checkout_ref=${SHA}" >> "$GITHUB_OUTPUT"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "scan_tag=${IMAGE}:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${IMAGE}:${PRIMARY_TAG}" >> "$GITHUB_OUTPUT"
echo "${IMAGE}:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
if [[ "$EVENT_NAME" == "pull_request" ]]; then
echo "should_push=false" >> "$GITHUB_OUTPUT"
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${IMAGE}:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
else
echo "should_push=true" >> "$GITHUB_OUTPUT"
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${IMAGE}:${GH_REF_NAME}" >> "$GITHUB_OUTPUT"
echo "${IMAGE}:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
fi
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.vars.outputs.checkout_ref }}
- name: Verify CI succeeded for tag commit
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
shell: bash
env:
GH_TOKEN: ${{ github.token }}
COMMIT_SHA: ${{ github.sha }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
echo "Checking CI status for commit ${COMMIT_SHA}..."
success_count=$(gh api --paginate \
"repos/${GH_REPO}/actions/workflows/ci.yml/runs?head_sha=${COMMIT_SHA}&status=completed&event=push&branch=main" \
--jq '[.workflow_runs[] | select(.conclusion == "success")] | length' \
| awk '{sum += $1} END {print sum}')
if [[ "${success_count:-0}" -lt 1 ]]; then
echo "No successful CI run found for tag commit ${COMMIT_SHA}. Refusing to publish image."
exit 1
fi
echo "CI verification passed: ${success_count} successful run(s) found."
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
@@ -124,6 +109,7 @@ jobs:
exit-code: "1"
- name: Log in to GHCR
if: steps.vars.outputs.should_push == 'true'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
@@ -131,6 +117,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image
if: steps.vars.outputs.should_push == 'true'
shell: bash
run: |
while IFS= read -r tag; do
+233
View File
@@ -0,0 +1,233 @@
name: Release
run-name: >-
${{ github.event_name == 'workflow_dispatch' && format('Release - {0}', github.ref_name) || format('Release checks - PR #{0}', github.event.pull_request.number) }}
on:
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && 'release' || format('pr-{0}', github.event.pull_request.number) }}
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
permissions:
actions: read
contents: write
issues: write
pull-requests: write
packages: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24"
PNPM_VERSION: "10"
jobs:
commitlint:
name: Lint PR Title
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Lint PR title
shell: bash
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: printf '%s\n' "$PR_TITLE" | pnpm exec commitlint --config commitlint.config.cjs
release-dry-run:
name: Release Dry Run
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Semantic release dry run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm exec semantic-release --dry-run --no-ci
release:
name: Publish Release
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
issues: write
pull-requests: write
outputs:
published: ${{ steps.semantic.outputs.new_release_published }}
version: ${{ steps.semantic.outputs.new_release_version }}
tag: ${{ steps.semantic.outputs.new_release_git_tag }}
steps:
- name: Guard against non-main branch
shell: bash
env:
GH_REF: ${{ github.ref }}
run: |
if [[ "$GH_REF" != "refs/heads/main" ]]; then
echo "::error::Release workflow must be dispatched from main. Current ref: $GH_REF"
exit 1
fi
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Verify CI success for current SHA
shell: bash
env:
GH_TOKEN: ${{ github.token }}
COMMIT_SHA: ${{ github.sha }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
echo "Checking CI status for commit ${COMMIT_SHA}..."
success_count=$(gh api --paginate \
"repos/${GH_REPO}/actions/workflows/ci.yml/runs?head_sha=${COMMIT_SHA}&status=completed&event=push&branch=main" \
--jq '[.workflow_runs[] | select(.conclusion == "success")] | length' \
| awk '{sum += $1} END {print sum}')
if [[ "${success_count:-0}" -lt 1 ]]; then
echo "::error::No successful CI run found for commit ${COMMIT_SHA}. Refusing to release."
exit 1
fi
echo "CI verification passed: ${success_count} successful run(s) found."
- name: Set up Node.js and pnpm
uses: ./.github/actions/setup-node-pnpm
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Run semantic-release
id: semantic
uses: cycjimmy/semantic-release-action@16ca923e6ccbb50770c415a0ccd43709a8c5f7a4 # v4.2.2
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release summary
if: steps.semantic.outputs.new_release_published == 'true'
shell: bash
env:
VERSION: ${{ steps.semantic.outputs.new_release_version }}
TAG: ${{ steps.semantic.outputs.new_release_git_tag }}
run: |
echo "## Release Published" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Version:** ${VERSION}" >> "$GITHUB_STEP_SUMMARY"
echo "- **Tag:** ${TAG}" >> "$GITHUB_STEP_SUMMARY"
docker-publish:
name: Publish Docker Image
needs: [release]
if: needs.release.outputs.published == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release.outputs.tag }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Resolve image tags
id: tags
shell: bash
env:
VERSION: ${{ needs.release.outputs.version }}
REGISTRY: ${{ env.REGISTRY }}
IMAGE: ${{ env.IMAGE_NAME }}
run: |
FULL_IMAGE="${REGISTRY}/${IMAGE}"
echo "scan_tag=${FULL_IMAGE}:${VERSION}" >> "$GITHUB_OUTPUT"
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${FULL_IMAGE}:${VERSION}" >> "$GITHUB_OUTPUT"
echo "${FULL_IMAGE}:latest" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./Dockerfile
push: false
load: true
tags: ${{ steps.tags.outputs.tags }}
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ steps.tags.outputs.scan_tag }}
format: table
vuln-type: os,library
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: "1"
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image
shell: bash
run: |
while IFS= read -r tag; do
[ -n "$tag" ] || continue
docker push "$tag"
done <<'EOF'
${{ steps.tags.outputs.tags }}
EOF
- name: Docker publish summary
shell: bash
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
echo "## Docker Image Published" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- **Tags:** \`${VERSION}\`, \`latest\`" >> "$GITHUB_STEP_SUMMARY"
+38
View File
@@ -0,0 +1,38 @@
{
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "security", "release": "patch" },
{ "type": "db", "release": "patch" },
{ "type": "style", "release": "patch" },
{ "type": "revert", "release": false }
]
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "security", "section": "Security" },
{ "type": "perf", "section": "Performance" },
{ "type": "db", "section": "Database" },
{ "type": "style", "section": "Visual Changes" }
]
}
}
],
"@semantic-release/github"
]
}
+13
View File
@@ -368,6 +368,19 @@ The same pipeline plus:
- **Container image build** (`.github/workflows/container-image.yml`) - Builds a Docker image and pushes to GitHub Container Registry.
### Releases
Releases are created manually via `workflow_dispatch` on the `release.yml` workflow. The release workflow:
1. Verifies CI has passed for the current `main` commit.
2. Runs semantic-release to determine the next version from commit history.
3. Creates a Git tag and GitHub Release with auto-generated release notes.
4. Builds, scans, and publishes a Docker image with version and `latest` tags.
PR titles are validated by commitlint to ensure they follow the [commit guidelines](COMMIT_GUIDELINES.md). Only releasable types (`feat`, `fix`, `perf`, `security`, `db`, `style`) trigger version bumps.
For the full release runbook, see [`docs/release-process.md`](docs/release-process.md).
### E2E in CI
E2E suites are dispatched automatically based on which files changed. On nightly builds and manual dispatches, all suites run. Each suite runs in its own job with a dedicated PostgreSQL service container.
+35 -38
View File
@@ -1,6 +1,7 @@
# BookOrbit
[![CI](https://github.com/neonsolstice/bookorbit/actions/workflows/ci.yml/badge.svg)](https://github.com/neonsolstice/bookorbit/actions/workflows/ci.yml)
[![Release](https://github.com/neonsolstice/bookorbit/actions/workflows/release.yml/badge.svg)](https://github.com/neonsolstice/bookorbit/actions/workflows/release.yml)
A self-hostable book and library management app with Kobo device support. Organize your epub, pdf, and cbz collections, track reading progress, and sync with your Kobo.
@@ -92,14 +93,13 @@ All commands run from the **repo root** unless noted otherwise.
### Everyday workflow
| Command | Description |
| -------------------- | -------------------------------------------------- |
| `pnpm setup` | One-time bootstrap |
| `pnpm dev` | Daily development |
| `pnpm verify` | Default local checks before push |
| `pnpm quick` | Faster local checks while coding |
| `pnpm verify:strict` | Aspirational strict gate (format + full typecheck) |
| `pnpm guide` | Print command reference |
| Command | Description |
| -------------------- | --------------------------------------------------- |
| `pnpm setup` | One-time bootstrap |
| `pnpm dev` | Daily development |
| `pnpm verify` | Default local checks before push |
| `pnpm verify:fast` | Faster local checks while coding (lint + typecheck) |
| `pnpm verify:strict` | Aspirational strict gate (format + full typecheck) |
### Development
@@ -111,14 +111,14 @@ All commands run from the **repo root** unless noted otherwise.
### Testing
| Command | Description |
| ------------------------------- | ---------------------------- |
| `pnpm test` | Server + client unit tests |
| `pnpm test:server` | Server unit tests |
| `pnpm test:client` | Client unit tests |
| `pnpm test:e2e:smoke` | Server smoke e2e |
| `pnpm coverage` | Coverage for server + client |
| `pnpm --filter server test:e2e` | Server e2e only |
| Command | Description |
| ------------------------------------- | ---------------------------- |
| `pnpm test` | Server + client unit tests |
| `pnpm test:server` | Server unit tests |
| `pnpm test:client` | Client unit tests |
| `pnpm run e2e:run -- guard-mechanics` | Server smoke e2e |
| `pnpm coverage` | Coverage for server + client |
| `pnpm --filter server test:e2e` | Server e2e only |
### Database
@@ -133,25 +133,21 @@ All commands run from the **repo root** unless noted otherwise.
### Production operations
| Command | Description |
| ------------------------------------------ | -------------------------------------------- |
| `pnpm prod:up` | Start/update production compose stack |
| `pnpm prod:down` | Stop production compose stack |
| `pnpm db:backup:prod` | Create Postgres backup (`local/backups/`) |
| `pnpm db:restore:prod -- <file.dump>` | Restore backup into production database |
| `pnpm db:restore:test:prod -- <file.dump>` | Restore into temp DB to validate backup file |
| Command | Description |
| ---------------- | ------------------------------------- |
| `pnpm prod:up` | Start/update production compose stack |
| `pnpm prod:down` | Stop production compose stack |
### Linting & formatting
| Command | Description |
| --------------------------------------- | ------------------------------------------ |
| `pnpm format:check` | Check formatting |
| `pnpm format` | Auto-format source files |
| `pnpm lint:check` | Non-mutating lint checks |
| `pnpm lint:fix` | Auto-fix lint issues |
| `pnpm typecheck` | Server typecheck + no-new client TS errors |
| `pnpm typecheck:full` | Typecheck server + client (strict) |
| `pnpm typecheck:client:baseline:update` | Refresh accepted client TS baseline |
| Command | Description |
| --------------------- | ---------------------------------- |
| `pnpm format:check` | Check formatting |
| `pnpm format` | Auto-format source files |
| `pnpm lint:check` | Non-mutating lint checks |
| `pnpm lint:fix` | Auto-fix lint issues |
| `pnpm typecheck` | Typecheck server + client |
| `pnpm typecheck:full` | Typecheck server + client (strict) |
---
@@ -375,9 +371,10 @@ Make sure you're accessing the client at `http://localhost:5173` (not port 3000)
## Further Reading
| Doc | What it covers |
| ------------------------------------------ | ------------------------------------------------------------ |
| [`docs/production.md`](docs/production.md) | Production compose deployment, backups, restore testing |
| [`server/README.md`](server/README.md) | Backend module map, DB commands, NestJS conventions |
| [`client/README.md`](client/README.md) | Frontend project layout, IDE setup, Vue/Tailwind conventions |
| [`packages/types/`](packages/types/) | Shared type definitions between server and client |
| Doc | What it covers |
| ---------------------------------------------------- | ------------------------------------------------------------ |
| [`docs/production.md`](docs/production.md) | Production compose deployment, backups, restore testing |
| [`docs/release-process.md`](docs/release-process.md) | Release workflow, PR title format, Docker publish, rollback |
| [`server/README.md`](server/README.md) | Backend module map, DB commands, NestJS conventions |
| [`client/README.md`](client/README.md) | Frontend project layout, IDE setup, Vue/Tailwind conventions |
| [`packages/types/`](packages/types/) | Shared type definitions between server and client |
+38
View File
@@ -0,0 +1,38 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [2, "always", ["feat", "fix", "db", "perf", "refactor", "style", "docs", "test", "build", "ci", "chore", "security", "revert"]],
"scope-enum": [
2,
"always",
[
"auth",
"library",
"books",
"authors",
"series",
"cover",
"metadata",
"kobo",
"opds",
"reader",
"collections",
"annotations",
"users",
"stats",
"notifications",
"settings",
"scanner",
"email",
"audit",
"smart-scope",
"types",
"docker",
"deps",
"server",
"client",
],
],
"scope-empty": [0],
},
};
+167
View File
@@ -0,0 +1,167 @@
# Release Process
This document covers how releases are created, what triggers Docker image publishing, and how to roll back if needed.
---
## Overview
BookOrbit uses [semantic-release](https://github.com/semantic-release/semantic-release) for automated versioning and GitHub Release creation. Releases are triggered manually via `workflow_dispatch` and produce:
- A Git tag (`vX.Y.Z`)
- A GitHub Release with auto-generated release notes
- A Docker image pushed to GHCR (`X.Y.Z` + `latest`)
---
## PR Requirements
### Squash merge required
All PRs to `main` must use **squash merge**. The squash commit message becomes the commit that semantic-release analyzes for version bumps.
### PR title format
PR titles must follow the [commit guidelines](../COMMIT_GUIDELINES.md):
```
<type>(<scope>): <summary>
```
The PR title is validated by commitlint in the release workflow. Invalid titles will fail the `Lint PR Title` check.
### Types that trigger releases
| Type | Release | Notes |
| ---------- | ------- | ------------------------ |
| `feat` | minor | New features |
| `fix` | patch | Bug fixes |
| `perf` | patch | Performance improvements |
| `security` | patch | Security patches |
| `db` | patch | Database/schema changes |
| `style` | patch | Visual/CSS changes |
| `revert` | none | No release |
| `refactor` | none | No release |
| `docs` | none | No release |
| `test` | none | No release |
| `build` | none | No release |
| `ci` | none | No release |
| `chore` | none | No release |
Breaking changes (`BREAKING CHANGE:` in footer) always trigger a **major** release regardless of type.
---
## Manual Release Runbook
Releases are triggered manually from the GitHub Actions UI.
### Prerequisites
1. All changes are merged to `main`.
2. CI has passed for the latest commit on `main`.
### Steps
1. Go to **Actions** > **Release** workflow.
2. Click **Run workflow**.
3. Select `main` branch.
4. Click **Run workflow**.
The workflow will:
1. Verify the dispatch is from `main`.
2. Verify CI has passed for the current commit.
3. Run semantic-release to determine the next version.
4. Create a Git tag and GitHub Release with release notes.
5. Build, scan, and push the Docker image (if a new version was published).
### No release produced
If all commits since the last release are non-releasable types (`chore`, `docs`, `test`, `build`, `ci`, `refactor`), semantic-release will skip the release. The Docker publish job will also be skipped.
---
## Docker Image Publishing
### Release images
The `release.yml` workflow publishes Docker images after a successful release:
- `ghcr.io/neonsolstice/bookorbit:<X.Y.Z>` - version-specific tag
- `ghcr.io/neonsolstice/bookorbit:latest` - latest stable release
Images are scanned with Trivy before push. If critical or high vulnerabilities are found, the push is blocked.
### Continuous validation images
The `container-image.yml` workflow still runs on CI success (push to `main`) and publishes validation images with branch and SHA tags. These are not release images.
---
## Rollback
### Rolling back to a previous release
To deploy a previous version:
```bash
# In your production docker-compose.yml or .env, set:
APP_IMAGE=ghcr.io/neonsolstice/bookorbit:X.Y.Z
```
Replace `X.Y.Z` with the version you want to roll back to, then redeploy:
```bash
pnpm prod:down
pnpm prod:up
```
### Deleting a bad release tag
If a release was created in error and needs to be removed:
```bash
# Delete tag locally and remotely
git tag -d vX.Y.Z
git push origin :refs/tags/vX.Y.Z
```
Then delete the corresponding GitHub Release from the Releases page.
---
## Release Workflow Architecture
The `release.yml` workflow has four jobs:
```
┌──────────────┐ ┌──────────────────┐
│ commitlint │ │ release-dry-run │ (PR only)
└──────────────┘ └──────────────────┘
┌──────────────┐ ┌──────────────────┐
│ release │────>│ docker-publish │ (workflow_dispatch only)
└──────────────┘ └──────────────────┘
```
- **commitlint**: Validates PR title format (PR only).
- **release-dry-run**: Runs semantic-release in dry-run mode to validate config (PR only).
- **release**: Creates the actual release (manual dispatch only). Outputs `published`, `version`, and `tag`.
- **docker-publish**: Builds and pushes Docker image (only when a release was published).
### Concurrency
- PR checks cancel older runs for the same PR.
- Manual release runs are serialized and never canceled mid-flight.
- PR checks and manual releases do not block each other.
---
## Configuration Files
| File | Purpose |
| ------------------------------- | ------------------------------------------------ |
| `.releaserc.json` | semantic-release config (plugins, release rules) |
| `commitlint.config.cjs` | commitlint config (type/scope validation) |
| `.github/workflows/release.yml` | Release and Docker publish workflow |
+12 -5
View File
@@ -4,10 +4,10 @@
"packageManager": "pnpm@10.32.1",
"scripts": {
"setup": "bash ./scripts/bootstrap/local.sh",
"dev": "concurrently --names \"types,server,client\" --prefix-colors \"cyan,blue,green\" \"pnpm --filter @bookorbit/types dev\" \"npm run dev:server\" \"npm run dev:client\"",
"dev:client": "cd client && npm run dev -- --host",
"dev:server": "cd server && npm run start:dev",
"verify:fast": "pnpm run lint:check && pnpm run typecheck && pnpm run test",
"dev": "concurrently --names \"types,server,client\" --prefix-colors \"cyan,blue,green\" \"pnpm --filter @bookorbit/types dev\" \"pnpm run dev:server\" \"pnpm run dev:client\"",
"dev:client": "cd client && pnpm dev -- --host",
"dev:server": "cd server && pnpm start:dev",
"verify:fast": "pnpm run lint:check && pnpm run typecheck",
"verify": "pnpm run lint:check && pnpm run typecheck && pnpm run test",
"verify:strict": "pnpm run format:check && pnpm run lint:check && pnpm run typecheck:full && pnpm run test",
"build:client": "pnpm --filter client build",
@@ -44,10 +44,17 @@
"pnpm": ">=9"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/github": "^11.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"concurrently": "^9.2.1",
"conventional-changelog-conventionalcommits": "^8.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"prettier": "^3.8.3"
"prettier": "^3.8.3",
"semantic-release": "^24.2.5"
},
"lint-staged": {
"*.{md,json,yml,yaml}": [
+2302
View File
File diff suppressed because it is too large Load Diff