From c72966bd9a2c8af9e5e1f8f835eb2deda82cf2d8 Mon Sep 17 00:00:00 2001 From: Peter Repukat Date: Wed, 3 Dec 2025 14:07:30 +0100 Subject: [PATCH] Update publish/release actions-workflows - Switch to trusted publishing on tag push - Add changelog gen based on changelog commits - Add pre-releases - Push to GitHub releases - Automate package version injection via git-tags This makes the version-info in .csproj files irrelevant Removes the need for github-actions to push automated commits Changelog(misc) --- .github/scripts/generate-changelog.sh | 140 ++++++++++++++++ .github/workflows/generate-changelog.yml | 43 +++++ .github/workflows/publish-nuget.yml | 111 ------------- .github/workflows/release.yml | 202 +++++++++++++++++++++++ .github/workflows/run-tests.yml | 7 +- .github/workflows/snapshots.yml | 168 +++++++++++++++++++ 6 files changed, 558 insertions(+), 113 deletions(-) create mode 100644 .github/scripts/generate-changelog.sh create mode 100644 .github/workflows/generate-changelog.yml delete mode 100644 .github/workflows/publish-nuget.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/snapshots.yml diff --git a/.github/scripts/generate-changelog.sh b/.github/scripts/generate-changelog.sh new file mode 100644 index 0000000..39fb65c --- /dev/null +++ b/.github/scripts/generate-changelog.sh @@ -0,0 +1,140 @@ +#!/bin/bash +set -euo pipefail + +# Usage: generate-changelog.sh [tag_or_range] +# If tag_or_range is empty, generates from latest tag to HEAD + +OUTPUT_FILE="$1" +TAG_OR_RANGE="${2:-}" + +mkdir -p "$(dirname "$OUTPUT_FILE")" + +if [[ -z "$TAG_OR_RANGE" ]]; then + LATEST_TAG=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "") + if [[ -z "$LATEST_TAG" ]]; then + LOG_RANGE="" + CONTEXT_MSG="All unreleased changes:" + VERSION_TITLE="Development Version" + else + LOG_RANGE="$LATEST_TAG..HEAD" + CONTEXT_MSG="Changes since $LATEST_TAG:" + VERSION_TITLE="Development Version" + fi +else + VERSION="$TAG_OR_RANGE" + PREVIOUS_TAG=$(git describe --tags --abbrev=0 --match "v*.*.*" "$TAG_OR_RANGE^" 2>/dev/null || echo "") + if [[ -z "$PREVIOUS_TAG" ]]; then + LOG_RANGE="$TAG_OR_RANGE" + CONTEXT_MSG="All changes in this release:" + else + LOG_RANGE="$PREVIOUS_TAG..$TAG_OR_RANGE" + CONTEXT_MSG="Changes since $PREVIOUS_TAG:" + fi + VERSION_TITLE="Version ${VERSION#v}" +fi + +mapfile -t COMMITS < <(git log --pretty=format:'%H' $LOG_RANGE) +FEATURES="" +FIXES="" +MISC="" + +for commit_hash in "${COMMITS[@]}"; do + commit_msg=$(git log -1 --pretty=format:'%s' "$commit_hash") + commit_body=$(git log -1 --pretty=format:'%b' "$commit_hash") + + changelog_type="" + + if echo "$commit_msg" | grep -iqE 'changelog[(: ]'; then + if echo "$commit_msg" | grep -iqE 'changelog\((feature|feat)\)'; then + changelog_type="feature" + elif echo "$commit_msg" | grep -iqE 'changelog\((fix)\)'; then + changelog_type="fix" + elif echo "$commit_msg" | grep -iqE 'changelog\((misc)\)'; then + changelog_type="misc" + fi + fi + + if [ -z "$changelog_type" ]; then + if echo "$commit_body" | grep -iqE 'changelog[(: ]'; then + if echo "$commit_body" | grep -iqE 'changelog\((feature|feat)\)'; then + changelog_type="feature" + elif echo "$commit_body" | grep -iqE 'changelog\((fix)\)'; then + changelog_type="fix" + elif echo "$commit_body" | grep -iqE 'changelog\((misc)\)'; then + changelog_type="misc" + fi + fi + fi + + if [ -z "$changelog_type" ]; then + if echo "$commit_msg" | grep -iqE '^(feat|feature)(\(.*\))?:'; then + changelog_type="feature" + commit_msg=$(echo "$commit_msg" | sed -E 's/^(feat|feature)(\(.*\))?:[[:space:]]*//') + elif echo "$commit_msg" | grep -iqE '^fix(\(.*\))?:'; then + changelog_type="fix" + commit_msg=$(echo "$commit_msg" | sed -E 's/^fix(\(.*\))?:[[:space:]]*//') + elif echo "$commit_msg" | grep -iqE '^(chore|docs|style|refactor|perf|test)(\(.*\))?:'; then + changelog_type="misc" + commit_msg=$(echo "$commit_msg" | sed -E 's/^(chore|docs|style|refactor|perf|test)(\(.*\))?:[[:space:]]*//') + fi + fi + + if [ -n "$changelog_type" ]; then + body_content=$(echo "$commit_body" | awk 'BEGIN{IGNORECASE=1} !/changelog[(: ]/ && NF') + entry="- $commit_msg" + if [ -n "$body_content" ]; then + entry=$(printf "%s\n%s" "$entry" "$(echo "$body_content" | sed 's/^/ /')") + fi + if [ "$changelog_type" = "feature" ]; then + FEATURES=$(printf "%s\n%s" "$FEATURES" "$entry") + elif [ "$changelog_type" = "fix" ]; then + FIXES=$(printf "%s\n%s" "$FIXES" "$entry") + else + MISC=$(printf "%s\n%s" "$MISC" "$entry") + fi + fi +done + +{ + echo "# Changelog for $VERSION_TITLE" + echo "" + if [[ -z "$TAG_OR_RANGE" ]]; then + echo "This page shows unreleased changes in the development version." + else + echo "Release Date: $(date +'%Y-%m-%d')" + fi + echo "" + echo "$CONTEXT_MSG" + echo "" + + if [[ -n "$FEATURES" ]]; then + echo "## ✨ New Features" + echo "" + echo "$FEATURES" + echo "" + fi + + if [[ -n "$FIXES" ]]; then + echo "## 🐛 Fixes" + echo "" + echo "$FIXES" + echo "" + fi + + if [[ -n "$MISC" ]]; then + echo "## 🔧 Miscellaneous" + echo "" + echo "$MISC" + echo "" + fi + + if [[ -z "$FEATURES" && -z "$FIXES" && -z "$MISC" ]]; then + if [[ -z "$TAG_OR_RANGE" ]]; then + echo "No changes yet." + else + echo "No changes." + fi + fi +} > "$OUTPUT_FILE" + +echo "Changelog generated: $OUTPUT_FILE" diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml new file mode 100644 index 0000000..3d87ba0 --- /dev/null +++ b/.github/workflows/generate-changelog.yml @@ -0,0 +1,43 @@ +name: Generate Changelog + +on: + workflow_call: + inputs: + mode: + required: true + type: string + description: 'Mode of operation: "release" or "snapshot"' + tag_name: + required: false + type: string + description: "Current tag name (required for release mode)" + outputs: + changelog: + description: "Generated changelog" + value: ${{ jobs.generate.outputs.changelog }} + +jobs: + generate: + runs-on: ubuntu-latest + outputs: + changelog: ${{ steps.generate_changelog.outputs.changelog }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate Changelog + id: generate_changelog + shell: bash + run: | + chmod +x .github/scripts/generate-changelog.sh + OUTFILE=changelog_output.md + if [[ "${{ inputs.mode }}" == "release" ]]; then + .github/scripts/generate-changelog.sh "$OUTFILE" "${{ inputs.tag_name }}" + else + .github/scripts/generate-changelog.sh "$OUTFILE" + fi + echo "changelog<> $GITHUB_OUTPUT + cat "$OUTFILE" >> $GITHUB_OUTPUT + echo "CHANGELOG_EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml deleted file mode 100644 index 2277cc3..0000000 --- a/.github/workflows/publish-nuget.yml +++ /dev/null @@ -1,111 +0,0 @@ -# .github/workflows/publish-nuget.yml - -name: Publish NuGet Packages - -# This workflow is triggered manually from the GitHub Actions tab -on: - workflow_dispatch: - inputs: - version_bump: - description: 'release version' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Run tests - run: dotnet test - - publish: - name: Build, Pack, and Publish - runs-on: ubuntu-latest - needs: test - env: - CORE_PROJECT_PATH: "src/Eftdb/Eftdb.csproj" - DESIGN_PROJECT_PATH: "src/Eftdb.Design/Eftdb.Design.csproj" - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.ADMIN_PAT }} - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Read, Bump, and Update Version - id: version_bumper - run: | - # Read the current version from the main .csproj file - CURRENT_VERSION=$(grep '' ${{ env.CORE_PROJECT_PATH }} | sed -n 's/.*\(.*\)<\/Version>.*/\1/p') - echo "Current version: $CURRENT_VERSION" - - # Split the version into parts - IFS='.' read -r -a version_parts <<< "$CURRENT_VERSION" - major=${version_parts[0]} - minor=${version_parts[1]} - patch=${version_parts[2]} - - # Increment the version based on the workflow input - case "${{ github.event.inputs.version_bump }}" in - major) - major=$((major + 1)); minor=0; patch=0 - ;; - minor) - minor=$((minor + 1)); patch=0 - ;; - patch) - patch=$((patch + 1)) - ;; - esac - - NEW_VERSION="$major.$minor.$patch" - echo "Bumping to new version: $NEW_VERSION" - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT - - # Update the tag in both .csproj files - sed -i "s|.*|$NEW_VERSION|g" ${{ env.CORE_PROJECT_PATH }} - sed -i "s|.*|$NEW_VERSION|g" ${{ env.DESIGN_PROJECT_PATH }} - - - name: Commit and Tag Version Bump - run: | - git config --global user.name "GitHub Actions" - git config --global user.email "actions@github.com" - git add ${{ env.CORE_PROJECT_PATH }} ${{ env.DESIGN_PROJECT_PATH }} - git commit -m "Bump version to ${{ steps.version_bumper.outputs.NEW_VERSION }}" - git push - git tag "v${{ steps.version_bumper.outputs.NEW_VERSION }}" - git push origin "v${{ steps.version_bumper.outputs.NEW_VERSION }}" - - - name: Build and Pack - run: | - # Build the entire solution - dotnet clean - dotnet build -c Release - - # Pack both projects - dotnet pack ${{ env.CORE_PROJECT_PATH }} -c Release --no-build -o ./packages - dotnet pack ${{ env.DESIGN_PROJECT_PATH }} -c Release --no-build -o ./packages - - - name: Publish to NuGet.org - run: | - dotnet nuget push "./packages/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source "https://api.nuget.org/v3/index.json" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e0c73d3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,202 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + id-token: write + +jobs: + test: + name: Run Tests + uses: ./.github/workflows/run-tests.yml + secrets: inherit + + build-and-pack: + name: Build and Pack NuGet Packages + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract version from tag + id: version + shell: bash + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + VERSION=${TAG_NAME#v} + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Tag: $TAG_NAME" + echo "Version: $VERSION" + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore + + - name: Pack Eftdb + run: | + dotnet pack src/Eftdb/Eftdb.csproj -c Release --no-build -o ./packages /p:PackageVersion=${{ steps.version.outputs.version }} + + - name: Pack Eftdb.Design + run: | + dotnet pack src/Eftdb.Design/Eftdb.Design.csproj -c Release --no-build -o ./packages /p:PackageVersion=${{ steps.version.outputs.version }} + + - name: Upload NuGet packages as artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages-release + path: ./packages/*.nupkg + + generate-changelog: + name: Generate Changelog + needs: build-and-pack + runs-on: ubuntu-latest + outputs: + changelog: ${{ steps.generate.outputs.changelog }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract tag name + id: tag + shell: bash + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + + - name: Generate Changelog + id: generate + shell: bash + run: | + chmod +x .github/scripts/generate-changelog.sh + OUTFILE=changelog_output.md + .github/scripts/generate-changelog.sh "$OUTFILE" "${{ steps.tag.outputs.tag_name }}" + echo "changelog<> $GITHUB_OUTPUT + cat "$OUTFILE" >> $GITHUB_OUTPUT + echo "CHANGELOG_EOF" >> $GITHUB_OUTPUT + + publish-to-nuget: + name: Publish to NuGet + needs: [build-and-pack, generate-changelog] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download NuGet packages + uses: actions/download-artifact@v4 + with: + name: nuget-packages-release + path: ./packages + + - name: Setup .NET SDK (for NuGet publish) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: NuGet login (OIDC → temp API key) + id: login + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} + + - name: Publish to NuGet.org + shell: bash + working-directory: ./packages + run: | + set -euo pipefail + for nupkg in *.nupkg; do + if [ ! -f "$nupkg" ]; then + echo "ERROR: No .nupkg files found" >&2 + exit 1 + fi + echo "Publishing NuGet package: $nupkg" + dotnet nuget push "$nupkg" \ + --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate + done + + create-release: + name: Create Release + needs: [build-and-pack, generate-changelog, publish-to-nuget] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Organize artifacts + shell: bash + run: | + set -euo pipefail + mkdir -p release_files + for dir in artifacts/*; do + if [ -d "$dir" ]; then + echo "Processing artifact: $(basename "$dir")" + for file in "$dir"/*; do + if [ -f "$file" ]; then + cp "$file" "release_files/$(basename "$file")" + fi + done + fi + done + ls -la release_files/ + + - name: Extract build info + id: build_info + shell: bash + run: | + echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT + echo "time=$(date +'%H%M')" >> $GITHUB_OUTPUT + echo "sha=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_OUTPUT + TAG_NAME=${GITHUB_REF#refs/tags/} + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + GIT_VERSION=$(git describe --tags --match "v[0-9]*.[0-9]*.[0-9]*" --always || echo "") + if [[ ! $GIT_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + COMMIT_COUNT=$(git rev-list --count HEAD) + GIT_VERSION="v0.0.0-${COMMIT_COUNT}-${GITHUB_SHA:0:7}" + fi + echo "version=$GIT_VERSION" >> $GITHUB_OUTPUT + echo "Version from git: $GIT_VERSION" + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.build_info.outputs.tag_name }} + name: "Release ${{ steps.build_info.outputs.version }}" + body: | + ## CmdScale.EntityFrameworkCore.TimescaleDB Release ${{ steps.build_info.outputs.version }} + + Release Date: ${{ steps.build_info.outputs.date }} ${{ steps.build_info.outputs.time }} + + ### Changes + ${{ needs.generate-changelog.outputs.changelog }} + files: release_files/* + prerelease: false + draft: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 98c4a84..253ae24 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,9 +1,12 @@ -name: Run Tests +name: Test -# This workflow runs on pushes and pull requests to the main branch on: push: + branches-ignore: [main] # handled by snapshots... + tags-ignore: + - "v*.*.*" # handled by release.yml pull_request: + workflow_call: jobs: test: diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml new file mode 100644 index 0000000..00d090b --- /dev/null +++ b/.github/workflows/snapshots.yml @@ -0,0 +1,168 @@ +name: Snapshot Build and Pre-Release + +on: + push: + branches: ["main"] + +permissions: + contents: write + +jobs: + test: + name: Run Tests + uses: ./.github/workflows/run-tests.yml + secrets: inherit + + version-from-git: + name: Version from git + needs: test + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Calculate version + id: version + shell: bash + run: | + GIT_VERSION=$(git describe --tags --match "v[0-9]*.[0-9]*.[0-9]*" --always || echo "") + if [[ ! $GIT_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + COMMIT_COUNT=$(git rev-list --count HEAD) + GIT_VERSION="v0.0.0-dev.${COMMIT_COUNT}.${GITHUB_SHA:0:7}" + else + GIT_VERSION=$(echo "$GIT_VERSION" | sed -E 's/^v//; s/-([0-9]+)-g([a-f0-9]+)$/-dev.\1.\2/') + fi + GIT_VERSION=${GIT_VERSION#v} + echo "version=$GIT_VERSION" >> $GITHUB_OUTPUT + echo "Version: $GIT_VERSION" + + build-and-pack: + name: Build and Pack NuGet Packages + needs: version-from-git + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore + + - name: Pack Eftdb + run: | + dotnet pack src/Eftdb/Eftdb.csproj -c Release --no-build -o ./packages /p:PackageVersion=${{ needs.version-from-git.outputs.version }} + + - name: Pack Eftdb.Design + run: | + dotnet pack src/Eftdb.Design/Eftdb.Design.csproj -c Release --no-build -o ./packages /p:PackageVersion=${{ needs.version-from-git.outputs.version }} + - name: Upload NuGet packages as artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages-snapshot + path: ./packages/*.nupkg + + generate-changelog: + name: Generate Changelog + runs-on: ubuntu-latest + needs: build-and-pack + outputs: + changelog: ${{ steps.generate.outputs.changelog }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate Changelog + id: generate + shell: bash + run: | + chmod +x .github/scripts/generate-changelog.sh + OUTFILE=changelog_output.md + .github/scripts/generate-changelog.sh "$OUTFILE" + echo "changelog<> $GITHUB_OUTPUT + cat "$OUTFILE" >> $GITHUB_OUTPUT + echo "CHANGELOG_EOF" >> $GITHUB_OUTPUT + + create-pre-release: + name: Create Pre-Release + needs: [version-from-git, build-and-pack, generate-changelog] + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/main') + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Organize artifacts + shell: bash + run: | + set -euo pipefail + mkdir -p release_files + for dir in artifacts/*; do + if [ -d "$dir" ]; then + echo "Processing artifact: $(basename "$dir")" + for file in "$dir"/*; do + if [ -f "$file" ]; then + cp "$file" "release_files/$(basename "$file")" + fi + done + fi + done + ls -la release_files/ + + - name: Extract build info + id: build_info + shell: bash + run: | + echo "date=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT + echo "time=$(date +'%H%M')" >> $GITHUB_OUTPUT + echo "sha=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_OUTPUT + GIT_VERSION=$(git describe --tags --match "v[0-9]*.[0-9]*.[0-9]*" --always || echo "") + if [[ ! $GIT_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + COMMIT_COUNT=$(git rev-list --count HEAD) + GIT_VERSION="v0.0.0-${COMMIT_COUNT}-${GITHUB_SHA:0:7}" + fi + echo "version=$GIT_VERSION" >> $GITHUB_OUTPUT + echo "Version from git: $GIT_VERSION" + + - name: Update Dev Snapshot Release + uses: andelf/nightly-release@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: dev-snapshot + name: "Dev Build ${{ steps.build_info.outputs.version }}" + prerelease: true + body: | + ## Automated dev-snapshot + + Version: ${{ steps.build_info.outputs.version }} + Date: ${{ steps.build_info.outputs.date }} ${{ steps.build_info.outputs.time }} + Commit: [${{ steps.build_info.outputs.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) + + > ⚠️ These are the latest development builds and may contain bugs or unfinished features. + + ### Changes + ${{ needs.generate-changelog.outputs.changelog }} + files: | + ./release_files/*