diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4f4a3d..e6b8733 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,8 +5,13 @@ on: branches: - master tags: - - '*' + - 'v*' pull_request: + workflow_dispatch: + inputs: + version: + description: 'UPM version to build (without leading v), e.g. 3.0.0. Only used by the upm job; the main build runs on the current ref.' + required: false permissions: contents: write @@ -14,6 +19,9 @@ permissions: jobs: build: runs-on: ubuntu-latest + permissions: + contents: write # upload nupkgs to the draft release + id-token: write # mint GitHub OIDC token for NuGet trusted publishing steps: - name: Checkout uses: actions/checkout@v4 @@ -52,8 +60,173 @@ jobs: - name: Build NuGet Packages run: dotnet pack ./src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Enjin.Platform.Sdk.csproj --configuration Release --no-build --output ./src/Enjin.Platform.Sdk/nuget-packages/ - - name: Upload Artifacts + - name: Upload nupkgs to draft release uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/v') with: + # Keep the release in draft mode until the publish-release job flips + # it. softprops/action-gh-release upserts the release, so uploads from + # multiple jobs/workflows on the same tag all attach to the same + # draft. This avoids the "immutable release" upload error that + # happens when assets are pushed after publication. + draft: true files: ./src/Enjin.Platform.Sdk/nuget-packages/*.nupkg + + - name: NuGet login (OIDC -> temp API key) + id: nuget-login + if: startsWith(github.ref, 'refs/tags/v') + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} + + - name: Publish to NuGet.org + if: startsWith(github.ref, 'refs/tags/v') + run: | + dotnet nuget push ./src/Enjin.Platform.Sdk/nuget-packages/*.nupkg \ + --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate + + upm: + # Assembles the Unity Package Manager distribution from unity-template/ + + # the freshly-built DLL and publishes it to the dedicated + # `enjin/platform-unity-sdk` repo: the package tree is committed to that + # repo's `master` branch (latest, always browsable) and pinned with a + # `v` git tag. Users reference either in their Unity Package + # Manager git URL: + # https://github.com/enjin/platform-unity-sdk.git (latest) + # https://github.com/enjin/platform-unity-sdk.git#v3.0.0 (pinned) + # The tarball is still attached to this repo's draft GitHub Release. + needs: build + # Run on v* tag pushes, or on manual dispatches that supply a version. A + # workflow_dispatch with an empty version input is treated as "main build + # only" and skips the UPM job rather than hard-failing. + if: | + startsWith(github.ref, 'refs/tags/v') + || (github.event_name == 'workflow_dispatch' && github.event.inputs.version != '') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine version + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ github.event.inputs.version }}" + if [[ -z "${VERSION}" ]]; then + echo "::error::workflow_dispatch requires the 'version' input" + exit 1 + fi + TAG="v${VERSION}" + elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + # strip leading 'v' from refs/tags/v3.0.0 + VERSION="${GITHUB_REF_NAME#v}" + TAG="v${VERSION}" + else + echo "::error::upm job triggered on an unsupported ref ${GITHUB_REF}" + exit 1 + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "Building UPM package for version ${VERSION} (tag ${TAG})" + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Cache NuGet Packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Build SDK (Release) + run: dotnet build ./src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Enjin.Platform.Sdk.csproj --configuration Release + + - name: Assemble UPM package + run: ./scripts/assemble-upm.sh ${{ steps.version.outputs.version }} + + - name: Setup SSH + # Loads the deploy-key for enjin/platform-unity-sdk so the publish step + # below can push over SSH. webfactory/ssh-agent also adds github.com to + # known_hosts. The matching public key must be registered as a + # write-enabled deploy key on enjin/platform-unity-sdk. + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.UNITY_SDK_DEPLOY_KEY }} + + - name: Publish package to platform-unity-sdk + # Commits the assembled upm-staging/ tree to the dedicated repo's master + # branch (latest) and pins it with a v tag. End users reference + # either in their Unity Package Manager git URL: + # https://github.com/enjin/platform-unity-sdk.git (latest) + # https://github.com/enjin/platform-unity-sdk.git#v3.0.0 (pinned) + # The package lives at the repo root, so no ?path= suffix is needed. + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + TAG="${{ steps.version.outputs.tag }}" + BRANCH="master" + + WORK="$(mktemp -d)" + git clone --depth 1 git@github.com:enjin/platform-unity-sdk.git "${WORK}" + ( + cd "${WORK}" + git checkout -B "${BRANCH}" + # Replace tracked contents with the freshly assembled package. + git rm -rqf . >/dev/null 2>&1 || true + cp -R "${GITHUB_WORKSPACE}/upm-staging/." . + git add -A + # Skip the commit when the assembled package is identical to the + # current branch contents (e.g. re-running a release tag), so the + # job stays idempotent instead of failing on "nothing to commit". + if git diff --cached --quiet; then + echo "UPM package contents unchanged; skipping commit" + else + git -c user.name="github-actions[bot]" \ + -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ + commit -m "UPM package v${VERSION}" + fi + git tag -f "${TAG}" + git push origin "HEAD:${BRANCH}" + git push -f origin "refs/tags/${TAG}" + ) + rm -rf "${WORK}" + echo "Published v${VERSION} to platform-unity-sdk (${BRANCH} + tag ${TAG})" + + - name: Attach UPM tarball to draft release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/v') + with: + draft: true + files: EnjinPlatformSdk-v${{ steps.version.outputs.version }}-upm.tar.gz + + - name: Upload tarball as workflow artifact + # Always upload (including workflow_dispatch runs) so manual builds are + # downloadable for inspection without needing a release. + uses: actions/upload-artifact@v4 + with: + name: EnjinPlatformSdk-v${{ steps.version.outputs.version }}-upm + path: EnjinPlatformSdk-v${{ steps.version.outputs.version }}-upm.tar.gz + + publish-release: + # Flip the draft release to published once both the nupkgs and UPM tarball + # have been attached. This step is what makes the release "go live" on the + # repo's Releases page and on the GitHub Releases API. + needs: [build, upm] + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + echo "Publishing release ${TAG}" + gh release edit "${TAG}" --draft=false diff --git a/.github/workflows/unity.yml b/.github/workflows/unity.yml deleted file mode 100644 index 73788e5..0000000 --- a/.github/workflows/unity.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Unity Package - -# Triggered on version-tag pushes (e.g. v3.0.0). Assembles the UPM distribution -# from unity-template/ + the freshly-built DLL, pushes it to a parallel -# `upm/` git tag (which is what users reference in their Unity -# Package Manager git URL), and attaches the tarball to the GitHub Release. - -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - version: - description: 'Version to build (without leading v), e.g. 3.0.0' - required: true - -permissions: - contents: write - -jobs: - upm: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # Need full history + a writable token so we can push the upm/ tag. - fetch-depth: 0 - persist-credentials: true - - - name: Determine version - id: version - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - VERSION="${{ github.event.inputs.version }}" - else - # strip leading 'v' from refs/tags/v3.0.0 - VERSION="${GITHUB_REF_NAME#v}" - fi - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "Building UPM package for version ${VERSION}" - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - - name: Cache NuGet Packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Build SDK (Release) - run: dotnet build ./src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Enjin.Platform.Sdk.csproj --configuration Release - - - name: Assemble UPM package - run: ./scripts/assemble-upm.sh ${{ steps.version.outputs.version }} - - - name: Push upm/ tag - # This is what end users reference in their Unity Package Manager git URL: - # https://github.com/enjin/platform-csharp-sdk.git#upm/v3.0.0 - # The tag points at an orphan commit whose tree is exactly the contents - # of upm-staging/, so Unity's git fetcher sees a valid UPM package at - # the repo root (without any ?path= suffix needed). - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - VERSION="${{ steps.version.outputs.version }}" - UPM_TAG="upm/v${VERSION}" - - WORK="$(mktemp -d)" - cp -R upm-staging/. "${WORK}/" - ( - cd "${WORK}" - git init -q -b "${UPM_TAG}" - git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - git add -A - git -c user.name="github-actions[bot]" \ - -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ - commit -q -m "UPM package v${VERSION}" - git tag -f "${UPM_TAG}" - git push -f origin "refs/tags/${UPM_TAG}" - ) - rm -rf "${WORK}" - echo "Pushed tag ${UPM_TAG}" - - - name: Attach tarball to GitHub Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/v') - with: - files: EnjinPlatformSdk-v${{ steps.version.outputs.version }}-upm.tar.gz - - - name: Upload tarball as workflow artifact - # Always upload (including workflow_dispatch runs) so manual builds are - # downloadable for inspection without needing a release. - uses: actions/upload-artifact@v4 - with: - name: EnjinPlatformSdk-v${{ steps.version.outputs.version }}-upm - path: EnjinPlatformSdk-v${{ steps.version.outputs.version }}-upm.tar.gz diff --git a/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk.Tests/Unit/Platform/PlatformClientSmokeTest.cs b/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk.Tests/Unit/Platform/PlatformClientSmokeTest.cs index c58dc1f..821f3c4 100644 --- a/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk.Tests/Unit/Platform/PlatformClientSmokeTest.cs +++ b/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk.Tests/Unit/Platform/PlatformClientSmokeTest.cs @@ -211,7 +211,7 @@ public void DefaultUserAgentIsDerivedFromAssemblyVersion() version = version[1..]; } - var expected = $"Enjin.Platform.Sdk/{version}"; + var expected = $"Enjin-Platform-CSharp-SDK/{version}"; // Act using var client = new PlatformClient(new Uri(_server.Urls[0] + "/graphql")); @@ -219,4 +219,24 @@ public void DefaultUserAgentIsDerivedFromAssemblyVersion() // Assert Assert.That(client.UserAgent, Is.EqualTo(expected)); } + + [Test] + public void ParameterlessConstructorUsesDefaultBaseAddress() + { + // Act + using var client = new PlatformClient(); + + // Assert + Assert.That(client.BaseAddress, Is.EqualTo(PlatformClient.DefaultBaseAddress)); + } + + [Test] + public void NullBaseAddressUsesDefaultBaseAddress() + { + // Act + using var client = new PlatformClient(null); + + // Assert + Assert.That(client.BaseAddress, Is.EqualTo(PlatformClient.DefaultBaseAddress)); + } } diff --git a/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Platform/PlatformClient.cs b/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Platform/PlatformClient.cs index faa9a50..a861ced 100644 --- a/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Platform/PlatformClient.cs +++ b/src/Enjin.Platform.Sdk/Enjin.Platform.Sdk/Platform/PlatformClient.cs @@ -15,6 +15,11 @@ namespace Enjin.Platform.Sdk; [PublicAPI] public sealed class PlatformClient : IPlatformClient { + /// + /// The default base address of the Enjin Platform GraphQL endpoint. + /// + public static readonly Uri DefaultBaseAddress = new("https://platform.enjin.io/graphql"); + private static readonly string DefaultUserAgent = BuildDefaultUserAgent(); private static string BuildDefaultUserAgent() @@ -38,7 +43,7 @@ private static string BuildDefaultUserAgent() version = version[1..]; } - return $"Enjin.Platform.Sdk/{version}"; + return $"Enjin-Platform-CSharp-SDK/{version}"; } private readonly HttpClient _httpClient; @@ -58,21 +63,18 @@ private static string BuildDefaultUserAgent() /// /// Initializes a new . /// - /// The base address of the platform's GraphQL endpoint (e.g. https://platform.enjin.io/graphql). - /// Optional User-Agent header value. Defaults to Enjin.Platform.Sdk/{assembly-version}. + /// The base address of the platform's GraphQL endpoint (e.g. https://platform.enjin.io/graphql). Defaults to . + /// Optional User-Agent header value. Defaults to Enjin-Platform-CSharp-SDK/{assembly-version}. /// Optional logger; when provided HTTP traffic is logged at the given . /// HTTP log level. Ignored when is null. public PlatformClient( - Uri baseAddress, + Uri? baseAddress = null, string? userAgent = null, ILogger? logger = null, HttpLogLevel httpLogLevel = HttpLogLevel.None ) { - if (baseAddress == null) - { - throw new ArgumentNullException(nameof(baseAddress)); - } + baseAddress ??= DefaultBaseAddress; UserAgent = userAgent ?? DefaultUserAgent;