From db273fbd3273b0a339866ee498dae4b5bb25d50a Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:54:31 +0900 Subject: [PATCH] ci(release): sign OpenUPM packages with Unity UPM CLI Why OpenUPM must preserve the publisher's signing attestation instead of repacking Git source. What Changed Pack and sign the exact release commit, verify its archive, and attach it to the draft Release. Trigger and confirm signed OpenUPM publication after the Release is published. Check credentials and registry tracking before preparing a version. Impact Releases require Unity service-account secrets and the companion OpenUPM metadata migration. The existing Editor build, unitypackage export, and draft review flow are preserved. --- .github/RELEASING.md | 37 ++++++++++++ .github/scripts/sign-upm.sh | 44 ++++++++++++++ .github/workflows/release.yaml | 106 +++++++++++++++++++++++++++++++-- 3 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 .github/RELEASING.md create mode 100644 .github/scripts/sign-upm.sh diff --git a/.github/RELEASING.md b/.github/RELEASING.md new file mode 100644 index 0000000..4bb1f76 --- /dev/null +++ b/.github/RELEASING.md @@ -0,0 +1,37 @@ +# Releasing + +## One-time signing setup + +Create a service account in the Unity organization that should sign the package. +Give it the **Package Manager Package Signer** organization role, then add these +repository secrets: + +- `UPM_SERVICE_ACCOUNT_KEY_ID` +- `UPM_SERVICE_ACCOUNT_KEY_SECRET` +- `UPM_ORG_ID` + +See [Unity's UPM CLI setup](https://docs.unity3d.com/6000.6/Documentation/Manual/upm-cli-install.html#prereqs). +The existing `UNITY_EMAIL` and `UNITY_PASSWORD` secrets remain the credentials +for the Editor license used by the example build and `.unitypackage` export. + +Before the next version tag is created, merge these settings into the package's +[OpenUPM metadata](https://github.com/openupm/openupm/blob/master/data/packages/com.mackysoft.serializereference-extensions.yml): + +```yaml +trackingMode: githubRelease +githubReleaseAssetName: com.mackysoft.serializereference-extensions- +``` + +This order prevents OpenUPM from publishing an unsigned archive packed from Git. +Existing published versions remain unchanged. See [OpenUPM's signing workflow](https://openupm.com/docs/signing-upm-packages). + +## Release a version + +1. Run **Release** from a branch with the new SemVer tag. It updates `package.json`, + builds the example, exports the `.unitypackage`, and signs the UPM `.tgz` from + the same commit. Signing must succeed before the tag and draft Release are created. +2. Review the draft's release notes and attached packages, then publish it. +3. The **Release / publish-openupm** job requests publication through GitHub OIDC + and checks that OpenUPM reports the version as signed. No OpenUPM token is needed. + +Keep the signed `.tgz` attached to the public Release so OpenUPM can retrieve it. diff --git a/.github/scripts/sign-upm.sh b/.github/scripts/sign-upm.sh new file mode 100644 index 0000000..13fdb4b --- /dev/null +++ b/.github/scripts/sign-upm.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +package_dir="${1:?Expected package directory}" +destination="${2:?Expected output directory}" +: "${VERSION:?Expected release version}" +: "${UPM_ORG_ID:?Expected signing organization ID}" + +package_name="$(jq -er '.name' "$package_dir/package.json")" +package_version="$(jq -er '.version' "$package_dir/package.json")" +if [[ "$package_version" != "$VERSION" ]]; then + echo "::error::UPM package version does not match the release version." + exit 1 +fi + +upm pack "$package_dir" --organization-id "$UPM_ORG_ID" --destination "$destination" + +shopt -s nullglob +archives=("$destination"/*.tgz "$destination"/*.tar.gz) +if [[ "${#archives[@]}" != 1 ]]; then + echo "::error::Expected exactly one signed UPM archive." + exit 1 +fi +archive="${archives[0]}" + +if ! tar -xOzf "$archive" package/package.json | jq --exit-status \ + --arg name "$package_name" --arg version "$VERSION" \ + '.name == $name and .version == $version' > /dev/null; then + echo "::error::Signed UPM archive has an unexpected package name or version." + exit 1 +fi + +signature_size="$(tar -xOzf "$archive" package/.attestation.p7m | wc -c)" +if [[ "$signature_size" -eq 0 ]]; then + echo "::error::UPM archive has no signing attestation." + exit 1 +fi + +# Rename only; repacking would change the archive produced by Unity's signer. +release_archive="$destination/$package_name-$VERSION.tgz" +if [[ "$archive" != "$release_archive" ]]; then + mv -- "$archive" "$release_archive" +fi +printf 'Signed UPM package: %s\n' "$release_archive" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ce82d78..accbace 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,8 @@ on: description: Package version and Git tag (for example 1.7.1). required: true type: string + release: + types: [published] concurrency: group: release-${{ github.ref }} @@ -17,6 +19,7 @@ permissions: jobs: prepare: + if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-24.04 env: TARGET_FILE: ./Assets/MackySoft/MackySoft.SerializeReferenceExtensions/package.json @@ -24,6 +27,32 @@ jobs: sha: ${{ steps.metadata.outputs.sha }} version: ${{ steps.metadata.outputs.version }} steps: + - name: Check signing credentials + env: + UPM_SERVICE_ACCOUNT_KEY_ID: ${{ secrets.UPM_SERVICE_ACCOUNT_KEY_ID }} + UPM_SERVICE_ACCOUNT_KEY_SECRET: ${{ secrets.UPM_SERVICE_ACCOUNT_KEY_SECRET }} + UPM_ORG_ID: ${{ secrets.UPM_ORG_ID }} + run: | + for setting in UPM_SERVICE_ACCOUNT_KEY_ID UPM_SERVICE_ACCOUNT_KEY_SECRET UPM_ORG_ID; do + if [[ -z "${!setting}" ]]; then + echo "::error::Repository secret $setting is required for signed OpenUPM releases." + exit 1 + fi + done + + # The Git tracker must stop packing unsigned source before a new tag exists. + - name: Require OpenUPM release asset tracking + run: | + curl --fail --silent --show-error --location \ + https://raw.githubusercontent.com/openupm/openupm/master/data/packages/com.mackysoft.serializereference-extensions.yml \ + --output "$RUNNER_TEMP/openupm-package.yml" + if ! yq --exit-status \ + '.trackingMode == "githubRelease" and .githubReleaseAssetName == "com.mackysoft.serializereference-extensions-"' \ + "$RUNNER_TEMP/openupm-package.yml" > /dev/null; then + echo "::error::Merge the OpenUPM metadata change to signed GitHub Release assets before releasing." + exit 1 + fi + - name: Create release app token id: release_app uses: actions/create-github-app-token@v3 @@ -55,6 +84,42 @@ jobs: RELEASE_REF: ${{ github.ref }} run: bash .github/scripts/release.sh prepare + sign-upm: + needs: prepare + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Install Unity UPM CLI + run: | + curl --fail --silent --show-error --location \ + https://cdn.packages.unity.com/upm-cli/install.sh \ + --output "$RUNNER_TEMP/install-upm.sh" + bash "$RUNNER_TEMP/install-upm.sh" v9.35.0 + echo "$HOME/.upm/bin" >> "$GITHUB_PATH" + "$HOME/.upm/bin/upm" --version + + - name: Pack and sign UPM package + env: + VERSION: ${{ needs.prepare.outputs.version }} + UPM_SERVICE_ACCOUNT_KEY_ID: ${{ secrets.UPM_SERVICE_ACCOUNT_KEY_ID }} + UPM_SERVICE_ACCOUNT_KEY_SECRET: ${{ secrets.UPM_SERVICE_ACCOUNT_KEY_SECRET }} + UPM_ORG_ID: ${{ secrets.UPM_ORG_ID }} + run: | + bash .github/scripts/sign-upm.sh \ + Assets/MackySoft/MackySoft.SerializeReferenceExtensions \ + "$RUNNER_TEMP/signed-upm" + + - name: Upload signed UPM package + uses: actions/upload-artifact@v7 + with: + name: Signed-UPM + path: ${{ runner.temp }}/signed-upm/*.tgz + if-no-files-found: error + build: needs: prepare runs-on: ubuntu-22.04 @@ -131,7 +196,7 @@ jobs: path: Logs/ create-release: - needs: [prepare, build] + needs: [prepare, build, sign-upm] runs-on: ubuntu-24.04 steps: # Each job owns a short-lived token; prepare's token is revoked before build. @@ -161,8 +226,14 @@ jobs: with: name: SerializeReference-Extensions.unitypackage path: SerializeReference-Extensions.unitypackage - - # Create release and upload unitypackage. + + - uses: actions/download-artifact@v8 + with: + name: Signed-UPM + path: Signed-UPM + + # Keep the release draft until its notes are ready. Both packages must be + # attached before publication makes the signed tarball visible to OpenUPM. - uses: softprops/action-gh-release@v3 with: token: ${{ steps.release_app.outputs.token }} @@ -171,4 +242,31 @@ jobs: target_commitish: ${{ needs.prepare.outputs.sha }} draft: true prerelease: false - files: SerializeReference-Extensions.unitypackage/SerializeReference-Extensions.unitypackage + fail_on_unmatched_files: true + files: | + SerializeReference-Extensions.unitypackage/SerializeReference-Extensions.unitypackage + Signed-UPM/*.tgz + + publish-openupm: + if: github.event_name == 'release' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + id-token: write + steps: + - name: Publish signed release to OpenUPM + id: openupm + uses: openupm/openupm-action@v1 + with: + package: com.mackysoft.serializereference-extensions + tag: ${{ github.event.release.tag_name }} + + - name: Require a signed OpenUPM version + env: + SIGNED: ${{ steps.openupm.outputs.signed }} + run: | + if [[ "$SIGNED" != true ]]; then + echo "::error::OpenUPM did not publish the signed package." + exit 1 + fi