From 96469c0212567545c53fad2e00089722b06bcf89 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 27 Oct 2025 15:37:58 +0100 Subject: [PATCH] add changelog tooling --- docs/modules/ROOT/nav.adoc | 3 +- .../ROOT/pages/web-portal-changelog.adoc | 20 +++ hack/README.md | 116 ++++++++++++++ hack/bumpver-post-commit-hook.sh | 141 ++++++++++++++++++ hack/bumpver-pre-commit-hook.sh | 134 +++++++++++++++++ pyproject.toml | 4 +- 6 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 docs/modules/ROOT/pages/web-portal-changelog.adoc create mode 100644 hack/README.md create mode 100755 hack/bumpver-post-commit-hook.sh create mode 100755 hack/bumpver-pre-commit-hook.sh diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index cff0b83..d133fa2 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -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[] \ No newline at end of file +** xref:exoscale-osb.adoc[] diff --git a/docs/modules/ROOT/pages/web-portal-changelog.adoc b/docs/modules/ROOT/pages/web-portal-changelog.adoc new file mode 100644 index 0000000..bf2d3aa --- /dev/null +++ b/docs/modules/ROOT/pages/web-portal-changelog.adoc @@ -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]) + diff --git a/hack/README.md b/hack/README.md new file mode 100644 index 0000000..6626efc --- /dev/null +++ b/hack/README.md @@ -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 +``` diff --git a/hack/bumpver-post-commit-hook.sh b/hack/bumpver-post-commit-hook.sh new file mode 100755 index 0000000..ca175bd --- /dev/null +++ b/hack/bumpver-post-commit-hook.sh @@ -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 diff --git a/hack/bumpver-pre-commit-hook.sh b/hack/bumpver-pre-commit-hook.sh new file mode 100755 index 0000000..89ce3c4 --- /dev/null +++ b/hack/bumpver-pre-commit-hook.sh @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 60b51da..f10e50d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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