Skip to content

Commit ed277d2

Browse files
committed
Add github setup
1 parent 9b8c2fc commit ed277d2

File tree

5 files changed

+319
-0
lines changed

5 files changed

+319
-0
lines changed

.github/dependabot.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: 2
2+
updates:
3+
# Since we use central package management, we can just specify the root directory here as it contains
4+
# Directory.Packages.props which is the source of our dependency versions.
5+
- directory: "/"
6+
package-ecosystem: "nuget"
7+
schedule:
8+
interval: "daily"
9+
assignees:
10+
- "ehonda"
11+
open-pull-requests-limit: 5
12+
13+
# We can just specify the directory root here, as there is special casing for the /.github/workflows directory, as
14+
# documented in https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#directory
15+
- directory: "/"
16+
package-ecosystem: "github-actions"
17+
schedule:
18+
interval: "daily"
19+
assignees:
20+
- "ehonda"
21+
open-pull-requests-limit: 5

.github/release.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
changelog:
2+
exclude:
3+
authors:
4+
- dependabot[bot]
5+
categories:
6+
- title: Features 🦚
7+
labels:
8+
- enhancement
9+
- title: Infrastructure 🏗
10+
labels:
11+
- devops
12+
- infrastructure
13+
- title: Repository 💾
14+
labels:
15+
- repository
16+
- title: Miscellaneous 🦄
17+
labels:
18+
- "*"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# This GitHub Actions workflow, named ".NET Build and Test", is designed to be called by other workflows.
2+
# It defines a single job that runs on an Ubuntu environment.
3+
# This job checks out the repository's code, sets up .NET version 10,
4+
# explicitly builds the entire solution (including sample projects) to catch build errors,
5+
# and then executes the tests in Release configuration using TUnit.
6+
name: .NET Build and Test
7+
8+
on:
9+
workflow_call:
10+
11+
jobs:
12+
build_and_test:
13+
name: Build solution and run tests
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup .NET
20+
uses: actions/setup-dotnet@v4
21+
with:
22+
dotnet-version: '10.x'
23+
24+
# Explicitly build the entire solution first to catch any build errors,
25+
# including in sample projects that aren't built as part of the test dependencies.
26+
# This ensures that broken sample projects cannot be merged, as they would
27+
# previously pass CI since tests don't depend on sample projects.
28+
- name: Build solution
29+
run: dotnet build --configuration Release
30+
31+
# TUnit with Microsoft Testing Platform requires special handling:
32+
# 1. TUnit doesn't use VSTest - it uses Microsoft Testing Platform with source generators
33+
# 2. 'dotnet test' at solution level builds but doesn't execute tests (observed behavior)
34+
# 3. Must use 'dotnet run' on individual test projects to actually execute tests
35+
# 4. The test project must have OutputType=Exe and include Microsoft.Testing.Extensions
36+
# packages (like TrxReport) for reporting features to work
37+
- name: Discover test projects
38+
id: discover-tests
39+
run: |
40+
TEST_PROJECTS=$(find tests -name "*.csproj" -type f | tr '\n' ' ')
41+
echo "test_projects=$TEST_PROJECTS" >> $GITHUB_OUTPUT
42+
echo "Found test projects: $TEST_PROJECTS"
43+
44+
- name: Run tests
45+
run: |
46+
for project in ${{ steps.discover-tests.outputs.test_projects }}; do
47+
echo "Running tests in $project"
48+
# Use 'dotnet run' (not 'dotnet test') as required by TUnit with Microsoft Testing Platform
49+
# This executes the test project as an executable, allowing TUnit's source generators to work properly
50+
dotnet run --project "$project" --configuration Release
51+
done

.github/workflows/pull_request.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This GitHub Actions workflow, named "Pull Request", is triggered when a pull request is made to the `main` branch.
2+
# It runs a single job that calls the reusable workflow defined in `./.github/workflows/dotnet-build-and-test.yml` to build the solution and execute tests.
3+
name: Pull Request
4+
5+
on:
6+
pull_request:
7+
branches:
8+
- main
9+
10+
jobs:
11+
build_and_test:
12+
uses: ./.github/workflows/dotnet-build-and-test.yml

.github/workflows/push.yml

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
name: Release and publish to nuget.org
2+
3+
# This workflow automatically creates a release and publishes to nuget.org, if the package version does not already
4+
# exist. For now it is just copied from this work in progress (which has more context in comments):
5+
# https://github.com/ehonda/EHonda.HelloNuget/blob/eb9bd36a907fb7ca57b6a88df6e935a285517ca4/.github/workflows/auto-release.yml
6+
#
7+
# This GitHub Actions workflow, named "Release and publish to nuget.org", is triggered on pushes to the `main` branch.
8+
# It orchestrates a series of jobs for multiple projects within the `src` directory to:
9+
# 1. Run tests using a reusable workflow.
10+
# 2. Dynamically determine the list of projects to process using the `define-projects-matrix` job.
11+
# 3. For each project identified by the matrix (handled by a single matrix job "release-and-publish"):
12+
# a. Extract package metadata (version and ID).
13+
# b. Check if the extracted package version already exists on nuget.org.
14+
# c. If the package version does not exist:
15+
# i. Create a new GitHub release for that package.
16+
# ii. Pack the project.
17+
# iii. Publish the new version to nuget.org.
18+
#
19+
# It originates from this:
20+
# https://github.com/ehonda/EHonda.HelloNuget/blob/eb9bd36a907fb7ca57b6a88df6e935a285517ca4/.github/workflows/auto-release.yml
21+
22+
# This workflow is triggered on pushes to the `main` branch and can also be run manually via `workflow_dispatch`.
23+
# It requires the `NUGET_ORG_API_KEY` secret to be set in the repository settings for publishing to NuGet.org.
24+
on:
25+
workflow_dispatch:
26+
push:
27+
branches:
28+
- main
29+
30+
jobs:
31+
# This job, "build_and_test", builds the solution and executes the project's tests.
32+
# It achieves this by calling the reusable workflow defined in `./.github/workflows/dotnet-build-and-test.yml`.
33+
# This step is crucial for ensuring code quality and correctness before any release or publishing actions are taken.
34+
build_and_test:
35+
uses: ./.github/workflows/dotnet-build-and-test.yml
36+
37+
# This job, "define-projects-matrix", dynamically determines the list of projects to be processed.
38+
# It scans the src/ directory for subdirectories containing correspondingly named .csproj files.
39+
# The output is a JSON array of project names, used by the 'release-and-publish' job's matrix strategy.
40+
define-projects-matrix:
41+
name: Define Projects Matrix
42+
runs-on: ubuntu-latest
43+
outputs:
44+
projects: ${{ steps.generate-matrix.outputs.projects }}
45+
steps:
46+
- name: Checkout code
47+
uses: actions/checkout@v4
48+
- name: Generate projects matrix
49+
id: generate-matrix
50+
run: |
51+
PROJECT_ARRAY=()
52+
# Iterate over subdirectories in src/
53+
for dir_path in src/*/; do
54+
# Remove trailing slash for basename
55+
dir_path_cleaned=${dir_path%/}
56+
dir_name=$(basename "$dir_path_cleaned")
57+
58+
# Check if a .csproj file exists in this directory with the same name as the directory
59+
# e.g., src/Core/Core.csproj
60+
if [ -f "$dir_path_cleaned/$dir_name.csproj" ]; then
61+
# Add the directory name, correctly enclosed in double quotes, to the bash array
62+
PROJECT_ARRAY+=("\"$dir_name\"")
63+
fi
64+
done
65+
66+
# Join the array elements with a comma
67+
# IFS (Internal Field Separator) is set to comma for the join operation
68+
OLD_IFS="$IFS"
69+
IFS=','
70+
PROJECT_LIST="${PROJECT_ARRAY[*]}"
71+
IFS="$OLD_IFS" # Restore IFS
72+
73+
JSON_OUTPUT="[$PROJECT_LIST]"
74+
echo "Generated project matrix: $JSON_OUTPUT" # For debugging
75+
echo "projects=$JSON_OUTPUT" >> "$GITHUB_OUTPUT"
76+
77+
# Fail if no projects are found, as this likely indicates an issue.
78+
if [ ${#PROJECT_ARRAY[@]} -eq 0 ]; then
79+
echo "::error::No projects found matching the pattern src/*/<project_name>.csproj where project name matches directory name."
80+
echo "Resulting JSON_OUTPUT would be []."
81+
exit 1
82+
fi
83+
84+
# This job, "release-and-publish", handles the entire release and publish process for each project.
85+
# It uses a matrix strategy to iterate over specified projects. For each project, it:
86+
# 1. Checks out code and sets up .NET.
87+
# 2. Extracts package metadata (version and ID).
88+
# 3. Checks if the package version already exists on NuGet.org.
89+
# 4. If the version is new, it creates a GitHub release, packs the project, and publishes it to NuGet.org.
90+
release-and-publish:
91+
name: Release & Publish ${{ matrix.project_name }}
92+
runs-on: ubuntu-latest
93+
needs: [build_and_test, define-projects-matrix]
94+
permissions:
95+
contents: write # Required for creating GitHub releases
96+
strategy:
97+
matrix:
98+
# Dynamically set project_name from the output of define-projects-matrix job
99+
project_name: ${{ fromJSON(needs.define-projects-matrix.outputs.projects) }}
100+
steps:
101+
- name: Checkout
102+
uses: actions/checkout@v4
103+
104+
- name: Setup .NET
105+
uses: actions/setup-dotnet@v4
106+
with:
107+
dotnet-version: '10.x' # Updated to 10.x
108+
109+
# Objective: Extracts the PackageVersion and PackageId from the project's .csproj file.
110+
# Conditions: Runs for every project defined in the matrix. This is a foundational step
111+
# and does not have an 'if' condition.
112+
# How: Uses 'dotnet build --getProperty:PackageVersion' and '--getProperty:PackageId'
113+
# to retrieve metadata. The values are then trimmed and set as step outputs.
114+
# The job will fail if these values are empty or whitespace.
115+
- name: Get package metadata for ${{ matrix.project_name }}
116+
id: get-metadata
117+
run: |
118+
PROJECT_FILE_PATH="src/${{ matrix.project_name }}/${{ matrix.project_name }}.csproj"
119+
echo "Attempting to get PackageVersion from $PROJECT_FILE_PATH"
120+
VERSION_RAW=$(dotnet build "$PROJECT_FILE_PATH" --getProperty:PackageVersion --nologo -v q)
121+
echo "Attempting to get PackageId from $PROJECT_FILE_PATH"
122+
ID_RAW=$(dotnet build "$PROJECT_FILE_PATH" --getProperty:PackageId --nologo -v q)
123+
124+
echo "Raw output for Version for ${{ matrix.project_name }}: [$VERSION_RAW]"
125+
echo "Raw output for ID for ${{ matrix.project_name }}: [$ID_RAW]"
126+
127+
VERSION_VAL=$(echo "$VERSION_RAW" | xargs)
128+
ID_VAL=$(echo "$ID_RAW" | xargs)
129+
130+
echo "Trimmed Version for ${{ matrix.project_name }}: [$VERSION_VAL]"
131+
echo "Trimmed ID for ${{ matrix.project_name }}: [$ID_VAL]"
132+
133+
if [ -z "$VERSION_VAL" ]; then
134+
echo "::error::PackageVersion is empty or only whitespace for $PROJECT_FILE_PATH. Raw: [$VERSION_RAW]"
135+
exit 1
136+
fi
137+
if [ -z "$ID_VAL" ]; then
138+
echo "::error::PackageId is empty or only whitespace for $PROJECT_FILE_PATH. Raw: [$ID_RAW]"
139+
exit 1
140+
fi
141+
142+
echo "package-version=$VERSION_VAL" >> "$GITHUB_OUTPUT"
143+
echo "package-id=$ID_VAL" >> "$GITHUB_OUTPUT"
144+
145+
# Objective: Ensures a GitHub Release for the current package version exists. If not,
146+
# it creates one. This step determines if the package should be built and published.
147+
# Conditions: Runs for every project after package metadata has been successfully extracted.
148+
# How: Constructs a release tag (e.g., <PackageId>-v<PackageVersion>).
149+
# Uses 'gh release view' to check for an existing release.
150+
# If found, sets 'should-release=false'.
151+
# If not found, creates a new release using 'gh release create' and sets 'should-release=true'.
152+
# Requires GITHUB_TOKEN for authentication.
153+
- name: Ensure GitHub Release for ${{ matrix.project_name }}
154+
id: manage_release
155+
env:
156+
PACKAGE_ID: ${{ steps.get-metadata.outputs.package-id }}
157+
PACKAGE_VERSION: ${{ steps.get-metadata.outputs.package-version }}
158+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159+
run: |
160+
RELEASE_TAG="${PACKAGE_ID}-v${PACKAGE_VERSION}"
161+
echo "Attempting to manage GitHub release with tag: $RELEASE_TAG for project ${{ matrix.project_name }}"
162+
163+
# Check if the release already exists
164+
# Add --repo to be explicit, though gh usually infers it from the checkout.
165+
if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
166+
echo "INFO: GitHub release $RELEASE_TAG already exists for ${{ matrix.project_name }}."
167+
echo "should-release=false" >> $GITHUB_OUTPUT
168+
else
169+
echo "INFO: GitHub release $RELEASE_TAG does not exist for ${{ matrix.project_name }}. Creating it."
170+
# Create the release
171+
gh release create "$RELEASE_TAG" \
172+
--repo "$GITHUB_REPOSITORY" \
173+
--title "Release ${PACKAGE_ID} v${PACKAGE_VERSION}" \
174+
--generate-notes
175+
echo "INFO: Successfully created GitHub release $RELEASE_TAG for ${{ matrix.project_name }}."
176+
echo "should-release=true" >> $GITHUB_OUTPUT
177+
fi
178+
179+
# Objective: Creates a NuGet package (.nupkg file) for the project.
180+
# Conditions: Runs only if the 'Ensure GitHub Release' step output 'should-release' is 'true',
181+
# indicating a new GitHub release was created (or was intended to be created).
182+
# How: Uses the 'dotnet pack' command with 'Release' configuration.
183+
# The PackageVersion is explicitly passed to ensure consistency.
184+
# Outputs the path and filename of the generated .nupkg file.
185+
- name: Pack ${{ matrix.project_name }}
186+
id: pack
187+
if: steps.manage_release.outputs.should-release == 'true'
188+
env:
189+
PROJECT_NAME: ${{ matrix.project_name }}
190+
PACKAGE_ID: ${{ steps.get-metadata.outputs.package-id }}
191+
PACKAGE_VERSION: ${{ steps.get-metadata.outputs.package-version }}
192+
run: |
193+
PROJECT_FILE_PATH="src/$PROJECT_NAME/$PROJECT_NAME.csproj"
194+
echo "Packing $PROJECT_FILE_PATH version $PACKAGE_VERSION"
195+
dotnet pack "$PROJECT_FILE_PATH" \
196+
--configuration Release \
197+
--output ./packages \
198+
-p:PackageVersion="$PACKAGE_VERSION"
199+
200+
NUPKG_FILENAME="$PACKAGE_ID.$PACKAGE_VERSION.nupkg"
201+
echo "NuGet package will be: $NUPKG_FILENAME"
202+
echo "nupkg_path=./packages/$NUPKG_FILENAME" >> "$GITHUB_OUTPUT"
203+
echo "nupkg_filename=$NUPKG_FILENAME" >> "$GITHUB_OUTPUT"
204+
205+
# Objective: Publishes the generated NuGet package to NuGet.org.
206+
# Conditions: Runs only if the 'Ensure GitHub Release' step output 'should-release' is 'true',
207+
# meaning a new GitHub release was created and the package was packed.
208+
# How: Uses 'dotnet nuget push' with the .nupkg file path from the 'Pack' step.
209+
# Requires the NUGET_ORG_API_KEY secret for authentication with NuGet.org.
210+
- name: Push ${{ matrix.project_name }} to NuGet.org
211+
if: steps.manage_release.outputs.should-release == 'true'
212+
env:
213+
NUGET_ORG_API_KEY: ${{ secrets.NUGET_ORG_API_KEY }}
214+
NUPKG_PATH: ${{ steps.pack.outputs.nupkg_path }}
215+
run: |
216+
echo "Pushing ${{ steps.pack.outputs.nupkg_filename }} to NuGet.org"
217+
dotnet nuget push "$NUPKG_PATH" --source https://api.nuget.org/v3/index.json --api-key "$NUGET_ORG_API_KEY"

0 commit comments

Comments
 (0)