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