ci: release workflow — build + push container images to GHCR on release (#6)
* feat: add release workflow to build and push container images to GHCR
Triggers on GitHub Release publish. Builds three container images
(consentos-api, consentos-scanner, consentos-admin-ui) and pushes
them to ghcr.io/consentos/ tagged with the semver release version
(e.g. v1.0.0, 1.0), plus ``latest``.
Release flow:
1. Merge PRs to master.
2. Tag: ``git tag v1.0.0 && git push origin v1.0.0``
3. Create a GitHub Release from the tag.
4. Workflow fires, images land on GHCR.
5. Deploy by pointing Helm values or docker-compose at the tag.
Uses ``docker/metadata-action`` for tag derivation and
``docker/build-push-action`` for the builds. Auth uses the
default ``GITHUB_TOKEN`` with ``packages: write`` — no extra
secrets needed.
The admin-ui image uses the repo root as the build context (same
as ``docker-compose.prod.yml``) so the Dockerfile can pull in
``apps/banner/`` alongside ``apps/admin-ui/`` and bundle the
banner output at the nginx root.
* chore: auto-graduate changelog on release + CI path filters
CI workflow (``ci.yml``):
- Uses ``dorny/paths-filter`` to detect which apps changed. Each
job group (api, scanner, banner, admin-ui) now has an
``if: needs.changes.outputs.<app> == 'true'`` guard so it only
runs when files under its ``apps/<app>/`` directory were
modified. A docs-only or infra-only PR no longer triggers the
full lint + test + build matrix.
This commit is contained in:
50
.github/workflows/ci.yml
vendored
50
.github/workflows/ci.yml
vendored
@@ -11,8 +11,35 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ── Detect which apps changed ──────────────────────────────────────
|
||||||
|
changes:
|
||||||
|
name: Detect changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
api: ${{ steps.filter.outputs.api }}
|
||||||
|
scanner: ${{ steps.filter.outputs.scanner }}
|
||||||
|
banner: ${{ steps.filter.outputs.banner }}
|
||||||
|
admin-ui: ${{ steps.filter.outputs.admin-ui }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
api:
|
||||||
|
- 'apps/api/**'
|
||||||
|
scanner:
|
||||||
|
- 'apps/scanner/**'
|
||||||
|
banner:
|
||||||
|
- 'apps/banner/**'
|
||||||
|
admin-ui:
|
||||||
|
- 'apps/admin-ui/**'
|
||||||
|
|
||||||
|
# ── API ────────────────────────────────────────────────────────────
|
||||||
api-lint:
|
api-lint:
|
||||||
name: API Lint
|
name: API Lint
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.api == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -29,6 +56,8 @@ jobs:
|
|||||||
|
|
||||||
api-test:
|
api-test:
|
||||||
name: API Tests
|
name: API Tests
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.api == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -74,8 +103,11 @@ jobs:
|
|||||||
DATABASE_URL: postgresql://consentos_test:consentos_test@localhost:5432/consentos_test
|
DATABASE_URL: postgresql://consentos_test:consentos_test@localhost:5432/consentos_test
|
||||||
- run: pytest --cov=src --cov-report=term-missing -v
|
- run: pytest --cov=src --cov-report=term-missing -v
|
||||||
|
|
||||||
|
# ── Scanner ────────────────────────────────────────────────────────
|
||||||
scanner-lint:
|
scanner-lint:
|
||||||
name: Scanner Lint
|
name: Scanner Lint
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.scanner == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -92,6 +124,8 @@ jobs:
|
|||||||
|
|
||||||
scanner-test:
|
scanner-test:
|
||||||
name: Scanner Tests
|
name: Scanner Tests
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.scanner == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -105,8 +139,11 @@ jobs:
|
|||||||
- run: pip install -e ".[dev]"
|
- run: pip install -e ".[dev]"
|
||||||
- run: pytest --cov=src --cov-report=term-missing -v
|
- run: pytest --cov=src --cov-report=term-missing -v
|
||||||
|
|
||||||
|
# ── Banner ─────────────────────────────────────────────────────────
|
||||||
banner-lint:
|
banner-lint:
|
||||||
name: Banner Lint & Typecheck
|
name: Banner Lint & Typecheck
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.banner == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -123,6 +160,8 @@ jobs:
|
|||||||
|
|
||||||
banner-test:
|
banner-test:
|
||||||
name: Banner Tests
|
name: Banner Tests
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.banner == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -139,8 +178,9 @@ jobs:
|
|||||||
|
|
||||||
banner-build:
|
banner-build:
|
||||||
name: Banner Build
|
name: Banner Build
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [banner-test, banner-lint]
|
needs: [banner-test, banner-lint]
|
||||||
|
if: needs.changes.outputs.banner == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: apps/banner
|
working-directory: apps/banner
|
||||||
@@ -163,8 +203,11 @@ jobs:
|
|||||||
echo "::warning::consent-loader.js is ${LOADER_SIZE} bytes (>20KB) — consider optimising"
|
echo "::warning::consent-loader.js is ${LOADER_SIZE} bytes (>20KB) — consider optimising"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Admin UI ───────────────────────────────────────────────────────
|
||||||
admin-ui-lint:
|
admin-ui-lint:
|
||||||
name: Admin UI Typecheck
|
name: Admin UI Typecheck
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.admin-ui == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -181,6 +224,8 @@ jobs:
|
|||||||
|
|
||||||
admin-ui-test:
|
admin-ui-test:
|
||||||
name: Admin UI Tests
|
name: Admin UI Tests
|
||||||
|
needs: changes
|
||||||
|
if: needs.changes.outputs.admin-ui == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -197,8 +242,9 @@ jobs:
|
|||||||
|
|
||||||
admin-ui-build:
|
admin-ui-build:
|
||||||
name: Admin UI Build
|
name: Admin UI Build
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [admin-ui-test, admin-ui-lint]
|
needs: [admin-ui-test, admin-ui-lint]
|
||||||
|
if: needs.changes.outputs.admin-ui == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: apps/admin-ui
|
working-directory: apps/admin-ui
|
||||||
|
|||||||
30
.github/workflows/pr-title.yml
vendored
Normal file
30
.github/workflows/pr-title.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: PR Title
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Conventional commit title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check PR title
|
||||||
|
uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
types: |
|
||||||
|
feat
|
||||||
|
fix
|
||||||
|
chore
|
||||||
|
refactor
|
||||||
|
docs
|
||||||
|
test
|
||||||
|
style
|
||||||
|
perf
|
||||||
|
ci
|
||||||
|
build
|
||||||
|
requireScope: false
|
||||||
|
subjectPattern: ^.+$
|
||||||
|
subjectPatternError: "PR title must follow conventional commits: type: description (e.g. feat: add cookie categories)"
|
||||||
181
.github/workflows/release.yml
vendored
Normal file
181
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Cut a release and build container images.
|
||||||
|
#
|
||||||
|
# Triggered manually via ``workflow_dispatch`` (Actions tab → Run
|
||||||
|
# workflow) when you're ready to ship. Not on every PR merge.
|
||||||
|
#
|
||||||
|
# Flow:
|
||||||
|
# 1. ``ietf-tools/semver-action`` derives the next version from
|
||||||
|
# conventional commit messages since the last tag (feat → minor,
|
||||||
|
# fix → patch, breaking → major).
|
||||||
|
# 2. ``requarks/changelog-action`` generates release notes from the
|
||||||
|
# commit diff between the new and previous tags.
|
||||||
|
# 3. ``ncipollo/release-action`` creates the GitHub Release.
|
||||||
|
# 4. ``requarks/changelog-action`` writes CHANGELOG.md and
|
||||||
|
# ``stefanzweifel/git-auto-commit-action`` commits it back to
|
||||||
|
# master with ``[skip ci]``.
|
||||||
|
# 5. All three container images are built and pushed to GHCR,
|
||||||
|
# tagged with the semver version + ``latest``.
|
||||||
|
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Version + Release + Changelog ────────────────────────────────
|
||||||
|
version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
new: ${{ steps.semver.outputs.next }}
|
||||||
|
newStrict: ${{ steps.semver.outputs.nextStrict }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get next version
|
||||||
|
id: semver
|
||||||
|
uses: ietf-tools/semver-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
branch: master
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: changelog
|
||||||
|
uses: requarks/changelog-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
fromTag: master
|
||||||
|
toTag: ${{ steps.semver.outputs.current }}
|
||||||
|
writeToFile: false
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: ncipollo/release-action@v1.12.0
|
||||||
|
with:
|
||||||
|
allowUpdates: true
|
||||||
|
draft: false
|
||||||
|
makeLatest: true
|
||||||
|
tag: ${{ steps.semver.outputs.next }}
|
||||||
|
body: ${{ steps.changelog.outputs.changes }}
|
||||||
|
token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Write CHANGELOG.md
|
||||||
|
uses: requarks/changelog-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
fromTag: ${{ steps.semver.outputs.next }}
|
||||||
|
toTag: ${{ steps.semver.outputs.current }}
|
||||||
|
writeToFile: true
|
||||||
|
|
||||||
|
- name: Commit CHANGELOG.md
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
|
with:
|
||||||
|
branch: master
|
||||||
|
commit_message: "docs: update CHANGELOG.md for ${{ steps.semver.outputs.next }} [skip ci]"
|
||||||
|
file_pattern: CHANGELOG.md
|
||||||
|
|
||||||
|
# ── Build and push container images ──────────────────────────────
|
||||||
|
build-api:
|
||||||
|
name: API image
|
||||||
|
needs: version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v5
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ghcr.io/consentos/consentos-api
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},value=${{ needs.version.outputs.newStrict }}
|
||||||
|
type=semver,pattern={{major}}.{{minor}},value=${{ needs.version.outputs.newStrict }}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: apps/api
|
||||||
|
file: apps/api/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
build-scanner:
|
||||||
|
name: Scanner image
|
||||||
|
needs: version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v5
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ghcr.io/consentos/consentos-scanner
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},value=${{ needs.version.outputs.newStrict }}
|
||||||
|
type=semver,pattern={{major}}.{{minor}},value=${{ needs.version.outputs.newStrict }}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: apps/scanner
|
||||||
|
file: apps/scanner/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
build-admin-ui:
|
||||||
|
name: Admin UI image
|
||||||
|
needs: version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v5
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ghcr.io/consentos/consentos-admin-ui
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},value=${{ needs.version.outputs.newStrict }}
|
||||||
|
type=semver,pattern={{major}}.{{minor}},value=${{ needs.version.outputs.newStrict }}
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
# Context is the repo root — the admin-ui Dockerfile pulls in
|
||||||
|
# apps/banner/ alongside apps/admin-ui/ and bundles the banner
|
||||||
|
# output at the nginx root.
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: apps/admin-ui/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -5,8 +5,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-03-18
|
## [0.1.0] - 2026-03-18
|
||||||
|
|
||||||
Initial public release of ConsentOS.
|
Initial public release of ConsentOS.
|
||||||
|
|||||||
Reference in New Issue
Block a user