@@ -2,63 +2,49 @@ name: Release
22
33on :
44 push :
5- tags :
6- - ' v*'
5+ branches : [ main ]
76 workflow_dispatch :
8- inputs :
9- version :
10- description : Version from Directory.Build.props, for example 10.0.0
11- required : true
12- type : string
137
148permissions :
15- contents : write
9+ contents : read
1610
1711env :
1812 DOTNET_VERSION : 10.0.x
1913 SOLUTION : ManagedCode.FeatureChecker.slnx
2014 PROJECT : ManagedCode.FeatureChecker/ManagedCode.FeatureChecker.csproj
15+ COVERAGE_THRESHOLD : 85
16+ COVERAGE_FILE : ManagedCode.FeatureChecker.Tests/bin/Release/net10.0/TestResults/coverage.cobertura.xml
2117
2218jobs :
23- release :
19+ build :
20+ name : Build, Test, and Pack
2421 runs-on : ubuntu-latest
2522
23+ outputs :
24+ version : ${{ steps.version.outputs.version }}
25+
2626 steps :
27- - uses : actions/checkout@v6
28- with :
29- fetch-depth : 0
27+ - name : Checkout
28+ uses : actions/checkout@v6
3029
3130 - name : Setup .NET
3231 uses : actions/setup-dotnet@v5
3332 with :
3433 dotnet-version : ${{ env.DOTNET_VERSION }}
3534
36- - name : Resolve release version
35+ - name : Resolve package version
3736 id : version
3837 shell : bash
39- env :
40- DISPATCH_VERSION : ${{ inputs.version }}
4138 run : |
42- props_version="$(grep -oPm1 '(?<=<Version>)[^<]+' Directory.Build.props)"
43-
44- if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
45- version="${GITHUB_REF_NAME#v}"
46- else
47- version="${DISPATCH_VERSION}"
48- fi
39+ version="$(grep -oPm1 '(?<=<Version>)[^<]+' Directory.Build.props)"
4940
5041 if [[ -z "${version}" ]]; then
51- echo "::error::Release version is required."
52- exit 1
53- fi
54-
55- if [[ "${version}" != "${props_version}" ]]; then
56- echo "::error::Release version ${version} does not match Directory.Build.props version ${props_version}."
42+ echo "::error::Directory.Build.props must define <Version>."
5743 exit 1
5844 fi
5945
6046 echo "version=${version}" >> "${GITHUB_OUTPUT}"
61- echo "tag=v${ version}" >> "${GITHUB_OUTPUT }"
47+ echo "Package version: ${version }"
6248
6349 - name : Restore dependencies
6450 run : dotnet restore ${{ env.SOLUTION }}
@@ -73,54 +59,86 @@ jobs:
7359 run : dotnet build ${{ env.SOLUTION }} --configuration Release --no-restore -p:RunAnalyzers=true
7460
7561 - name : Test
76- run : dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-restore --verbosity normal
62+ run : dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal
7763
78- - name : Coverage
79- run : dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --coverage --coverage-output-format cobertura
64+ - name : Integration tests
65+ run : dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --treenode-filter "/*/*/*/*[Category=Integration]"
8066
81- - name : Pack
82- run : dotnet pack ${{ env.PROJECT }} --configuration Release --no-build --output artifacts
67+ - name : Coverage
68+ run : dotnet test --solution ${{ env.SOLUTION }} --configuration Release --no-build --verbosity normal -- --coverage --coverage- output coverage.cobertura.xml --coverage-output-format cobertura
8369
84- - name : Ensure release tag exists
70+ - name : Enforce coverage threshold
8571 shell : bash
8672 run : |
87- tag="${{ steps.version.outputs.tag }}"
88-
89- if git rev-parse "${tag}" >/dev/null 2>&1; then
90- echo "Tag ${tag} already exists."
91- exit 0
73+ if [[ ! -f "${COVERAGE_FILE}" ]]; then
74+ echo "::error::Coverage file not found: ${COVERAGE_FILE}"
75+ exit 1
9276 fi
9377
94- git config user.name "github-actions[bot]"
95- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
96- git tag -a "${tag}" -m "Release ${tag}"
97- git push origin "${tag}"
78+ line_rate="$(sed -n 's/.*line-rate="\([^"]*\)".*/\1/p' "${COVERAGE_FILE}" | head -n 1)"
9879
99- - name : Generate release notes
100- shell : bash
101- run : |
102- tag="${{ steps.version.outputs.tag }}"
103- previous_tag="$(git describe --tags --abbrev=0 "${tag}^" 2>/dev/null || true)"
80+ if [[ -z "${line_rate}" ]]; then
81+ echo "::error::Could not read line-rate from ${COVERAGE_FILE}"
82+ exit 1
83+ fi
10484
105- {
106- echo "## ${tag}"
107- echo
85+ awk -v rate="${line_rate}" -v threshold="${COVERAGE_THRESHOLD}" 'BEGIN {
86+ coverage = rate * 100
87+ printf("Line coverage: %.2f%%\n", coverage)
10888
109- if [[ -n "${previous_tag}" ]]; then
110- git log --pretty=format:'- %s (%h)' "${previous_tag}..HEAD"
111- else
112- git log --pretty=format:'- %s (%h)'
113- fi
114- } > release-notes.md
89+ if (coverage + 0.000001 < threshold) {
90+ printf("Line coverage %.2f%% is below required %.2f%%\n", coverage, threshold)
91+ exit 1
92+ }
93+ }'
94+
95+ - name : Pack
96+ run : dotnet pack ${{ env.PROJECT }} --configuration Release --no-build --output artifacts
11597
11698 - name : Upload package artifacts
11799 uses : actions/upload-artifact@v4
118100 with :
119101 name : nuget-packages
120- path : artifacts/*
102+ path : |
103+ artifacts/*.nupkg
104+ artifacts/*.snupkg
121105 if-no-files-found : error
106+ retention-days : 5
107+
108+ - name : Upload test artifacts
109+ uses : actions/upload-artifact@v4
110+ if : always()
111+ with :
112+ name : test-results
113+ path : |
114+ **/TestResults/**
115+ if-no-files-found : ignore
116+ retention-days : 5
117+
118+ publish-nuget :
119+ name : Publish to NuGet
120+ needs : build
121+ runs-on : ubuntu-latest
122+ if : github.ref == 'refs/heads/main'
123+
124+ outputs :
125+ published : ${{ steps.publish.outputs.published }}
126+ version : ${{ needs.build.outputs.version }}
127+
128+ steps :
129+ - name : Download package artifacts
130+ uses : actions/download-artifact@v5
131+ with :
132+ name : nuget-packages
133+ path : artifacts
134+
135+ - name : Setup .NET
136+ uses : actions/setup-dotnet@v5
137+ with :
138+ dotnet-version : ${{ env.DOTNET_VERSION }}
122139
123140 - name : Publish NuGet packages
141+ id : publish
124142 shell : bash
125143 env :
126144 NUGET_API_KEY : ${{ secrets.NUGET_API_KEY }}
@@ -134,18 +152,109 @@ jobs:
134152 packages=(artifacts/*.nupkg)
135153
136154 if [[ "${#packages[@]}" -eq 0 ]]; then
137- echo "::error::No NuGet packages were produced ."
155+ echo "::error::No NuGet packages were downloaded ."
138156 exit 1
139157 fi
140158
159+ published=false
160+
141161 for package in "${packages[@]}"; do
142- dotnet nuget push "${package}" --api-key "${NUGET_API_KEY}" --source https://api.nuget.org/v3/index.json --skip-duplicate
162+ echo "Publishing ${package}..."
163+ output="$(dotnet nuget push "${package}" --api-key "${NUGET_API_KEY}" --source https://api.nuget.org/v3/index.json --skip-duplicate 2>&1)"
164+ exit_code=$?
165+ echo "${output}"
166+
167+ if [[ "${exit_code}" -ne 0 ]]; then
168+ echo "::error::Failed to publish ${package}."
169+ exit "${exit_code}"
170+ fi
171+
172+ if echo "${output}" | grep -Eiq 'your package was pushed|successfully published'; then
173+ published=true
174+ fi
143175 done
144176
177+ echo "published=${published}" >> "${GITHUB_OUTPUT}"
178+
179+ create-release :
180+ name : Create GitHub Release and Tag
181+ needs : publish-nuget
182+ runs-on : ubuntu-latest
183+ if : needs.publish-nuget.outputs.published == 'true'
184+
185+ permissions :
186+ contents : write
187+
188+ steps :
189+ - name : Checkout
190+ uses : actions/checkout@v6
191+ with :
192+ fetch-depth : 0
193+
194+ - name : Download package artifacts
195+ uses : actions/download-artifact@v5
196+ with :
197+ name : nuget-packages
198+ path : artifacts
199+
200+ - name : Create and push tag
201+ shell : bash
202+ run : |
203+ version="${{ needs.publish-nuget.outputs.version }}"
204+ tag="v${version}"
205+
206+ git config user.name "github-actions[bot]"
207+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
208+
209+ if git rev-parse "${tag}" >/dev/null 2>&1; then
210+ echo "Tag ${tag} already exists."
211+ exit 0
212+ fi
213+
214+ git tag -a "${tag}" -m "Release ${tag}"
215+ git push origin "${tag}"
216+
217+ - name : Generate release notes
218+ shell : bash
219+ run : |
220+ version="${{ needs.publish-nuget.outputs.version }}"
221+ tag="v${version}"
222+ previous_tag="$(git tag --sort=-version:refname | grep -v "^${tag}$" | head -n 1 || true)"
223+
224+ {
225+ echo "# Release ${version}"
226+ echo
227+ echo "Released on $(date +'%Y-%m-%d')"
228+ echo
229+
230+ if [[ -n "${previous_tag}" ]]; then
231+ echo "## Changes since ${previous_tag}"
232+ echo
233+ git log --pretty=format:'- %s (%h)' "${previous_tag}..HEAD"
234+ else
235+ echo "## Initial release"
236+ echo
237+ git log --pretty=format:'- %s (%h)' --max-count=20
238+ fi
239+
240+ echo
241+ echo
242+ echo "## NuGet Packages"
243+ echo
244+
245+ for package in artifacts/*.nupkg; do
246+ package_name="$(basename "${package}" .nupkg)"
247+ package_id="${package_name%.${version}}"
248+ echo "- [${package_id} ${version}](https://www.nuget.org/packages/${package_id}/${version})"
249+ done
250+ } > release-notes.md
251+
145252 - name : Create GitHub Release
146253 uses : softprops/action-gh-release@v2
147254 with :
148- tag_name : ${{ steps.version .outputs.tag }}
149- name : ${{ steps.version .outputs.tag }}
255+ tag_name : v ${{ needs.publish-nuget .outputs.version }}
256+ name : v ${{ needs.publish-nuget .outputs.version }}
150257 body_path : release-notes.md
151- files : artifacts/*
258+ files : |
259+ artifacts/*.nupkg
260+ artifacts/*.snupkg
0 commit comments