From 808e61518cc597bfb5aa3b6cf0ff51c8ce17fe9e Mon Sep 17 00:00:00 2001 From: Spruce Weber-Milne Date: Fri, 14 Aug 2026 10:56:57 -0700 Subject: [PATCH 1/3] Add NuGet publish workflow and package metadata updates --- .github/workflows/ci-cd.yml | 142 +++++++++++++++++++ README.md | 6 +- UCD.Rosetta.Client/UCD.Rosetta.Client.csproj | 6 +- 3 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..537c346 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,142 @@ +name: CI/CD + +on: + pull_request: + branches: + - main + push: + branches: + - main + paths: + - 'UCD.Rosetta.Client/**' + - 'specs/**' + - 'Directory.Build.targets' + - '.config/dotnet-tools.json' + - 'UCD.Rosetta.sln' + - 'LICENSE' + +permissions: + contents: read + +env: + DOTNET_VERSION: 8.0.x + SOLUTION_PATH: UCD.Rosetta.sln + PACKAGE_PROJECT: UCD.Rosetta.Client/UCD.Rosetta.Client.csproj + PACKAGE_ID: UCD.Rosetta.Client + NUGET_SOURCE: https://api.nuget.org/v3/index.json + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore + run: dotnet restore "${{ env.SOLUTION_PATH }}" + + - name: Build + run: dotnet build "${{ env.SOLUTION_PATH }}" --configuration Release --no-restore + + publish: + name: Publish to NuGet + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + concurrency: + group: nuget-publish-main + cancel-in-progress: false + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore + run: dotnet restore "${{ env.SOLUTION_PATH }}" + + - name: Compute package version + id: package-version + shell: python + run: | + import json + import os + import re + import sys + import urllib.error + import urllib.request + import xml.etree.ElementTree as ET + + package_project = os.environ["PACKAGE_PROJECT"] + package_id = os.environ["PACKAGE_ID"] + + version = ET.parse(package_project).findtext("./PropertyGroup/Version") + if not version: + raise SystemExit(f"No found in {package_project}") + + match = re.fullmatch(r"(\d+)\.(\d+)(?:\.(\d+))?(?:[-+].*)?", version.strip()) + if not match: + raise SystemExit(f"Project version must start with major.minor[.patch], got: {version}") + + major, minor = match.group(1), match.group(2) + prefix = f"{major}.{minor}." + published_patches = [] + index_url = f"https://api.nuget.org/v3-flatcontainer/{package_id.lower()}/index.json" + + try: + with urllib.request.urlopen(index_url, timeout=30) as response: + versions = json.load(response).get("versions", []) + except urllib.error.HTTPError as ex: + if ex.code == 404: + versions = [] + else: + raise + + for published_version in versions: + if not published_version.startswith(prefix): + continue + + patch_part = published_version[len(prefix):] + if re.fullmatch(r"\d+", patch_part): + published_patches.append(int(patch_part)) + + next_patch = max(published_patches, default=-1) + 1 + next_version = f"{major}.{minor}.{next_patch}" + + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"version={next_version}\n") + + print(f"Package version: {next_version}") + + - name: Build + run: > + dotnet build "${{ env.SOLUTION_PATH }}" + --configuration Release + --no-restore + /p:Version=${{ steps.package-version.outputs.version }} + + - name: Pack + run: > + dotnet pack "${{ env.PACKAGE_PROJECT }}" + --configuration Release + --no-build + --output artifacts + /p:Version=${{ steps.package-version.outputs.version }} + + - name: Publish + run: > + dotnet nuget push "artifacts/*.nupkg" + --api-key "${{ secrets.NUGET_API_KEY }}" + --source "${{ env.NUGET_SOURCE }}" + --skip-duplicate diff --git a/README.md b/README.md index e9dc3ed..28473c3 100644 --- a/README.md +++ b/README.md @@ -407,7 +407,7 @@ To update both specs to a new API version, use the convenience script: ./update-spec.sh # e.g. 1.0.33 ``` -To find the latest version number: open the [Rosetta API Exchange page](https://anypoint.mulesoft.com/exchange/portals/university-of-california-346/9b04bfa8-6eeb-4d85-b676-91db930f8411/iam-rosetta-api/), open the **Download** dropdown, and hover over any link — the version appears in the URL shown in the browser status bar. +The [Rosetta API Anypoint Exchange page](https://anypoint.mulesoft.com/exchange/portals/university-of-california-346/9b04bfa8-6eeb-4d85-b676-91db930f8411/iam-rosetta-api/) is the source of truth for the API. To find the latest version number, open the **Download** dropdown and hover over any link — the version appears in the URL shown in the browser status bar. The script downloads the spec from MuleSoft Exchange, extracts the embedded GraphQL SDL into `specs/rosetta-api.graphql` (appending the `schema { query: Query }` root required by ZeroQL), and updates the README version badge. Then rebuild: ```bash @@ -448,7 +448,7 @@ Running integration tests: - The integration test project `IntegrationTests` uses the same `.env` file as the example project. - Tests also read environment variables, which is useful for CI. The supported environment variable names use double underscores for hierarchy (for example: `RosettaClient__ClientId` and `RosettaClient__ClientSecret`). -- See [IntegrationTests/README.md](IntegrationTests/README.md) for more details. +- See [IntegrationTests/README.md](https://github.com/ucdavis/Rosetta-API-Client-DotNet/blob/main/IntegrationTests/README.md) for more details. Run tests locally with: @@ -487,6 +487,6 @@ MIT License - See LICENSE file for details ## Support For issues, questions, or contributions: -- **Issues**: [GitHub Issues](https://github.com/ucdavis/UCD.Rosetta.Client/issues) +- **Issues**: [GitHub Issues](https://github.com/ucdavis/Rosetta-API-Client-DotNet/issues) - **Documentation**: [Rosetta API Docs](https://anypoint.mulesoft.com/exchange/portals/university-of-california-346/9b04bfa8-6eeb-4d85-b676-91db930f8411/iam-rosetta-api/) - **UC Davis IAM Team**: Contact your IAM representative for credentials and API access diff --git a/UCD.Rosetta.Client/UCD.Rosetta.Client.csproj b/UCD.Rosetta.Client/UCD.Rosetta.Client.csproj index 7eea0fe..3c6916e 100644 --- a/UCD.Rosetta.Client/UCD.Rosetta.Client.csproj +++ b/UCD.Rosetta.Client/UCD.Rosetta.Client.csproj @@ -13,8 +13,8 @@ University of California, Davis Official .NET client library for the UC Davis IAM Rosetta API. Provides easy access to identity and access management data from UC Davis IAM services. ucdavis;iam;identity;rosetta;api-client - https://github.com/ucdavis/UCD.Rosetta.Client - https://github.com/ucdavis/UCD.Rosetta.Client + https://github.com/ucdavis/Rosetta-API-Client-DotNet + https://github.com/ucdavis/Rosetta-API-Client-DotNet git MIT README.md @@ -36,7 +36,7 @@ - + From 4c55360bdf9b9eb90e0252ea8ba21527fd93ade4 Mon Sep 17 00:00:00 2001 From: Spruce Weber-Milne Date: Fri, 14 Aug 2026 11:17:57 -0700 Subject: [PATCH 2/3] Fix CI tool restore --- .github/workflows/ci-cd.yml | 10 +++++++++- Directory.Build.targets | 9 +++++++-- README.md | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 537c346..8932fb6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -39,11 +39,14 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Restore .NET tools + run: dotnet tool restore + - name: Restore run: dotnet restore "${{ env.SOLUTION_PATH }}" - name: Build - run: dotnet build "${{ env.SOLUTION_PATH }}" --configuration Release --no-restore + run: dotnet build "${{ env.SOLUTION_PATH }}" --configuration Release --no-restore /p:RestoreDotNetToolsOnBuild=false publish: name: Publish to NuGet @@ -63,6 +66,9 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Restore .NET tools + run: dotnet tool restore + - name: Restore run: dotnet restore "${{ env.SOLUTION_PATH }}" @@ -124,6 +130,7 @@ jobs: dotnet build "${{ env.SOLUTION_PATH }}" --configuration Release --no-restore + /p:RestoreDotNetToolsOnBuild=false /p:Version=${{ steps.package-version.outputs.version }} - name: Pack @@ -132,6 +139,7 @@ jobs: --configuration Release --no-build --output artifacts + /p:RestoreDotNetToolsOnBuild=false /p:Version=${{ steps.package-version.outputs.version }} - name: Publish diff --git a/Directory.Build.targets b/Directory.Build.targets index f99dd1e..f3a7a05 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,6 +1,11 @@ - - + + false + true + + + + diff --git a/README.md b/README.md index 28473c3..1bc7d5f 100644 --- a/README.md +++ b/README.md @@ -400,7 +400,7 @@ The REST client (`RosettaApiClient.g.cs`) is regenerated from the OpenAPI spec b The GraphQL client (`obj/ZeroQL/rosetta.zeroql.json.g.cs`) is regenerated from `specs/rosetta-api.graphql` by ZeroQL on every build. -> **Note:** ZeroQL codegen runs via a local .NET tool declared in `.config/dotnet-tools.json`. A `Directory.Build.targets` at the repo root automatically runs `dotnet tool restore` before every build, so no manual setup is required after cloning. +> **Note:** ZeroQL codegen runs via a local .NET tool declared in `.config/dotnet-tools.json`. A `Directory.Build.targets` at the repo root automatically runs `dotnet tool restore` before local builds, so no manual setup is required after cloning. CI restores tools explicitly once per job to avoid concurrent tool restore races during solution builds. To update both specs to a new API version, use the convenience script: ```bash From a78aa58b335f241c886c4587491a1bc187d19a6d Mon Sep 17 00:00:00 2001 From: Spruce Weber-Milne Date: Fri, 14 Aug 2026 11:28:19 -0700 Subject: [PATCH 3/3] Fail on publish duplicate --- .github/workflows/ci-cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 8932fb6..35b715b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -147,4 +147,3 @@ jobs: dotnet nuget push "artifacts/*.nupkg" --api-key "${{ secrets.NUGET_API_KEY }}" --source "${{ env.NUGET_SOURCE }}" - --skip-duplicate