diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..35b715b --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,149 @@ +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 .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 /p:RestoreDotNetToolsOnBuild=false + + 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 .NET tools + run: dotnet tool restore + + - 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:RestoreDotNetToolsOnBuild=false + /p:Version=${{ steps.package-version.outputs.version }} + + - name: Pack + run: > + dotnet pack "${{ env.PACKAGE_PROJECT }}" + --configuration Release + --no-build + --output artifacts + /p:RestoreDotNetToolsOnBuild=false + /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 }}" 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 e9dc3ed..1bc7d5f 100644 --- a/README.md +++ b/README.md @@ -400,14 +400,14 @@ 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 ./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 @@ - +