Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
name: Release

on:
push:
branches:
- main

concurrency:
group: release
cancel-in-progress: false

env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true

jobs:
release:
name: Build Β· Test Β· Package Β· Release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read

# ── Projects to package (mirrors AppVeyor $PROJECTS) ──────────────────────
# Update this list if you add or remove sub-projects.
env:
PACKAGE_PROJECTS: "Configuration ElasticSearch Elmah EventLog I18n.PtBr Log4Net RabbitMQ Redis Utils"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add CouchDB to PACKAGE_PROJECTS.

Src/CrispyWaffle.CouchDB/CrispyWaffle.CouchDB.csproj:1-10 already carries package metadata, but this list omits it. The binary and NuGet loops below therefore never collect or publish the CouchDB artifacts.

Suggested fix
-      PACKAGE_PROJECTS: "Configuration ElasticSearch Elmah EventLog I18n.PtBr Log4Net RabbitMQ Redis Utils"
+      PACKAGE_PROJECTS: "Configuration CouchDB ElasticSearch Elmah EventLog I18n.PtBr Log4Net RabbitMQ Redis Utils"
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml at line 27, The PACKAGE_PROJECTS environment
list in the release workflow is missing the CouchDB project, so update the
PACKAGE_PROJECTS value (the variable named PACKAGE_PROJECTS in the release.yml
workflow) to include "CouchDB" among the space-separated projects used by the
binary/NuGet collection and publish loops; ensure the entry matches the project
folder/name (e.g., add CouchDB to the existing string "Configuration
ElasticSearch Elmah EventLog I18n.PtBr Log4Net RabbitMQ Redis Utils") so the
Src/CrispyWaffle.CouchDB/CrispyWaffle.CouchDB.csproj artifacts are picked up and
published.

TARGET_FRAMEWORKS: "netstandard2.0 netstandard2.1 net6.0 net9.0"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sync TARGET_FRAMEWORKS with Directory.Build.props.

Directory.Build.props:2-4 and Src/CrispyWaffle/CrispyWaffle.csproj:1-10 currently build netstandard2.0;netstandard2.1;net8.0;net9.0. Keeping net6.0 here means the artifact loops below skip the real net8.0 output and still create empty net6.0 release zips.

Suggested fix
-      TARGET_FRAMEWORKS: "netstandard2.0 netstandard2.1 net6.0 net9.0"
+      TARGET_FRAMEWORKS: "netstandard2.0 netstandard2.1 net8.0 net9.0"
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml at line 28, The TARGET_FRAMEWORKS value in the
release workflow is out of sync with the project files: update the
TARGET_FRAMEWORKS variable (the symbol TARGET_FRAMEWORKS) from "netstandard2.0
netstandard2.1 net6.0 net9.0" to include net8.0 instead of net6.0 (e.g.
"netstandard2.0 netstandard2.1 net8.0 net9.0") so the artifact creation loop
picks up the real net8.0 build outputs rather than creating empty net6.0 zips.


# ── CouchDB service container ──────────────────────────────────────────────
services:
couchdb:
image: couchdb:3.3.3
env:
COUCHDB_USER: Admin
COUCHDB_PASSWORD: myP@ssw0rd
ports:
- 5984:5984
options: >-
--health-cmd "curl -sf http://localhost:5984/_up"
--health-interval 10s
--health-timeout 5s
--health-retries 10

steps:
# ── Checkout ─────────────────────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

# ── Toolchain ────────────────────────────────────────────────────────────
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"

- name: Setup Java 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

# ── GitVersion ───────────────────────────────────────────────────────────
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v3
with:
versionSpec: "6.x"

- name: Determine version
id: gitversion
uses: gittools/actions/gitversion/execute@v3

- name: Patch .props files with computed version
run: |
VERSION="${{ steps.gitversion.outputs.semVer }}"
echo "Patching version β†’ $VERSION"
find . -name "*.props" -print0 | xargs -0 sed -i \
-e "s|<Version>[^<]*</Version>|<Version>$VERSION</Version>|g" \
-e "s|<PackageVersion>[^<]*</PackageVersion>|<PackageVersion>$VERSION</PackageVersion>|g" \
-e "s|<AssemblyVersion>[^<]*</AssemblyVersion>|<AssemblyVersion>$VERSION</AssemblyVersion>|g" \
-e "s|<FileVersion>[^<]*</FileVersion>|<FileVersion>$VERSION</FileVersion>|g" \
-e "s|<InformationalVersion>[^<]*</InformationalVersion>|<InformationalVersion>$VERSION</InformationalVersion>|g"

# ── Resolve solution name ─────────────────────────────────────────────────
- name: Resolve solution name
id: solution
run: |
SLN=$(find . -maxdepth 2 -name "*.sln" | head -1 | xargs basename -s .sln)
echo "name=$SLN" >> "$GITHUB_OUTPUT"
echo "Solution: $SLN"

# ── Install global tools ──────────────────────────────────────────────────
- name: Install dotnet-sonarscanner
run: dotnet tool install --global dotnet-sonarscanner

- name: Restore NuGet packages
run: dotnet restore

# ── SonarCloud: begin ─────────────────────────────────────────────────────
- name: SonarCloud – begin scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
SLN="${{ steps.solution.outputs.name }}"
VERSION="${{ steps.gitversion.outputs.semVer }}"
PROJECT="${GITHUB_REPOSITORY/\//_}"
ORG="${GITHUB_REPOSITORY_OWNER}"

dotnet sonarscanner begin \
"/k:$PROJECT" \
"/o:$ORG" \
"/v:$VERSION" \
"/d:sonar.host.url=https://sonarcloud.io" \
"/d:sonar.token=$SONAR_TOKEN" \
"/d:sonar.scanner.scanAll=false" \
"/d:sonar.branch.name=main" \
"/d:sonar.exclusions=**/bin/**/*,**/obj/**/*" \
"/d:sonar.coverage.exclusions=**/${SLN}.Tests/**,**/*Tests.cs" \
"/d:sonar.cs.opencover.reportsPaths=Tests/${SLN}.Tests/coverage.net9.0.opencover.xml"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Aggregate coverage from all test projects, not just ${SLN}.Tests.

The test step runs every Tests/**/*.csproj, but all reporting/copy steps only read Tests/${SLN}.Tests/.... The repo already has Tests/CrispyWaffle.IntegrationTests, so its coverage will never reach SonarCloud, Codecov, Codacy, or the coverage bundle.

Also applies to: 128-137, 147-163, 198-201

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml at line 121, The workflow currently passes a
hard-coded report path
"/d:sonar.cs.opencover.reportsPaths=Tests/${SLN}.Tests/coverage.net9.0.opencover.xml"
which only includes the `${SLN}.Tests` project; update these reporter/copy
arguments (the Sonar `sonar.cs.opencover.reportsPaths`, the Codecov/Codacy
upload and any copy steps referenced at the same locations) to accept a glob
that aggregates all test project coverage files such as
`Tests/**/coverage.net9.0.opencover.xml` (or build the list from the
`Tests/**/*.csproj` step), ensuring the Sonar/Codecov/Codacy parameters use that
aggregated list instead of the single `Tests/${SLN}.Tests/...` path; apply the
same change to the other occurrences mentioned (the blocks around the other
lines referenced) so all test projects’ coverage is uploaded.


# ── Build ─────────────────────────────────────────────────────────────────
- name: Build
run: dotnet build --no-restore --verbosity minimal ${{ steps.solution.outputs.name }}.sln

# ── Test + coverage ───────────────────────────────────────────────────────
- name: Test with coverage
run: |
for TEST_PROJ in $(find ./Tests -name "*.csproj" -type f); do
dotnet test "$TEST_PROJ" \
--no-build \
--verbosity minimal \
/p:CollectCoverage=true \
/p:CoverletOutputFormat='"cobertura,opencover,lcov"' \
--logger:"junit;LogFilePath=test-results.xml"
Comment on lines +128 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Coverage report file naming may not match the hardcoded paths used later for Sonar/Codecov/Codacy.

dotnet test is run with Coverlet enabled but without an explicit CoverletOutput (or per-framework outputs). With multiple TFMs/projects, Coverlet typically generates framework/project-specific filenames (e.g. coverage.net9.0.opencover.xml), while later steps assume a single fixed path (Tests/${SLN}.Tests/coverage.net9.0.opencover.xml) for SonarCloud/Codecov/Codacy. This mismatch can cause those tools to miss or fail on coverage. Consider either setting CoverletOutput explicitly per test project/TFM to match the expected path, or using globs and merging coverage reports before passing them on.

done

- name: Publish test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ github.run_id }}
path: Tests/**/test-results.xml

# ── Coverage reporting ────────────────────────────────────────────────────
- name: Report coverage β†’ Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: Tests/${{ steps.solution.outputs.name }}.Tests/coverage.net9.0.opencover.xml
fail_ci_if_error: false

- name: Report coverage β†’ Codacy
run: |
SLN="${{ steps.solution.outputs.name }}"
curl -fsSL \
https://github.com/codacy/codacy-coverage-reporter/releases/latest/download/codacy-coverage-reporter-assembly.jar \
-o codacy-reporter.jar
java -jar codacy-reporter.jar report \
-l CSharp \
-t "${{ secrets.CODACY_PROJECT_TOKEN }}" \
-r "Tests/${SLN}.Tests/coverage.net9.0.opencover.xml"

# ── SonarCloud: end ───────────────────────────────────────────────────────
- name: SonarCloud – end scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN"

# ═══════════════════════════════════════════════════════════════════════════
# RELEASE PHASE – everything below runs only after a clean green build/test
# ═══════════════════════════════════════════════════════════════════════════

# ── Collect binary artifacts ──────────────────────────────────────────────
- name: Collect binary artifacts
run: |
SLN="${{ steps.solution.outputs.name }}"
VERSION="${{ steps.gitversion.outputs.semVer }}"
mkdir -p Artifacts/Coverage

for TFM in $TARGET_FRAMEWORKS; do
# Core project
SRC="Src/${SLN}/bin/Release/${TFM}"
DST="Artifacts/Build/Core/${TFM}"
mkdir -p "$DST"
[ -d "$SRC" ] && rsync -a --exclude="refs/" "$SRC/" "$DST/"

# Sub-projects
for PROJ in $PACKAGE_PROJECTS; do
SRC="Src/${SLN}.${PROJ}/bin/Release/${TFM}"
DST="Artifacts/Build/${PROJ}/${TFM}"
mkdir -p "$DST"
[ -d "$SRC" ] && rsync -a --exclude="refs/" "$SRC/" "$DST/"
done
done

# Coverage reports
cp Tests/${SLN}.Tests/*.xml Artifacts/Coverage/ 2>/dev/null || true
cp Tests/${SLN}.Tests/*.json Artifacts/Coverage/ 2>/dev/null || true
cp Tests/${SLN}.Tests/*.info Artifacts/Coverage/ 2>/dev/null || true

# ── Compress release zips ─────────────────────────────────────────────────
- name: Compress release archives
run: |
SLN="${{ steps.solution.outputs.name }}"
VERSION="${{ steps.gitversion.outputs.semVer }}"
mkdir -p Zips

# Coverage archive
zip -9 -r "Zips/${SLN}.${VERSION}.Coverage.zip" Artifacts/Coverage/

# Per-TFM binary archives
for TFM in $TARGET_FRAMEWORKS; do
zip -9 -r "Zips/${SLN}.Core.${TFM}.${VERSION}.zip" \
"Artifacts/Build/Core/${TFM}/"

for PROJ in $PACKAGE_PROJECTS; do
zip -9 -r "Zips/${SLN}.${PROJ}.${TFM}.${VERSION}.zip" \
"Artifacts/Build/${PROJ}/${TFM}/"
done
done

# ── Collect NuGet packages ────────────────────────────────────────────────
- name: Collect NuGet packages
run: |
SLN="${{ steps.solution.outputs.name }}"
VERSION="${{ steps.gitversion.outputs.semVer }}"
mkdir -p Zips

# Core nupkg / snupkg
cp "Src/${SLN}/bin/Release/${SLN}.${VERSION}.nupkg" Zips/ 2>/dev/null || true
cp "Src/${SLN}/bin/Release/${SLN}.${VERSION}.snupkg" Zips/ 2>/dev/null || true

# Sub-project packages
for PROJ in $PACKAGE_PROJECTS; do
cp "Src/${SLN}.${PROJ}/bin/Release/${SLN}.${PROJ}.${VERSION}.nupkg" Zips/ 2>/dev/null || true
cp "Src/${SLN}.${PROJ}/bin/Release/${SLN}.${PROJ}.${VERSION}.snupkg" Zips/ 2>/dev/null || true
done

# ── Push packages to NuGet.org ────────────────────────────────────────────
- name: Push to NuGet.org
run: |
for PKG in Zips/*.nupkg; do
echo "Publishing $PKG …"
dotnet nuget push "$PKG" \
--api-key "${{ secrets.NUGET_TOKEN }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
done

# ── Tag the release commit ────────────────────────────────────────────────
# This tag is what GitVersion reads on the next run to derive the next
# version increment, so it must be pushed before any subsequent builds.
- name: Tag release version
run: |
VERSION="v${{ steps.gitversion.outputs.semVer }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
Comment on lines +255 to +261
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make tag creation idempotent.

git tag -a will fail on any rerun for the same version. That blocks recovery from a transient failure after packages were already pushed, unless someone manually deletes the tag first.

Suggested fix
       - name: Tag release version
         run: |
           VERSION="v${{ steps.gitversion.outputs.semVer }}"
           git config user.name  "github-actions[bot]"
           git config user.email "github-actions[bot]@users.noreply.github.com"
-          git tag -a "$VERSION" -m "Release $VERSION"
-          git push origin "$VERSION"
+          if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
+            echo "Tag $VERSION already exists; skipping"
+          else
+            git tag -a "$VERSION" -m "Release $VERSION"
+            git push origin "$VERSION"
+          fi
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Tag release version
run: |
VERSION="v${{ steps.gitversion.outputs.semVer }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
- name: Tag release version
run: |
VERSION="v${{ steps.gitversion.outputs.semVer }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "Tag $VERSION already exists; skipping"
else
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
fi
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 255 - 261, The "Tag release
version" step currently always runs git tag -a "$VERSION" which will fail on
reruns if the tag already exists; update the step to be idempotent by detecting
whether the tag already exists (e.g., use git ls-remote --tags origin "$VERSION"
or git rev-parse -q --verify "refs/tags/$VERSION") and only create and push the
tag if it is missing, otherwise skip tagging; reference the VERSION variable,
the git tag -a command and the git push origin "$VERSION" invocation when
implementing the conditional check.


# ── Create GitHub Release ─────────────────────────────────────────────────
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.gitversion.outputs.semVer }}
name: Release of ${{ steps.solution.outputs.name }} – v${{ steps.gitversion.outputs.semVer }}
generate_release_notes: true # auto-generates changelog from merged PRs
fail_on_unmatched_files: false
files: Zips/*
Loading