From 2746d5482bea590a2d63b13105af5b822955f7f7 Mon Sep 17 00:00:00 2001 From: "daniel.bohlin@live.se" Date: Sat, 2 May 2026 16:10:01 +0200 Subject: [PATCH 1/2] feat: migrate CI/CD from Azure DevOps to GitHub Actions Add .github/workflows/build.yml mirroring the Tharga.Crawler reference (MAJOR_MINOR=1.14, packs Tharga.Test.Toolkit). Remove obsolete azure-pipelines.yml and buildnumber.yml and drop them from Solution Items. --- .github/workflows/build.yml | 277 ++++++++++++++++++++++++++++++++++++ Tharga.Test.sln | 2 - azure-pipelines.yml | 103 -------------- buildnumber.yml | 102 ------------- plan/feature.md | 34 +++++ plan/plan.md | 25 ++++ 6 files changed, 336 insertions(+), 207 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 azure-pipelines.yml delete mode 100644 buildnumber.yml create mode 100644 plan/feature.md create mode 100644 plan/plan.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ad03c83 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,277 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +env: + MAJOR_MINOR: '1.14' + +permissions: + contents: write + pull-requests: write + security-events: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore 2>&1 | tee build.log + + - name: Check warnings + run: | + WARNING_COUNT=$(grep -c " warning " build.log || true) + echo "Build warnings: $WARNING_COUNT" + if [ "$WARNING_COUNT" -gt 15 ]; then + echo "::error::Build has $WARNING_COUNT warnings (threshold: 15)" + exit 1 + fi + + - name: Test with coverage + run: dotnet test -c Release --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + directory: ./coverage + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Compute version + id: version + run: | + LATEST_TAG=$(git tag -l "${MAJOR_MINOR}.*" --sort=-v:refname \ + | grep -E "^${MAJOR_MINOR}\.[0-9]+$" \ + | head -1) + + if [ -z "$LATEST_TAG" ]; then + PATCH=0 + else + PATCH=$(echo "$LATEST_TAG" | sed "s/${MAJOR_MINOR}\.\([0-9]*\)/\1/") + PATCH=$((PATCH + 1)) + fi + + VERSION="${MAJOR_MINOR}.${PATCH}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "previous_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT" + echo "Computed version: $VERSION (previous: $LATEST_TAG)" + + - name: Compute pre-release version + id: preversion + if: github.ref != 'refs/heads/master' + run: | + VERSION="${{ steps.version.outputs.version }}" + PRE_BASE="${VERSION}-pre" + + LATEST_PRE=$(git tag -l "${PRE_BASE}.*" --sort=-v:refname | head -1) + + if [ -z "$LATEST_PRE" ]; then + COUNTER=1 + else + COUNTER=$(echo "$LATEST_PRE" | sed "s/.*-pre\.\([0-9]*\)/\1/") + COUNTER=$((COUNTER + 1)) + fi + + PRE_VERSION="${PRE_BASE}.${COUNTER}" + echo "version=$PRE_VERSION" >> "$GITHUB_OUTPUT" + echo "Computed pre-release version: $PRE_VERSION" + + - name: Pack (stable — push to master) + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + run: | + dotnet pack Tharga.Test.Toolkit/Tharga.Test.Toolkit.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }} + + - name: Pack (pre-release — pull request) + if: github.event_name == 'pull_request' + run: | + dotnet pack Tharga.Test.Toolkit/Tharga.Test.Toolkit.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: ./artifacts/ + + security: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: csharp + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + + - name: Build for CodeQL + run: dotnet build -c Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + release: + needs: [build, security] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + environment: release + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: ./artifacts/ + + - name: Compute version + id: version + run: | + LATEST_TAG=$(git tag -l "${MAJOR_MINOR}.*" --sort=-v:refname \ + | grep -E "^${MAJOR_MINOR}\.[0-9]+$" \ + | head -1) + + if [ -z "$LATEST_TAG" ]; then + PATCH=0 + else + PATCH=$(echo "$LATEST_TAG" | sed "s/${MAJOR_MINOR}\.\([0-9]*\)/\1/") + PATCH=$((PATCH + 1)) + fi + + VERSION="${MAJOR_MINOR}.${PATCH}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "previous_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT" + + - name: Push to NuGet + run: | + for pkg in ./artifacts/*.nupkg; do + echo "Pushing $pkg..." + dotnet nuget push "$pkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate || true + done + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + NOTES_FLAG="" + if [ -n "${{ steps.version.outputs.previous_tag }}" ]; then + NOTES_FLAG="--notes-start-tag ${{ steps.version.outputs.previous_tag }}" + fi + gh release create "${{ steps.version.outputs.version }}" \ + --title "v${{ steps.version.outputs.version }}" \ + --generate-notes \ + $NOTES_FLAG \ + ./artifacts/*.nupkg + + - name: Comment released version on merged PR + if: always() + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=$(echo "${{ github.event.head_commit.message }}" | grep -oP 'Merge pull request #\K\d+' | head -1) + if [ -n "$PR_NUMBER" ]; then + gh pr comment "$PR_NUMBER" --body "Released as **v${{ steps.version.outputs.version }}** — https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.version }}" + else + echo "No PR number found in commit message — skipping PR comment." + fi + + prerelease: + needs: [build, security] + runs-on: ubuntu-latest + if: github.ref != 'refs/heads/master' && github.event_name == 'pull_request' + environment: prerelease + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: ./artifacts/ + + - name: Compute pre-release version + id: version + run: | + LATEST_TAG=$(git tag -l "${MAJOR_MINOR}.*" --sort=-v:refname \ + | grep -E "^${MAJOR_MINOR}\.[0-9]+$" \ + | head -1) + + if [ -z "$LATEST_TAG" ]; then + PATCH=0 + else + PATCH=$(echo "$LATEST_TAG" | sed "s/${MAJOR_MINOR}\.\([0-9]*\)/\1/") + PATCH=$((PATCH + 1)) + fi + + PRE_BASE="${MAJOR_MINOR}.${PATCH}-pre" + LATEST_PRE=$(git tag -l "${PRE_BASE}.*" --sort=-v:refname | head -1) + + if [ -z "$LATEST_PRE" ]; then + COUNTER=1 + else + COUNTER=$(echo "$LATEST_PRE" | sed "s/.*-pre\.\([0-9]*\)/\1/") + COUNTER=$((COUNTER + 1)) + fi + + VERSION="${PRE_BASE}.${COUNTER}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Push to NuGet + run: | + for pkg in ./artifacts/*.nupkg; do + echo "Pushing $pkg..." + dotnet nuget push "$pkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate || true + done + + - name: Create GitHub Pre-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.version }}" \ + --title "v${{ steps.version.outputs.version }} (pre-release)" \ + --notes "Pre-release from \`${{ github.head_ref }}\` branch." \ + --prerelease \ + ./artifacts/*.nupkg diff --git a/Tharga.Test.sln b/Tharga.Test.sln index a3ca656..ecd4613 100644 --- a/Tharga.Test.sln +++ b/Tharga.Test.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 18.2.11415.280 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{56A486D0-5EF5-47EF-8796-08C8F0577A5D}" ProjectSection(SolutionItems) = preProject - azure-pipelines.yml = azure-pipelines.yml - buildnumber.yml = buildnumber.yml LICENSE = LICENSE README.md = README.md EndProjectSection diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 0fd1ad9..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,103 +0,0 @@ -trigger: -- master - -pool: - vmImage: 'windows-latest' - -variables: - buildPlatform: 'Any CPU' - buildConfiguration: 'Release' - BQC.ForceNewBaseline: true - majorMinor: '1.14' - -stages: -- stage: Build - displayName: Build - jobs: - - job: Build - displayName: Build and Test - - steps: - - checkout: self - - - template: buildnumber.yml - parameters: - majorMinor: ${{ variables.majorMinor }} - - - task: NuGetToolInstaller@1 - - - task: DotNetCoreCLI@2 - displayName: 'Restore nuget packages' - inputs: - command: 'restore' - projects: '**/*.csproj' - feedsToUse: 'select' - - - task: DotNetCoreCLI@2 - displayName: 'Build' - inputs: - command: 'build' - projects: '**/*.csproj' - arguments: '-c Release --no-restore /p:Version=$(Build.BuildNumber)' - versioningScheme: 'byBuildNumber' - - - task: DotNetCoreCLI@2 - displayName: 'Test' - inputs: - command: 'test' - projects: '**/*.Tests.csproj' - arguments: '--collect "Code coverage" --configuration $(buildConfiguration) --filter "Category!=Database"' - - # - task: BuildQualityChecks@8 - # displayName: 'Build Quality Checks' - # inputs: - # checkWarnings: true - # warningFailOption: 'build' - # allowWarningVariance: true - # warningVariance: '5' - # checkCoverage: true - # coverageFailOption: 'build' - # coverageType: 'blocks' - # allowCoverageVariance: true - # coverageVariance: '5' - - - task: DotNetCoreCLI@2 - displayName: 'Pack Tharga.Test.Toolkit' - inputs: - command: 'pack' - packagesToPack: '**/Tharga.Test.Toolkit.csproj' - versioningScheme: 'byBuildNumber' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish artifacts' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'drop' - -- stage: Release - displayName: Release - dependsOn: Build - condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master')) - jobs: - - job: Release - displayName: Release - - steps: - - download: current - artifact: drop - - checkout: self - persistCredentials: true - - - task: NuGetCommand@2 - displayName: 'Push .nupkg to NuGet.org (with symbols)' - inputs: - command: 'push' - packagesToPush: '$(Pipeline.Workspace)/**/*.symbols.nupkg' - nuGetFeedType: 'external' - publishFeedCredentials: 'Nuget.org' - - - script: | - git tag $(Build.BuildNumber) - git push origin $(Build.BuildNumber) - workingDirectory: $(Build.SourcesDirectory) - displayName: Tag diff --git a/buildnumber.yml b/buildnumber.yml deleted file mode 100644 index ea37132..0000000 --- a/buildnumber.yml +++ /dev/null @@ -1,102 +0,0 @@ -# buildnumber.yml (steps template) -parameters: -- name: majorMinor - type: string - -- name: assignBuildVersionScript - type: string - default: | - $branchMaster = "refs/heads/master" - $majorMinor = $env:MAJOR_MINOR - - if ([string]::IsNullOrWhiteSpace($majorMinor)) { - throw "MAJOR_MINOR is empty. Ensure it is passed via env: MAJOR_MINOR." - } - - $orgUrl = $env:SYSTEM_COLLECTIONURI - $project = $env:SYSTEM_TEAMPROJECT - - $defId = $env:DEF_ID - if ([string]::IsNullOrWhiteSpace($defId) -or $defId -eq "-1") { - throw "DEF_ID is empty or -1 (defId='$defId')." - } - - $headers = @{ - Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" - } - - $uri = "$orgUrl$project/_apis/build/builds?definitions=$defId&branchName=$branchMaster&`$top=50&queryOrder=finishTimeDescending&api-version=7.1" - $resp = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get - - $releasePattern = "^$([regex]::Escape($majorMinor))\.(\d+)$" - $anyPattern = "^$([regex]::Escape($majorMinor))\.(\d+)(?:-pre\.\d+)?$" - - $picked = $null - if ($resp -and $resp.value -and $resp.count -ge 1) { - $latestRelease = $resp.value | Where-Object { $_.buildNumber -match $releasePattern } | Select-Object -First 1 - $latestAny = $resp.value | Where-Object { $_.buildNumber -match $anyPattern } | Select-Object -First 1 - - $picked = $latestRelease - if (-not $picked) { $picked = $latestAny } - } - - if (-not $picked) { - $patch = -1 # so nextPatch becomes 0 for a brand-new majorMinor/pipeline - } - else { - $latestBuildNumber = $picked.buildNumber - if ($latestBuildNumber -match $releasePattern) { $patch = $Matches[1] } - elseif ($latestBuildNumber -match $anyPattern) { $patch = $Matches[1] } - else { throw "Internal mismatch: '$latestBuildNumber' should have matched but did not." } - } - - $nextPatch = ([int]$patch) + 1 - - if ($env:BUILD_SOURCEBRANCH -eq $branchMaster) { - $newBuildNumber = "$majorMinor.$nextPatch" - } - else { - $preBase = "$majorMinor.$nextPatch-pre" - $prePattern = "^$([regex]::Escape($preBase))\.(\d+)$" - - $allUri = "$orgUrl$project/_apis/build/builds?definitions=$defId&`$top=100&queryOrder=finishTimeDescending&api-version=7.1" - $allResp = Invoke-RestMethod -Uri $allUri -Headers $headers -Method Get - - $maxCounter = $allResp.value | - Where-Object { $_.buildNumber -match $prePattern } | - ForEach-Object { [int]$Matches[1] } | - Measure-Object -Maximum | - Select-Object -ExpandProperty Maximum - - $preCounter = if ($maxCounter) { $maxCounter + 1 } else { 1 } - $newBuildNumber = "$preBase.$preCounter" - } - - Write-Host "##vso[build.updatebuildnumber]$newBuildNumber" - -steps: -# Windows -- task: PowerShell@2 - displayName: "Assign build version" - condition: eq( variables['Agent.OS'], 'Windows_NT' ) - inputs: - targetType: inline - pwsh: false - script: ${{ parameters.assignBuildVersionScript }} - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - DEF_ID: $(System.DefinitionId) - MAJOR_MINOR: ${{ parameters.majorMinor }} - -# Linux/macOS -- task: PowerShell@2 - displayName: "Assign build version" - condition: ne( variables['Agent.OS'], 'Windows_NT' ) - inputs: - targetType: inline - pwsh: true - script: ${{ parameters.assignBuildVersionScript }} - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - DEF_ID: $(System.DefinitionId) - MAJOR_MINOR: ${{ parameters.majorMinor }} diff --git a/plan/feature.md b/plan/feature.md new file mode 100644 index 0000000..bc8c1ef --- /dev/null +++ b/plan/feature.md @@ -0,0 +1,34 @@ +# Feature: Migrate Tharga.Test to GitHub Actions CI/CD + +## Source +Request: `Tharga.Test` under "GitHub Actions CI/CD" in `$DOC_ROOT/Tharga/Requests.md` (Internal, 2026-04-06, Priority Medium, Status Pending). + +## Goal +Replace the existing Azure DevOps pipeline with a GitHub Actions workflow that builds, tests, scans (CodeQL), packs, and publishes `Tharga.Test.Toolkit` to NuGet.org and creates GitHub Releases — matching the canonical Tharga.Crawler reference implementation. + +## Scope +- Add `.github/workflows/build.yml` mirroring `c:\dev\tharga\Toolkit\Crawler\.github\workflows\build.yml` +- Customize per project: + - `MAJOR_MINOR: '1.14'` (next stable: `1.14.8`) + - Pack only `Tharga.Test.Toolkit/Tharga.Test.Toolkit.csproj` + - .NET SDKs `8.0.x`, `9.0.x`, `10.0.x` + - Default warning threshold (15) + - No test filter (no test projects exist in this repo) +- Remove obsolete `azure-pipelines.yml` and `buildnumber.yml` +- Update `Tharga.Test.sln` to drop those entries from `Solution Items` + +## Out of scope (manual follow-ups in GitHub / Azure DevOps UI) +- Configure `NUGET_API_KEY` secret +- Configure `release` and `prerelease` environments +- Disable the old Azure DevOps build pipeline +- Delete the `develop` branch after PR merge + +## Acceptance criteria +- `feature/ci-cd-github-actions` branch contains the workflow, plan files, and removed AzDo files +- `dotnet build -c Release` and `dotnet test -c Release` still pass locally +- PR opened against `master` +- Workflow runs successfully on the PR (build + security + prerelease jobs) +- After merge: a stable release `1.14.8` is published to NuGet via the workflow + +## Done condition +User has confirmed the workflow runs green on a PR and validates the migration. diff --git a/plan/plan.md b/plan/plan.md new file mode 100644 index 0000000..878c2d8 --- /dev/null +++ b/plan/plan.md @@ -0,0 +1,25 @@ +# Plan: GitHub Actions CI/CD migration + +- [x] 1. Create feature branch `feature/ci-cd-github-actions` from `master` +- [x] 2. Create `plan/feature.md` and `plan/plan.md` +- [x] 3. Add `.github/workflows/build.yml` (Crawler-derived; MAJOR_MINOR=1.14, packs Tharga.Test.Toolkit) +- [x] 4. Delete `azure-pipelines.yml` and `buildnumber.yml` +- [x] 5. Update `Tharga.Test.sln` Solution Items section +- [x] 6. Verify local build + test still pass — `dotnet build -c Release` clean (0 warnings), `dotnet test -c Release` exits 0 with no test projects +- [~] 7. Commit changes +- [ ] 8. Push branch and open PR to `master` +- [ ] 9. Update Requests.md status to Done after PR is merged (separate session, post-merge) + +## Last session +Migrated Tharga.Test from Azure DevOps to GitHub Actions: +- Workflow mirrors `Tharga.Crawler` reference, customized to MAJOR_MINOR `1.14` and the single pack target `Tharga.Test.Toolkit/Tharga.Test.Toolkit.csproj`. No `--filter` since there are no test projects in the repo. +- Removed obsolete `azure-pipelines.yml` + `buildnumber.yml` and cleaned them out of the `Solution Items` section in `Tharga.Test.sln`. +- Local build clean, `dotnet test` no-op succeeds. + +Manual follow-ups required in GitHub / AzDo UI before the workflow can publish: +- Configure `NUGET_API_KEY` secret +- Configure `release` and `prerelease` environments +- Disable the old Azure DevOps pipeline +- Delete the `develop` branch after PR merge + +README.md does not need changes — the workflow is infrastructure, not consumer-facing API. From 81d896e6860cadf5c016a41620e44835d7cb06e6 Mon Sep 17 00:00:00 2001 From: "daniel.bohlin@live.se" Date: Sat, 2 May 2026 16:18:55 +0200 Subject: [PATCH 2/2] feat: ci-cd-github-actions complete --- plan/feature.md | 34 ---------------------------------- plan/plan.md | 25 ------------------------- 2 files changed, 59 deletions(-) delete mode 100644 plan/feature.md delete mode 100644 plan/plan.md diff --git a/plan/feature.md b/plan/feature.md deleted file mode 100644 index bc8c1ef..0000000 --- a/plan/feature.md +++ /dev/null @@ -1,34 +0,0 @@ -# Feature: Migrate Tharga.Test to GitHub Actions CI/CD - -## Source -Request: `Tharga.Test` under "GitHub Actions CI/CD" in `$DOC_ROOT/Tharga/Requests.md` (Internal, 2026-04-06, Priority Medium, Status Pending). - -## Goal -Replace the existing Azure DevOps pipeline with a GitHub Actions workflow that builds, tests, scans (CodeQL), packs, and publishes `Tharga.Test.Toolkit` to NuGet.org and creates GitHub Releases — matching the canonical Tharga.Crawler reference implementation. - -## Scope -- Add `.github/workflows/build.yml` mirroring `c:\dev\tharga\Toolkit\Crawler\.github\workflows\build.yml` -- Customize per project: - - `MAJOR_MINOR: '1.14'` (next stable: `1.14.8`) - - Pack only `Tharga.Test.Toolkit/Tharga.Test.Toolkit.csproj` - - .NET SDKs `8.0.x`, `9.0.x`, `10.0.x` - - Default warning threshold (15) - - No test filter (no test projects exist in this repo) -- Remove obsolete `azure-pipelines.yml` and `buildnumber.yml` -- Update `Tharga.Test.sln` to drop those entries from `Solution Items` - -## Out of scope (manual follow-ups in GitHub / Azure DevOps UI) -- Configure `NUGET_API_KEY` secret -- Configure `release` and `prerelease` environments -- Disable the old Azure DevOps build pipeline -- Delete the `develop` branch after PR merge - -## Acceptance criteria -- `feature/ci-cd-github-actions` branch contains the workflow, plan files, and removed AzDo files -- `dotnet build -c Release` and `dotnet test -c Release` still pass locally -- PR opened against `master` -- Workflow runs successfully on the PR (build + security + prerelease jobs) -- After merge: a stable release `1.14.8` is published to NuGet via the workflow - -## Done condition -User has confirmed the workflow runs green on a PR and validates the migration. diff --git a/plan/plan.md b/plan/plan.md deleted file mode 100644 index 878c2d8..0000000 --- a/plan/plan.md +++ /dev/null @@ -1,25 +0,0 @@ -# Plan: GitHub Actions CI/CD migration - -- [x] 1. Create feature branch `feature/ci-cd-github-actions` from `master` -- [x] 2. Create `plan/feature.md` and `plan/plan.md` -- [x] 3. Add `.github/workflows/build.yml` (Crawler-derived; MAJOR_MINOR=1.14, packs Tharga.Test.Toolkit) -- [x] 4. Delete `azure-pipelines.yml` and `buildnumber.yml` -- [x] 5. Update `Tharga.Test.sln` Solution Items section -- [x] 6. Verify local build + test still pass — `dotnet build -c Release` clean (0 warnings), `dotnet test -c Release` exits 0 with no test projects -- [~] 7. Commit changes -- [ ] 8. Push branch and open PR to `master` -- [ ] 9. Update Requests.md status to Done after PR is merged (separate session, post-merge) - -## Last session -Migrated Tharga.Test from Azure DevOps to GitHub Actions: -- Workflow mirrors `Tharga.Crawler` reference, customized to MAJOR_MINOR `1.14` and the single pack target `Tharga.Test.Toolkit/Tharga.Test.Toolkit.csproj`. No `--filter` since there are no test projects in the repo. -- Removed obsolete `azure-pipelines.yml` + `buildnumber.yml` and cleaned them out of the `Solution Items` section in `Tharga.Test.sln`. -- Local build clean, `dotnet test` no-op succeeds. - -Manual follow-ups required in GitHub / AzDo UI before the workflow can publish: -- Configure `NUGET_API_KEY` secret -- Configure `release` and `prerelease` environments -- Disable the old Azure DevOps pipeline -- Delete the `develop` branch after PR merge - -README.md does not need changes — the workflow is infrastructure, not consumer-facing API.