add changelog tooling
All checks were successful
Build and Deploy Staging / build (push) Successful in 44s
Build and Deploy Antora Docs / build (push) Successful in 36s
Tests / test (push) Successful in 27s
Build and Deploy Staging / deploy (push) Successful in 6s
Build and Deploy Antora Docs / deploy (push) Successful in 4s

This commit is contained in:
Tobias Brunner 2025-10-27 15:37:58 +01:00
parent 6ed6a8f4c3
commit 96469c0212
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
6 changed files with 415 additions and 3 deletions

View file

@ -5,6 +5,7 @@
** xref:web-portal-admin.adoc[Admin]
** xref:web-portal-controlplanes.adoc[Control-Planes]
** xref:web-portal-billingentity.adoc[Billing Entities]
** xref:web-portal-changelog.adoc[Changelog]
* xref:web-portal-planning.adoc[]
** xref:user-stories.adoc[]
@ -16,4 +17,4 @@
** xref:api.adoc[]
* Cloud Providers
** xref:exoscale-osb.adoc[]
** xref:exoscale-osb.adoc[]

View file

@ -0,0 +1,20 @@
= Portal Changelog
== 2025.10.27-0
=== UI/UX
* Restrict user input to more sensible ranges (link:https://servala.app.codey.ch/servala/servala-portal/pulls/251[#251])
* Inline user info in service offering page (link:https://servala.app.codey.ch/servala/servala-portal/pulls/250[#250])
=== bug
* Fix generated FQDN not being submitted (link:https://servala.app.codey.ch/servala/servala-portal/pulls/249[#249])
=== dependencies
* Update dependency isort to v7 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/252[#252])
* Update dependency pillow to v12 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/253[#253])
* Lock file maintenance (link:https://servala.app.codey.ch/servala/servala-portal/pulls/255[#255])
* Update https://github.com/astral-sh/setup-uv action to v7 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/254[#254])
* Update dependency flake8-bugbear to v25 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/248[#248])
* Update https://github.com/renovatebot/github-action action to v43.0.18 - autoclosed (link:https://servala.app.codey.ch/servala/servala-portal/pulls/239[#239])
* Update actions/setup-node action to v6 (link:https://servala.app.codey.ch/servala/servala-portal/pulls/247[#247])

116
hack/README.md Normal file
View file

@ -0,0 +1,116 @@
# Automation Scripts
This directory contains automation scripts for release management and changelog generation.
## Scripts
### bumpver-pre-commit-hook.sh
**Purpose**: Generates a changelog based on merged Pull Requests since the last release.
**What it does**:
1. Queries the Forgejo API for merged pull requests since the last release tag
2. Groups pull requests by their labels (first label if multiple labels are present)
3. Formats the changes in AsciiDoc format with third-level headers for each label group
4. Appends the changelog to `docs/modules/ROOT/pages/web-portal-changelog.adoc`
5. Adds the changelog file to git staging area
6. Saves the changelog content for the post-commit hook
**Note**: Pull requests without labels will be grouped under "Uncategorized".
**Requirements**:
- `FORGEJO_TOKEN` environment variable must be set with a valid Forgejo API token
- `jq` command-line JSON processor must be installed
- `curl` must be installed
### bumpver-post-commit-hook.sh
**Purpose**: Creates a release on Forgejo after a version bump.
**What it does**:
1. Gets the current version from `pyproject.toml`
2. Reads the changelog content generated by the pre-commit hook
3. Converts AsciiDoc format to Markdown (headers and links)
4. Creates or updates a release on Forgejo with the Markdown-formatted changelog
5. Cleans up temporary changelog files
**Note**: The script automatically converts AsciiDoc syntax to Markdown for Forgejo releases:
- `=== Header``### Header`
- `link:url[text]``[text](url)`
**Requirements**:
- `FORGEJO_TOKEN` environment variable must be set with a valid Forgejo API token
- `jq` command-line JSON processor must be installed
- `curl` must be installed
## Setup
### 1. Generate a Forgejo API Token
1. Log in to Forgejo at https://servala.app.codey.ch
2. Go to Settings → Applications → Generate New Token
3. Give it a descriptive name (e.g., "bumpver-automation")
4. Select the required permissions:
- `repo` (Full control of repositories)
5. Copy the generated token
### 2. Configure the token
Export the token as an environment variable:
```bash
export FORGEJO_TOKEN="your-token-here"
```
For permanent setup, add it to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
```bash
echo 'export FORGEJO_TOKEN="your-token-here"' >> ~/.bashrc
```
### 3. Update pyproject.toml
Update the bumpver configuration in `pyproject.toml` to use these hooks:
```toml
[tool.bumpver]
current_version = "2025.10.27-0"
version_pattern = "YYYY.0M.0D-INC0"
commit_message = "bump version {old_version} -> {new_version}"
tag_message = "{new_version}"
tag_scope = "default"
pre_commit_hook = "hack/bumpver-pre-commit-hook.sh"
post_commit_hook = "hack/bumpver-post-commit-hook.sh"
commit = true
tag = true
push = true
```
## Usage
Once configured, the hooks will run automatically when you bump the version:
```bash
# Or let bumpver determine the version based on the pattern
uvx bumpver update
```
The workflow is:
1. `bumpver` updates version files
2. **Pre-commit hook** runs: generates changelog, updates changelog file, stages changes
3. `bumpver` creates commit with version bump and changelog
4. `bumpver` creates git tag
5. **Post-commit hook** runs: creates Forgejo release
6. `bumpver` pushes commit and tags to remote
## Manual execution
You can also run the scripts manually:
```bash
# Generate changelog (run before committing)
./hack/pre-commit-hook.sh
# Create release (run after committing and tagging)
./hack/post-commit-hook.sh
```

141
hack/bumpver-post-commit-hook.sh Executable file
View file

@ -0,0 +1,141 @@
#!/bin/bash
set -euo pipefail
# Post-commit hook for bumpver to create a Forgejo release
# This script creates a release on Forgejo using the changelog generated in the pre-commit hook
# Configuration
FORGEJO_URL="https://servala.app.codey.ch"
REPO_OWNER="servala"
REPO_NAME="servala-portal"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check for required environment variable
if [ -z "${FORGEJO_TOKEN:-}" ]; then
echo -e "${RED}Error: FORGEJO_TOKEN environment variable is not set${NC}"
echo "Please set FORGEJO_TOKEN to your Forgejo API token"
exit 1
fi
# Get the current version from pyproject.toml
CURRENT_VERSION=$(grep -E 'current_version = ".*"' pyproject.toml | head -1 | sed -E 's/current_version = "(.*)"/\1/')
echo -e "${GREEN}Creating release for version: ${CURRENT_VERSION}${NC}"
# Get the latest tag (should match CURRENT_VERSION)
LATEST_TAG=$(git tag -l --sort=-v:refname | head -1)
if [ "$LATEST_TAG" != "$CURRENT_VERSION" ]; then
echo -e "${YELLOW}Warning: Latest tag (${LATEST_TAG}) doesn't match current version (${CURRENT_VERSION})${NC}"
echo -e "${YELLOW}Using current version: ${CURRENT_VERSION}${NC}"
fi
# Try to read the changelog generated by the pre-commit hook
CHANGELOG_DIR=".git/changelog"
CHANGELOG_FILE="${CHANGELOG_DIR}/${CURRENT_VERSION}.txt"
if [ -f "$CHANGELOG_FILE" ]; then
CHANGELOG_CONTENT=$(cat "$CHANGELOG_FILE")
echo -e "${GREEN}Found changelog content from pre-commit hook${NC}"
else
echo -e "${YELLOW}Warning: Changelog file not found at ${CHANGELOG_FILE}${NC}"
echo -e "${YELLOW}Generating changelog from tag information${NC}"
# Fallback: use git log to get commits since previous tag
PREVIOUS_TAG=$(git tag -l --sort=-v:refname | head -2 | tail -1)
if [ -n "$PREVIOUS_TAG" ]; then
CHANGELOG_CONTENT=$(git log --pretty=format:"* %s" "${PREVIOUS_TAG}..${LATEST_TAG}")
else
CHANGELOG_CONTENT="* Initial release"
fi
fi
# Convert AsciiDoc to Markdown
# 1. Convert === headers to ### (third-level headers)
# 2. Convert link:url[text] to [text](url)
CHANGELOG_MARKDOWN=$(echo "$CHANGELOG_CONTENT" | sed -E 's/^=== /### /g' | sed -E 's/link:([^[]+)\[([^]]+)\]/[\2](\1)/g')
# Create release body in Markdown format
RELEASE_BODY="## Release ${CURRENT_VERSION}
${CHANGELOG_MARKDOWN}
"
# Check if release already exists
API_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${CURRENT_VERSION}"
EXISTING_RELEASE=$(curl -s -H "Authorization: token ${FORGEJO_TOKEN}" "${API_URL}")
# Check if we got a release back (not an error)
if echo "$EXISTING_RELEASE" | jq -e '.id' > /dev/null 2>&1; then
echo -e "${YELLOW}Release ${CURRENT_VERSION} already exists${NC}"
RELEASE_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id')
echo -e "${GREEN}Updating existing release (ID: ${RELEASE_ID})${NC}"
# Update the existing release
UPDATE_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}"
RESPONSE=$(curl -s -X PATCH \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "${CURRENT_VERSION}" \
--arg name "Release ${CURRENT_VERSION}" \
--arg body "${RELEASE_BODY}" \
'{
tag_name: $tag,
name: $name,
body: $body
}')" \
"${UPDATE_URL}")
if echo "$RESPONSE" | jq -e '.id' > /dev/null 2>&1; then
RELEASE_URL=$(echo "$RESPONSE" | jq -r '.html_url')
echo -e "${GREEN}Release updated successfully!${NC}"
echo -e "${GREEN}Release URL: ${RELEASE_URL}${NC}"
else
echo -e "${RED}Error updating release${NC}"
echo "$RESPONSE" | jq .
exit 1
fi
else
echo -e "${GREEN}Creating new release${NC}"
# Create new release
CREATE_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases"
RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "${CURRENT_VERSION}" \
--arg name "Release ${CURRENT_VERSION}" \
--arg body "${RELEASE_BODY}" \
'{
tag_name: $tag,
name: $name,
body: $body,
draft: false,
prerelease: false
}')" \
"${CREATE_URL}")
if echo "$RESPONSE" | jq -e '.id' > /dev/null 2>&1; then
RELEASE_URL=$(echo "$RESPONSE" | jq -r '.html_url')
echo -e "${GREEN}Release created successfully!${NC}"
echo -e "${GREEN}Release URL: ${RELEASE_URL}${NC}"
else
echo -e "${RED}Error creating release${NC}"
echo "$RESPONSE" | jq .
exit 1
fi
fi
# Clean up the changelog file
if [ -f "$CHANGELOG_FILE" ]; then
rm -f "$CHANGELOG_FILE"
fi
exit 0

134
hack/bumpver-pre-commit-hook.sh Executable file
View file

@ -0,0 +1,134 @@
#!/bin/bash
set -euo pipefail
# Pre-commit hook for bumpver to generate changelog from Forgejo merge requests
# This script queries the Forgejo API for merged pull requests since the last release
# and appends them to the changelog file in AsciiDoc format
# Configuration
FORGEJO_URL="https://servala.app.codey.ch"
REPO_OWNER="servala"
REPO_NAME="servala-portal"
CHANGELOG_FILE="docs/modules/ROOT/pages/web-portal-changelog.adoc"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check for required environment variable
if [ -z "${FORGEJO_TOKEN:-}" ]; then
echo -e "${RED}Error: FORGEJO_TOKEN environment variable is not set${NC}"
echo "Please set FORGEJO_TOKEN to your Forgejo API token"
exit 1
fi
# Get the new version from pyproject.toml (bumpver updates this before running the hook)
NEW_VERSION=$(grep -E 'current_version = ".*"' pyproject.toml | head -1 | sed -E 's/current_version = "(.*)"/\1/')
echo -e "${GREEN}Generating changelog for version: ${NEW_VERSION}${NC}"
# Get the previous tag
PREVIOUS_TAG=$(git tag -l --sort=-v:refname | head -2 | tail -1)
if [ -z "$PREVIOUS_TAG" ]; then
echo -e "${YELLOW}Warning: No previous tag found, using all merge requests${NC}"
# Get date of first commit
SINCE_DATE=$(git log --reverse --format=%aI | head -1)
else
echo -e "${GREEN}Previous version: ${PREVIOUS_TAG}${NC}"
# Get the date of the previous tag
SINCE_DATE=$(git log -1 --format=%aI "${PREVIOUS_TAG}")
fi
echo -e "${GREEN}Fetching merge requests since ${SINCE_DATE}${NC}"
# Query Forgejo API for closed/merged pull requests
# Forgejo API returns pull requests sorted by updated time
API_URL="${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls?state=closed&sort=updated&limit=100"
RESPONSE=$(curl -s -H "Authorization: token ${FORGEJO_TOKEN}" "${API_URL}")
# Check if curl was successful
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to fetch pull requests from Forgejo API${NC}"
exit 1
fi
# Filter merged PRs since the previous tag and group by labels
# Using jq to parse JSON, filter by merge date, and group by labels
CHANGELOG_ENTRIES=$(echo "${RESPONSE}" | jq -r --arg since "${SINCE_DATE}" '
# Filter merged PRs since the last tag
[.[] | select(.merged_at != null and .merged_at > $since)] |
sort_by(.merged_at) | reverse |
# Group by primary label (first label if multiple, or "Uncategorized")
group_by(
if (.labels | length) > 0 then
.labels[0].name
else
"Uncategorized"
end
) |
# Format each group
map(
# Group header
"=== " + (
if .[0].labels | length > 0 then
.[0].labels[0].name
else
"Uncategorized"
end
) + "\n" +
# List items in this group
(map("* " + .title + " (link:" + .html_url + "[#" + (.number | tostring) + "])") | join("\n"))
) |
join("\n\n")
')
if [ -z "$CHANGELOG_ENTRIES" ]; then
echo -e "${YELLOW}Warning: No merged pull requests found since ${PREVIOUS_TAG}${NC}"
CHANGELOG_ENTRIES="=== Uncategorized\n\n* No changes recorded"
fi
# Create changelog section
CHANGELOG_SECTION="
== ${NEW_VERSION}
${CHANGELOG_ENTRIES}
"
# Check if changelog file exists
if [ ! -f "$CHANGELOG_FILE" ]; then
echo -e "${RED}Error: Changelog file ${CHANGELOG_FILE} not found${NC}"
exit 1
fi
# Create temporary file with new changelog entry at the top
TMP_FILE=$(mktemp)
{
# Keep the title
head -1 "$CHANGELOG_FILE"
# Add the new changelog section
echo "$CHANGELOG_SECTION"
# Add the rest of the file (skip the title)
tail -n +2 "$CHANGELOG_FILE"
} > "$TMP_FILE"
# Replace the original file
mv "$TMP_FILE" "$CHANGELOG_FILE"
chmod 0644 "$CHANGELOG_FILE"
# Add the changelog file to git
git add "$CHANGELOG_FILE"
echo -e "${GREEN}Changelog updated successfully${NC}"
echo -e "${GREEN}Added ${CHANGELOG_FILE} to git staging area${NC}"
# Save changelog for post-commit hook
CHANGELOG_DIR=".git/changelog"
mkdir -p "$CHANGELOG_DIR"
echo "$CHANGELOG_ENTRIES" > "${CHANGELOG_DIR}/${NEW_VERSION}.txt"
exit 0

View file

@ -66,8 +66,8 @@ version_pattern = "YYYY.0M.0D-INC0"
commit_message = "bump version {old_version} -> {new_version}"
tag_message = "{new_version}"
tag_scope = "default"
pre_commit_hook = ""
post_commit_hook = ""
pre_commit_hook = "hack/bumpver-pre-commit-hook.sh"
post_commit_hook = "hack/bumpver-post-commit-hook.sh"
commit = true
tag = true
push = true