mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
Revert release branch flow
This commit is contained in:
377
.github/RELEASE_WORKFLOW.md
vendored
377
.github/RELEASE_WORKFLOW.md
vendored
@@ -1,377 +0,0 @@
|
||||
# Release Workflow
|
||||
|
||||
This document describes the branch strategy, CI/CD pipelines, and release process for Booklore.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
```
|
||||
master Stable releases only. Tagged with vX.Y.Z
|
||||
↑
|
||||
release/X.Y Release stabilization. RC builds published here
|
||||
↑
|
||||
develop Active development. All feature PRs merge here
|
||||
```
|
||||
|
||||
| Branch | Purpose | Stability |
|
||||
|--------|---------|-----------|
|
||||
| `master` | Production releases | Stable |
|
||||
| `release/X.Y` | Release candidates, bug fixes | Stabilizing |
|
||||
| `develop` | New features, active development | Unstable |
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
### 1. Develop Pipeline (`develop-pipeline.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push to `develop`
|
||||
- PRs targeting `develop`, `release/**`, or `master`
|
||||
|
||||
**Actions:**
|
||||
- Flyway migration check
|
||||
- Backend tests (Gradle)
|
||||
- Frontend tests (Angular/Vitest)
|
||||
- Docker build and push (for pushes and internal PRs)
|
||||
|
||||
**Image Tags:** `develop-{sha}`, `pr-{number}-{sha}`
|
||||
|
||||
---
|
||||
|
||||
### 2. Release Pipeline (`release-pipeline.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push to `release/**` branches
|
||||
- PRs targeting `release/**` branches
|
||||
|
||||
**Actions:**
|
||||
- Flyway migration check (against `master`)
|
||||
- Backend and frontend tests
|
||||
- Docker build and push with RC tags
|
||||
|
||||
**Image Tags:** `v1.18.0-rc.1`, `v1.18.0-rc.2`, `release-1.18-{sha}`
|
||||
|
||||
**Validations:**
|
||||
- Branch name must be `release/X.Y` or `release/X.Y.Z`
|
||||
- Version must be newer than latest tag
|
||||
- Rejects suspicious version jumps (typo protection)
|
||||
|
||||
---
|
||||
|
||||
### 3. Master Tag Pipeline (`master-pipeline.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push of tags matching `v*`
|
||||
|
||||
**Actions:**
|
||||
- Tag format validation
|
||||
- Flyway migration check
|
||||
- Backend and frontend tests
|
||||
- Docker build and push
|
||||
- GitHub Release publication (publishes the draft release)
|
||||
|
||||
**Image Tags:** `v1.18.0`, `latest` (stable releases only)
|
||||
|
||||
**Validations:**
|
||||
- Tag must be valid semver (`vX.Y.Z` or `vX.Y.Z-prerelease.N`)
|
||||
- Version must be newer than latest stable tag
|
||||
- Rejects suspicious version jumps (typo protection)
|
||||
- Blocks prereleases if stable version already exists
|
||||
|
||||
---
|
||||
|
||||
### 4. Draft Release Pipeline (`draft-release.yml`)
|
||||
|
||||
**Triggers:**
|
||||
- Push to `master`
|
||||
|
||||
**Actions:**
|
||||
- Updates draft GitHub Release with changelog from merged PRs
|
||||
- Categorizes changes based on PR labels (feature, bug, enhancement, etc.)
|
||||
- Auto-resolves next version based on labels (major/minor/patch)
|
||||
|
||||
**Configuration:** `.github/release-drafter.yml`
|
||||
|
||||
**How it works:**
|
||||
1. Each PR merged to `master` triggers this workflow
|
||||
2. Release-drafter accumulates changes into a draft release
|
||||
3. When you push a `v*` tag, the master-pipeline publishes the draft
|
||||
|
||||
**Label → Version mapping:**
|
||||
| Label | Version Bump |
|
||||
|-------|--------------|
|
||||
| `major` | X.0.0 |
|
||||
| `minor` | 0.X.0 |
|
||||
| `patch` | 0.0.X |
|
||||
|
||||
**Label → Changelog category:**
|
||||
| Labels | Category |
|
||||
|--------|----------|
|
||||
| `feature` | New Features |
|
||||
| `enhancement` | Enhancements |
|
||||
| `bug`, `fix` | Bug Fixes |
|
||||
| `refactor`, `cleanup`, `chore` | Refactoring & Maintenance |
|
||||
| `dependencies`, `deps` | Dependencies |
|
||||
| `ci`, `cd`, `workflow` | CI/CD |
|
||||
| `docs`, `documentation` | Documentation |
|
||||
|
||||
PRs with the `skip changelog` label are excluded from release notes.
|
||||
|
||||
---
|
||||
|
||||
## Release Flow Diagram
|
||||
|
||||
```
|
||||
v1.17.0 (current)
|
||||
│
|
||||
develop ────●────●────●────●─────────────────●──▶ (continues)
|
||||
│ ↑
|
||||
│ (1) cut branch │ (5) merge back
|
||||
▼ │
|
||||
release/1.18 ──●──●──●────────────────┼──▶ (delete)
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ │
|
||||
rc.1 rc.2 rc.3 │
|
||||
│
|
||||
master ──────────────────────────────────────●──▶
|
||||
│
|
||||
(4) tag v1.18.0
|
||||
│
|
||||
▼
|
||||
Docker: v1.18.0 + latest
|
||||
GitHub Release created
|
||||
```
|
||||
|
||||
## Step-by-Step Release Process
|
||||
|
||||
### Phase 1: Cut Release Branch
|
||||
|
||||
When `develop` is feature-complete for the release:
|
||||
|
||||
```bash
|
||||
git checkout develop && git pull
|
||||
git checkout -b release/1.18
|
||||
git push -u origin release/1.18
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- Release pipeline runs tests
|
||||
- Publishes `v1.18.0-rc.1` to Docker Hub and GHCR
|
||||
- RC number increments with each subsequent push
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Stabilize (Optional)
|
||||
|
||||
If bugs are found during RC testing, fix them on the release branch:
|
||||
|
||||
```bash
|
||||
git checkout release/1.18
|
||||
git checkout -b fix/critical-bug
|
||||
# ... make fixes ...
|
||||
git commit -m "Fix critical bug in authentication"
|
||||
git push -u origin fix/critical-bug
|
||||
|
||||
# Create PR targeting the release branch
|
||||
gh pr create --base release/1.18 --title "Fix critical bug"
|
||||
gh pr merge --squash
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- Each merge to `release/1.18` publishes a new RC (`v1.18.0-rc.2`, `rc.3`, etc.)
|
||||
- Testers can pull specific RC versions to validate fixes
|
||||
|
||||
**Recommended: Keep `develop` in sync**
|
||||
|
||||
To avoid `develop` having stale/buggy code until the release completes, create PRs targeting both branches:
|
||||
|
||||
```bash
|
||||
git checkout release/1.18
|
||||
git checkout -b fix/critical-bug
|
||||
# ... make fixes ...
|
||||
git commit -m "Fix critical bug in authentication"
|
||||
git push -u origin fix/critical-bug
|
||||
|
||||
# Create PR for release branch
|
||||
gh pr create --base release/1.18 --title "Fix critical bug"
|
||||
|
||||
# Create PR for develop (same branch, different target)
|
||||
gh pr create --base develop --title "Fix critical bug"
|
||||
```
|
||||
|
||||
Both PRs can be reviewed and merged independently. When `master` is merged back to `develop` in Phase 5, Git will recognize the identical changes and resolve cleanly.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Merge to Master
|
||||
|
||||
When the RC is validated and stable:
|
||||
|
||||
```bash
|
||||
# Create PR from release branch to master
|
||||
gh pr create --base master --head release/1.18 --title "Release v1.18.0"
|
||||
|
||||
# After approval, merge (use merge commit, not squash)
|
||||
gh pr merge --merge
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- Develop pipeline runs tests on the PR
|
||||
- Merge commit lands on `master`
|
||||
- No release is published yet (waiting for tag)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Tag and Release
|
||||
|
||||
Create the release tag to trigger the final release:
|
||||
|
||||
```bash
|
||||
git checkout master && git pull
|
||||
git tag v1.18.0
|
||||
git push origin v1.18.0
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
- Master Tag pipeline validates the tag
|
||||
- Runs full test suite
|
||||
- Builds and pushes Docker images: `v1.18.0` + `latest`
|
||||
- Publishes the draft GitHub Release (created by `draft-release.yml`) with changelog
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Post-Release Cleanup
|
||||
|
||||
Sync the release back to `develop` and clean up:
|
||||
|
||||
```bash
|
||||
# Merge master back to develop (includes any RC fixes)
|
||||
git checkout develop && git pull
|
||||
git merge master
|
||||
git push origin develop
|
||||
|
||||
# Delete the release branch
|
||||
git push origin --delete release/1.18
|
||||
git branch -d release/1.18
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
### New Minor Release (v1.17.0 → v1.18.0)
|
||||
|
||||
```bash
|
||||
# Cut release
|
||||
git checkout develop && git pull
|
||||
git checkout -b release/1.18
|
||||
git push -u origin release/1.18
|
||||
|
||||
# When stable, merge to master
|
||||
gh pr create --base master --head release/1.18 --title "Release v1.18.0"
|
||||
gh pr merge --merge
|
||||
|
||||
# Tag release
|
||||
git checkout master && git pull
|
||||
git tag v1.18.0
|
||||
git push origin v1.18.0
|
||||
|
||||
# Cleanup
|
||||
git checkout develop && git pull && git merge master && git push
|
||||
git push origin --delete release/1.18
|
||||
git branch -d release/1.18
|
||||
```
|
||||
|
||||
### New Patch Release (v1.18.0 → v1.18.1)
|
||||
|
||||
```bash
|
||||
# Cut release from master (or existing release branch)
|
||||
git checkout master && git pull
|
||||
git checkout -b release/1.18
|
||||
git push -u origin release/1.18
|
||||
|
||||
# Cherry-pick or fix bugs, then merge and tag
|
||||
gh pr create --base master --head release/1.18 --title "Release v1.18.1"
|
||||
gh pr merge --merge
|
||||
git checkout master && git pull
|
||||
git tag v1.18.1
|
||||
git push origin v1.18.1
|
||||
|
||||
# Cleanup
|
||||
git checkout develop && git pull && git merge master && git push
|
||||
git push origin --delete release/1.18
|
||||
```
|
||||
|
||||
### Hotfix (urgent production fix)
|
||||
|
||||
```bash
|
||||
# Create hotfix branch from master
|
||||
git checkout master && git pull
|
||||
git checkout -b hotfix/1.18.1
|
||||
# ... make urgent fix ...
|
||||
git commit -m "Fix critical security issue"
|
||||
git push -u origin hotfix/1.18.1
|
||||
|
||||
# Merge to master and tag
|
||||
gh pr create --base master --head hotfix/1.18.1 --title "Hotfix v1.18.1"
|
||||
gh pr merge --merge
|
||||
git checkout master && git pull
|
||||
git tag v1.18.1
|
||||
git push origin v1.18.1
|
||||
|
||||
# Merge fix to develop
|
||||
git checkout develop && git pull && git merge master && git push
|
||||
git push origin --delete hotfix/1.18.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Version Validation Rules
|
||||
|
||||
The pipelines enforce these rules to prevent mistakes:
|
||||
|
||||
| Rule | Example | Result |
|
||||
|------|---------|--------|
|
||||
| No backwards versions | `v1.17.0` after `v1.18.0` | Blocked |
|
||||
| No duplicate stable versions | `v1.18.0` after `v1.18.0` | Blocked |
|
||||
| No prerelease after stable | `v1.18.0-rc.1` after `v1.18.0` | Blocked |
|
||||
| Major jump ≤ 1 | `v3.0.0` after `v1.18.0` | Blocked |
|
||||
| Minor jump ≤ 5 | `v1.25.0` after `v1.18.0` | Blocked |
|
||||
| Patch jump ≤ 10 | `v1.18.15` after `v1.18.0` | Blocked |
|
||||
|
||||
These rules catch typos like `release/1.118` instead of `release/1.18`.
|
||||
|
||||
---
|
||||
|
||||
## Docker Image Tags
|
||||
|
||||
| Event | Tags Published |
|
||||
|-------|---------------|
|
||||
| Push to `develop` | `develop-abc1234` |
|
||||
| PR #42 | `pr-42-abc1234` |
|
||||
| Push to `release/1.18` | `v1.18.0-rc.1`, `release-1.18-abc1234` |
|
||||
| Tag `v1.18.0` | `v1.18.0`, `latest` |
|
||||
| Tag `v1.18.1-rc.1` | `v1.18.1-rc.1` (no `latest`) |
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Can I have multiple release branches simultaneously?**
|
||||
A: Yes. `release/1.18` and `release/1.19` can coexist. Each gets independent RC numbers.
|
||||
|
||||
**Q: What if I need to release a patch for an older version?**
|
||||
A: Create `release/1.17` from the `v1.17.0` tag, make fixes, and tag `v1.17.1`.
|
||||
|
||||
**Q: Why use merge commits instead of squash for release PRs?**
|
||||
A: Merge commits preserve the full history of RC fixes, making it easier to track what changed.
|
||||
|
||||
**Q: Can I skip the RC phase for urgent fixes?**
|
||||
A: Yes. For hotfixes, you can merge directly to master and tag. The release pipeline is optional.
|
||||
|
||||
**Q: What happens if I push a tag that fails validation?**
|
||||
A: The pipeline fails before publishing anything. Delete the tag (`git push --delete origin v1.18.0`), fix the issue, and re-tag.
|
||||
|
||||
**Q: How do I preview release notes before publishing?**
|
||||
A: Go to GitHub Releases. The draft release is automatically updated as PRs merge to `master`. Review and edit the draft before tagging.
|
||||
|
||||
**Q: Why don't I see changes in the draft release?**
|
||||
A: Ensure PRs have appropriate labels (`feature`, `bug`, `enhancement`, etc.). PRs without recognized labels still appear but won't be categorized. PRs with `skip changelog` label are excluded.
|
||||
8
.github/workflows/develop-pipeline.yml
vendored
8
.github/workflows/develop-pipeline.yml
vendored
@@ -6,17 +6,14 @@ on:
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- 'release/**'
|
||||
- master
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
migration-check:
|
||||
name: Flyway Migration Check
|
||||
uses: ./.github/workflows/migrations-check.yml
|
||||
with:
|
||||
# For PRs, compare against target branch; for pushes, compare against develop
|
||||
base_ref: ${{ github.event_name == 'pull_request' && github.base_ref || 'develop' }}
|
||||
base_ref: 'develop'
|
||||
head_ref: 'HEAD'
|
||||
|
||||
backend-tests:
|
||||
@@ -209,4 +206,3 @@ jobs:
|
||||
cache-to: |
|
||||
type=gha,mode=max
|
||||
type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max
|
||||
|
||||
|
||||
242
.github/workflows/master-pipeline.yml
vendored
242
.github/workflows/master-pipeline.yml
vendored
@@ -1,126 +1,32 @@
|
||||
name: Master Tag – Build, Push, and Publish
|
||||
name: Master - Build, Tag, Push, and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
validate-tag:
|
||||
name: Validate Release Tag
|
||||
get-base-ref:
|
||||
name: Get Base Ref
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.parse.outputs.version }}
|
||||
is_prerelease: ${{ steps.parse.outputs.is_prerelease }}
|
||||
base_ref: ${{ steps.get_base.outputs.base_ref }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Parse and Validate Tag
|
||||
id: parse
|
||||
run: |
|
||||
tag="${{ github.ref_name }}"
|
||||
echo "Processing tag: $tag"
|
||||
|
||||
# Validate semver format (v1.2.3 or v1.2.3-rc.1, v1.2.3-beta.2, etc.)
|
||||
if ! echo "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$'; then
|
||||
echo "::error::Invalid tag format. Expected 'vX.Y.Z' or 'vX.Y.Z-prerelease.N', got '$tag'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if this is a prerelease (contains hyphen after version)
|
||||
if echo "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+-'; then
|
||||
is_prerelease="true"
|
||||
else
|
||||
is_prerelease="false"
|
||||
fi
|
||||
|
||||
# ----------------------------------------
|
||||
# Version sanity check against latest tag
|
||||
# ----------------------------------------
|
||||
# Get latest stable tag (excluding prereleases and current tag)
|
||||
latest_tag=$(git tag --list "v*" --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^${tag}$" | head -n 1)
|
||||
latest_tag=${latest_tag:-"v0.0.0"}
|
||||
echo "Latest stable tag: $latest_tag"
|
||||
|
||||
# Extract base version (strip prerelease suffix if present)
|
||||
base_version=$(echo "$tag" | sed 's/^v//' | sed 's/-.*//')
|
||||
|
||||
# Parse latest version
|
||||
latest_semver=$(echo "$latest_tag" | sed 's/^v//')
|
||||
latest_major=$(echo "$latest_semver" | cut -d. -f1)
|
||||
latest_minor=$(echo "$latest_semver" | cut -d. -f2)
|
||||
latest_patch=$(echo "$latest_semver" | cut -d. -f3)
|
||||
|
||||
# Parse target version
|
||||
target_major=$(echo "$base_version" | cut -d. -f1)
|
||||
target_minor=$(echo "$base_version" | cut -d. -f2)
|
||||
target_patch=$(echo "$base_version" | cut -d. -f3)
|
||||
|
||||
echo "Latest: v${latest_major}.${latest_minor}.${latest_patch}"
|
||||
echo "Target: v${target_major}.${target_minor}.${target_patch}"
|
||||
|
||||
# Check for backwards version (target < latest)
|
||||
if [ "$target_major" -lt "$latest_major" ]; then
|
||||
echo "::error::Target version v${base_version} is older than latest tag ${latest_tag}"
|
||||
exit 1
|
||||
elif [ "$target_major" -eq "$latest_major" ] && [ "$target_minor" -lt "$latest_minor" ]; then
|
||||
echo "::error::Target version v${base_version} is older than latest tag ${latest_tag}"
|
||||
exit 1
|
||||
elif [ "$target_major" -eq "$latest_major" ] && [ "$target_minor" -eq "$latest_minor" ] && [ "$target_patch" -lt "$latest_patch" ]; then
|
||||
echo "::error::Target version v${base_version} is older than latest tag ${latest_tag}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# For stable releases, ensure version is strictly newer
|
||||
if [ "$is_prerelease" = "false" ]; then
|
||||
if [ "$target_major" -eq "$latest_major" ] && [ "$target_minor" -eq "$latest_minor" ] && [ "$target_patch" -eq "$latest_patch" ]; then
|
||||
echo "::error::Stable release v${base_version} already exists as ${latest_tag}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# For prereleases, check if a stable version already exists for this base version
|
||||
if [ "$is_prerelease" = "true" ]; then
|
||||
existing_stable=$(git tag --list "v${base_version}" | head -n 1)
|
||||
if [ -n "$existing_stable" ]; then
|
||||
echo "::error::Cannot create prerelease ${tag} - stable version v${base_version} already exists"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for suspicious jumps (likely typos)
|
||||
major_jump=$((target_major - latest_major))
|
||||
minor_jump=$((target_minor - latest_minor))
|
||||
patch_jump=$((target_patch - latest_patch))
|
||||
|
||||
# Flag suspicious patterns
|
||||
if [ "$major_jump" -gt 1 ]; then
|
||||
echo "::error::Suspicious major version jump: ${latest_tag} → ${tag} (jumping ${major_jump} major versions). Typo?"
|
||||
exit 1
|
||||
elif [ "$major_jump" -eq 0 ] && [ "$minor_jump" -gt 5 ]; then
|
||||
echo "::error::Suspicious minor version jump: ${latest_tag} → ${tag} (jumping ${minor_jump} minor versions). Typo?"
|
||||
exit 1
|
||||
elif [ "$major_jump" -eq 0 ] && [ "$minor_jump" -eq 0 ] && [ "$patch_jump" -gt 10 ]; then
|
||||
echo "::error::Suspicious patch version jump: ${latest_tag} → ${tag} (jumping ${patch_jump} patch versions). Typo?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Version sanity check passed: ${latest_tag} → ${tag}"
|
||||
|
||||
echo "version=$tag" >> $GITHUB_OUTPUT
|
||||
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
|
||||
echo "Version: $tag, Prerelease: $is_prerelease"
|
||||
fetch-depth: 2
|
||||
- name: Get Base Ref
|
||||
id: get_base
|
||||
run: echo "base_ref=$(git rev-parse HEAD~1)" >> $GITHUB_OUTPUT
|
||||
|
||||
migration-check:
|
||||
name: Flyway Migration Check
|
||||
needs: [ validate-tag ]
|
||||
name: Flyway Migration Check on Master
|
||||
needs: [ get-base-ref ]
|
||||
uses: ./.github/workflows/migrations-check.yml
|
||||
with:
|
||||
base_ref: 'master~1'
|
||||
head_ref: 'master'
|
||||
base_ref: ${{ needs.get-base-ref.outputs.base_ref }}
|
||||
head_ref: ${{ github.sha }}
|
||||
|
||||
backend-tests:
|
||||
name: Backend Tests
|
||||
@@ -232,12 +138,13 @@ jobs:
|
||||
exit 1
|
||||
|
||||
build-and-release:
|
||||
needs: [ validate-tag, backend-tests, frontend-tests ]
|
||||
needs: [ backend-tests, frontend-tests ]
|
||||
if: needs.backend-tests.result == 'success' && needs.frontend-tests.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
@@ -264,34 +171,82 @@ jobs:
|
||||
- name: Set Up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Compute Docker Tags
|
||||
id: docker_tags
|
||||
- name: Retrieve Latest Master Version Tag
|
||||
id: get_version
|
||||
run: |
|
||||
version="${{ needs.validate-tag.outputs.version }}"
|
||||
is_prerelease="${{ needs.validate-tag.outputs.is_prerelease }}"
|
||||
latest_tag=$(git tag --list "v*" --sort=-v:refname | head -n 1)
|
||||
latest_tag=${latest_tag:-"v0.0.0"}
|
||||
echo "latest_tag=$latest_tag" >> $GITHUB_ENV
|
||||
echo "Latest master tag: $latest_tag"
|
||||
|
||||
# Always include the version tag
|
||||
tags="booklore/booklore:${version}"
|
||||
tags="${tags},ghcr.io/booklore-app/booklore:${version}"
|
||||
- name: Determine Version Bump
|
||||
id: determine_bump
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Determining version bump from PR labels..."
|
||||
|
||||
# Only add 'latest' tag for stable releases (not prereleases)
|
||||
if [ "$is_prerelease" = "false" ]; then
|
||||
tags="${tags},booklore/booklore:latest"
|
||||
tags="${tags},ghcr.io/booklore-app/booklore:latest"
|
||||
# Extract PR number from merge commit
|
||||
pr_number=$(git log -1 --pretty=%B | grep -oE 'Merge pull request #[0-9]+' | grep -oE '[0-9]+') || true
|
||||
if [ -z "$pr_number" ]; then
|
||||
pr_number=$(gh pr list --state merged --base master --limit 1 --json number --jq '.[0].number')
|
||||
fi
|
||||
echo "PR number: $pr_number"
|
||||
|
||||
labels=$(gh pr view "$pr_number" --json labels --jq '.labels[].name' || echo "")
|
||||
echo "PR labels: $labels"
|
||||
|
||||
if echo "$labels" | grep -q 'bump:major'; then
|
||||
bump="major"
|
||||
elif echo "$labels" | grep -q 'bump:minor'; then
|
||||
bump="minor"
|
||||
elif echo "$labels" | grep -q 'bump:patch'; then
|
||||
bump="patch"
|
||||
else
|
||||
last_commit_msg=$(git log -1 --pretty=%B)
|
||||
if echo "$last_commit_msg" | grep -iq '#major'; then
|
||||
bump="major"
|
||||
elif echo "$last_commit_msg" | grep -iq '#minor'; then
|
||||
bump="minor"
|
||||
elif echo "$last_commit_msg" | grep -iq '#patch'; then
|
||||
bump="patch"
|
||||
else
|
||||
bump="patch"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "tags=$tags" >> $GITHUB_OUTPUT
|
||||
echo "Docker tags: $tags"
|
||||
# Calculate next version
|
||||
semver=$(echo ${{ env.latest_tag }} | sed 's/^v//')
|
||||
major=$(echo $semver | cut -d. -f1)
|
||||
minor=$(echo $semver | cut -d. -f2)
|
||||
patch=$(echo $semver | cut -d. -f3)
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
case "$bump" in
|
||||
major) major=$((major+1)); minor=0; patch=0 ;;
|
||||
minor) minor=$((minor+1)); patch=0 ;;
|
||||
patch) patch=$((patch+1)) ;;
|
||||
esac
|
||||
|
||||
next_version="v${major}.${minor}.${patch}"
|
||||
echo "Version bump type: $bump"
|
||||
echo "Next version: $next_version"
|
||||
|
||||
echo "bump=$bump" >> $GITHUB_ENV
|
||||
echo "new_tag=$next_version" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_tags.outputs.tags }}
|
||||
tags: |
|
||||
booklore/booklore:${{ env.new_tag }}
|
||||
booklore/booklore:latest
|
||||
ghcr.io/booklore-app/booklore:${{ env.new_tag }}
|
||||
ghcr.io/booklore-app/booklore:latest
|
||||
build-args: |
|
||||
APP_VERSION=${{ needs.validate-tag.outputs.version }}
|
||||
APP_VERSION=${{ env.new_tag }}
|
||||
APP_REVISION=${{ github.sha }}
|
||||
cache-from: |
|
||||
type=gha
|
||||
@@ -300,39 +255,16 @@ jobs:
|
||||
type=gha,mode=max
|
||||
type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max
|
||||
|
||||
- name: Create GitHub Release
|
||||
- name: Update GitHub Release Draft
|
||||
uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
tag: ${{ needs.validate-tag.outputs.version }}
|
||||
name: "Release ${{ needs.validate-tag.outputs.version }}"
|
||||
prerelease: ${{ needs.validate-tag.outputs.is_prerelease }}
|
||||
publish: true
|
||||
tag: ${{ env.new_tag }}
|
||||
name: "Release ${{ env.new_tag }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Release Summary
|
||||
- name: Publish GitHub Draft Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
version="${{ needs.validate-tag.outputs.version }}"
|
||||
is_prerelease="${{ needs.validate-tag.outputs.is_prerelease }}"
|
||||
|
||||
echo "## Release Published" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** \`${version}\`" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$is_prerelease" = "true" ]; then
|
||||
echo "**Type:** Prerelease" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "**Type:** Stable Release" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Docker Images:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`booklore/booklore:${version}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`ghcr.io/booklore-app/booklore:${version}\`" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$is_prerelease" = "false" ]; then
|
||||
echo "- \`booklore/booklore:latest\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`ghcr.io/booklore-app/booklore:latest\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Install:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull booklore/booklore:${version}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
gh release edit ${{ env.new_tag }} --draft=true
|
||||
|
||||
3
.github/workflows/migrations-check.yml
vendored
3
.github/workflows/migrations-check.yml
vendored
@@ -32,8 +32,7 @@ jobs:
|
||||
id: check_migrations
|
||||
run: |
|
||||
echo "Comparing ${{ inputs.base_ref }}...${{ inputs.head_ref }}"
|
||||
# Use explicit regex: V followed by digits, then double underscore, then any name, ending in .sql
|
||||
if git diff --name-only ${{ inputs.base_ref }}...${{ inputs.head_ref }} | grep -qE "booklore-api/src/main/resources/db/migration/V[0-9]+__.*\.sql"; then
|
||||
if git diff --name-only ${{ inputs.base_ref }}...${{ inputs.head_ref }} | grep -q "booklore-api/src/main/resources/db/migration/V.*.sql"; then
|
||||
echo "Migration file changes detected. Proceeding with migration preview."
|
||||
echo "has_migrations=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
|
||||
320
.github/workflows/release-pipeline.yml
vendored
320
.github/workflows/release-pipeline.yml
vendored
@@ -1,320 +0,0 @@
|
||||
name: Release Branch – Build, Test, and RC Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'release/**'
|
||||
|
||||
jobs:
|
||||
migration-check:
|
||||
name: Flyway Migration Check
|
||||
uses: ./.github/workflows/migrations-check.yml
|
||||
with:
|
||||
base_ref: 'master'
|
||||
head_ref: 'HEAD'
|
||||
|
||||
backend-tests:
|
||||
name: Backend Tests
|
||||
needs: [ migration-check ]
|
||||
if: needs.migration-check.result == 'success' || needs.migration-check.result == 'skipped'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set Up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
cache: gradle
|
||||
|
||||
- name: Execute Backend Tests
|
||||
id: backend_tests
|
||||
working-directory: ./booklore-api
|
||||
run: |
|
||||
echo "Running backend tests..."
|
||||
./gradlew test --no-daemon --parallel --build-cache
|
||||
continue-on-error: true
|
||||
|
||||
- name: Publish Backend Test Results
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
files: booklore-api/build/test-results/**/*.xml
|
||||
check_name: Backend Test Results
|
||||
|
||||
- name: Upload Backend Test Reports
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: backend-test-reports
|
||||
path: |
|
||||
booklore-api/build/reports/tests/
|
||||
booklore-api/build/test-results/
|
||||
retention-days: 30
|
||||
|
||||
- name: Validate Backend Test Results
|
||||
if: steps.backend_tests.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Backend tests failed"
|
||||
exit 1
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend Tests
|
||||
needs: [ migration-check ]
|
||||
if: needs.migration-check.result == 'success' || needs.migration-check.result == 'skipped'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set Up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: booklore-ui/package-lock.json
|
||||
|
||||
- name: Install Frontend Dependencies
|
||||
working-directory: ./booklore-ui
|
||||
run: npm ci --force
|
||||
|
||||
- name: Execute Frontend Tests
|
||||
id: frontend_tests
|
||||
working-directory: ./booklore-ui
|
||||
run: |
|
||||
echo "Running frontend tests..."
|
||||
npx ng test
|
||||
continue-on-error: true
|
||||
|
||||
- name: Publish Frontend Test Results
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
files: booklore-ui/test-results/vitest-results.xml
|
||||
check_name: Frontend Test Results
|
||||
|
||||
- name: Upload Frontend Test Reports
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: frontend-test-reports
|
||||
path: |
|
||||
booklore-ui/test-results/vitest-results.xml
|
||||
retention-days: 30
|
||||
|
||||
- name: Validate Frontend Test Results
|
||||
if: steps.frontend_tests.outcome == 'failure'
|
||||
run: |
|
||||
echo "❌ Frontend tests failed"
|
||||
exit 1
|
||||
|
||||
build-and-push-rc:
|
||||
name: Build and Push RC Container
|
||||
needs: [ backend-tests, frontend-tests ]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# ----------------------------------------
|
||||
# Environment setup
|
||||
# ----------------------------------------
|
||||
- name: Set Up QEMU for Multi-Arch Builds
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set Up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# ----------------------------------------
|
||||
# RC version tagging
|
||||
# ----------------------------------------
|
||||
- name: Generate RC Version Tag
|
||||
id: version
|
||||
run: |
|
||||
# Extract version from branch name (release/1.2 → 1.2)
|
||||
branch_name="${{ github.ref_name }}"
|
||||
branch_version=$(echo "$branch_name" | sed 's|release/||')
|
||||
|
||||
# Validate version format (must be X.Y or X.Y.Z)
|
||||
if ! echo "$branch_version" | grep -qE '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
|
||||
echo "::error::Invalid release branch format. Expected 'release/X.Y' or 'release/X.Y.Z', got '$branch_name'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalize to X.Y.Z format (append .0 if only X.Y)
|
||||
if echo "$branch_version" | grep -qE '^[0-9]+\.[0-9]+$'; then
|
||||
full_version="${branch_version}.0"
|
||||
else
|
||||
full_version="$branch_version"
|
||||
fi
|
||||
|
||||
# ----------------------------------------
|
||||
# Version sanity check against latest tag
|
||||
# ----------------------------------------
|
||||
latest_tag=$(git tag --list "v*" --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
|
||||
latest_tag=${latest_tag:-"v0.0.0"}
|
||||
echo "Latest stable tag: $latest_tag"
|
||||
|
||||
# Parse latest version
|
||||
latest_semver=$(echo "$latest_tag" | sed 's/^v//')
|
||||
latest_major=$(echo "$latest_semver" | cut -d. -f1)
|
||||
latest_minor=$(echo "$latest_semver" | cut -d. -f2)
|
||||
latest_patch=$(echo "$latest_semver" | cut -d. -f3)
|
||||
|
||||
# Parse target version
|
||||
target_major=$(echo "$full_version" | cut -d. -f1)
|
||||
target_minor=$(echo "$full_version" | cut -d. -f2)
|
||||
target_patch=$(echo "$full_version" | cut -d. -f3)
|
||||
|
||||
echo "Latest: v${latest_major}.${latest_minor}.${latest_patch}"
|
||||
echo "Target: v${target_major}.${target_minor}.${target_patch}"
|
||||
|
||||
# Check for backwards version (target < latest)
|
||||
if [ "$target_major" -lt "$latest_major" ]; then
|
||||
echo "::error::Target version v${full_version} is older than latest tag ${latest_tag}"
|
||||
exit 1
|
||||
elif [ "$target_major" -eq "$latest_major" ] && [ "$target_minor" -lt "$latest_minor" ]; then
|
||||
echo "::error::Target version v${full_version} is older than latest tag ${latest_tag}"
|
||||
exit 1
|
||||
elif [ "$target_major" -eq "$latest_major" ] && [ "$target_minor" -eq "$latest_minor" ] && [ "$target_patch" -lt "$latest_patch" ]; then
|
||||
echo "::error::Target version v${full_version} is older than latest tag ${latest_tag}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for suspicious jumps (likely typos)
|
||||
major_jump=$((target_major - latest_major))
|
||||
minor_jump=$((target_minor - latest_minor))
|
||||
patch_jump=$((target_patch - latest_patch))
|
||||
|
||||
# Flag suspicious patterns
|
||||
if [ "$major_jump" -gt 1 ]; then
|
||||
echo "::error::Suspicious major version jump: ${latest_tag} → v${full_version} (jumping ${major_jump} major versions). Typo?"
|
||||
exit 1
|
||||
elif [ "$major_jump" -eq 0 ] && [ "$minor_jump" -gt 5 ]; then
|
||||
echo "::error::Suspicious minor version jump: ${latest_tag} → v${full_version} (jumping ${minor_jump} minor versions). Typo?"
|
||||
exit 1
|
||||
elif [ "$major_jump" -eq 0 ] && [ "$minor_jump" -eq 0 ] && [ "$patch_jump" -gt 10 ]; then
|
||||
echo "::error::Suspicious patch version jump: ${latest_tag} → v${full_version} (jumping ${patch_jump} patch versions). Typo?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Warn (but don't fail) for moderate jumps
|
||||
if [ "$minor_jump" -gt 1 ] && [ "$minor_jump" -le 5 ]; then
|
||||
echo "::warning::Skipping minor versions: ${latest_tag} → v${full_version}. Is this intentional?"
|
||||
fi
|
||||
|
||||
echo "✅ Version sanity check passed: ${latest_tag} → v${full_version}"
|
||||
|
||||
# ----------------------------------------
|
||||
# Calculate RC number
|
||||
# ----------------------------------------
|
||||
# Find the merge-base with develop to count commits accurately
|
||||
# This ensures RC numbers are stable even if develop moves forward
|
||||
if git rev-parse origin/develop >/dev/null 2>&1; then
|
||||
merge_base=$(git merge-base origin/develop HEAD 2>/dev/null || echo "")
|
||||
if [ -n "$merge_base" ]; then
|
||||
rc_number=$(git rev-list --count ${merge_base}..HEAD 2>/dev/null || echo "1")
|
||||
else
|
||||
rc_number=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
fi
|
||||
else
|
||||
rc_number=$(git rev-list --count HEAD 2>/dev/null || echo "1")
|
||||
fi
|
||||
|
||||
# Convert commit count to RC number (0 commits = rc.1, 1 commit = rc.2, etc.)
|
||||
rc_number=$((rc_number + 1))
|
||||
|
||||
rc_tag="v${full_version}-rc.${rc_number}"
|
||||
short_sha=$(git rev-parse --short HEAD)
|
||||
# Sanitize branch name for Docker tag (replace / with -)
|
||||
safe_branch_name=$(echo "$branch_name" | tr '/' '-')
|
||||
|
||||
echo "rc_tag=$rc_tag" >> $GITHUB_ENV
|
||||
echo "branch_version=$branch_version" >> $GITHUB_ENV
|
||||
echo "full_version=$full_version" >> $GITHUB_ENV
|
||||
echo "short_sha=$short_sha" >> $GITHUB_ENV
|
||||
echo "safe_branch_name=$safe_branch_name" >> $GITHUB_ENV
|
||||
|
||||
echo "Release branch: $branch_name"
|
||||
echo "Full version: $full_version"
|
||||
echo "RC tag: $rc_tag"
|
||||
|
||||
# ----------------------------------------
|
||||
# Docker login
|
||||
# ----------------------------------------
|
||||
- name: Authenticate to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Authenticate to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
# ----------------------------------------
|
||||
# Docker build & push
|
||||
# ----------------------------------------
|
||||
- name: Build and Push RC Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
booklore/booklore:${{ env.rc_tag }}
|
||||
booklore/booklore:${{ env.safe_branch_name }}-${{ env.short_sha }}
|
||||
ghcr.io/booklore-app/booklore:${{ env.rc_tag }}
|
||||
ghcr.io/booklore-app/booklore:${{ env.safe_branch_name }}-${{ env.short_sha }}
|
||||
build-args: |
|
||||
APP_VERSION=${{ env.rc_tag }}
|
||||
APP_REVISION=${{ github.sha }}
|
||||
cache-from: |
|
||||
type=gha
|
||||
type=registry,ref=ghcr.io/booklore-app/booklore:buildcache
|
||||
cache-to: |
|
||||
type=gha,mode=max
|
||||
type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## RC Build Published" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** \`${{ env.rc_tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Images:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`booklore/booklore:${{ env.rc_tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- \`ghcr.io/booklore-app/booklore:${{ env.rc_tag }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Test this RC:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||
echo "docker pull booklore/booklore:${{ env.rc_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
Reference in New Issue
Block a user