Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,106 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Signs the UPM package with Unity's UPM CLI using service-account credentials,
# then attaches the signed .tgz to the just-created GitHub Release.
#
# OpenUPM picks up the signed tarball when its listing has `trackingMode: githubRelease`
# (see docs/openupm-signing.md for the cross-repo OpenUPM listing change). Until then,
# OpenUPM continues to pack from git and ships unsigned packages — the asset attached
# here is still uploaded for verification.
#
# Required repo secrets (configure via `gh secret set --repo IvanMurzak/Unity-MCP <NAME>`):
# - UPM_SERVICE_ACCOUNT_KEY_ID
# - UPM_SERVICE_ACCOUNT_KEY_SECRET
# - UPM_ORG_ID
# If any are missing the job soft-fails with `continue-on-error: true` so an absent
# signing key never blocks the release pipeline (the upstream release flow still ships).
# See https://openupm.com/docs/signing-upm-packages.html for the procedure.
sign-and-publish-upm:
runs-on: ubuntu-latest
needs: release-unity-plugin
if: needs.release-unity-plugin.outputs.success == 'true'
continue-on-error: true
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 }}
PACKAGE_DIR: Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp
DIST_DIR: /tmp/signed-upm-dist
steps:
- name: Verify signing secrets are configured
run: |
missing=()
[ -z "$UPM_SERVICE_ACCOUNT_KEY_ID" ] && missing+=("UPM_SERVICE_ACCOUNT_KEY_ID")
[ -z "$UPM_SERVICE_ACCOUNT_KEY_SECRET" ] && missing+=("UPM_SERVICE_ACCOUNT_KEY_SECRET")
[ -z "$UPM_ORG_ID" ] && missing+=("UPM_ORG_ID")
if [ "${#missing[@]}" -ne 0 ]; then
printf '::warning::UPM signing secrets are not configured (%s); skipping signing. See docs/openupm-signing.md.\n' "${missing[*]}"
# Exit non-zero so subsequent steps short-circuit; continue-on-error at the
# job level keeps the overall release pipeline green.
exit 1
fi

- name: Checkout repository
uses: actions/checkout@v6

- name: Log package metadata
run: |
package_name="$(jq -r '.name' "$PACKAGE_DIR/package.json")"
package_version="$(jq -r '.version' "$PACKAGE_DIR/package.json")"

printf 'Package name: %s\n' "$package_name"
printf 'Package version: %s\n' "$package_version"

- name: Install Unity UPM CLI
run: |
curl -fsSL https://cdn.packages.unity.com/upm-cli/install.sh -o install.sh
bash install.sh
echo "$HOME/.upm/bin" >> "$GITHUB_PATH"

- name: Verify Unity UPM CLI
run: upm --help

- name: Sign package
run: |
mkdir -p "$DIST_DIR"
upm pack "./$PACKAGE_DIR" --organization-id "$UPM_ORG_ID" --destination "$DIST_DIR"

- name: Verify signed package contains attestation
run: |
shopt -s nullglob
archives=("$DIST_DIR"/*.tgz "$DIST_DIR"/*.tar.gz)
if [ "${#archives[@]}" -ne 1 ]; then
printf 'Expected exactly one signed package archive, found %s: %s\n' "${#archives[@]}" "${archives[*]:-<none>}" >&2
exit 1
fi

archive="${archives[0]}"
archive_basename="$(basename "$archive")"
# OpenUPM consumes the asset via the `githubReleaseAssetName: 'com.ivanmurzak.unity.mcp-'`
# prefix documented in docs/openupm-signing.md. Enforce the contract here so a future
# `upm pack` naming change fails CI loudly instead of silently breaking OpenUPM pickup.
if [[ "$archive_basename" != com.ivanmurzak.unity.mcp-* ]]; then
printf 'Signed archive basename %q does not begin with the OpenUPM-expected prefix com.ivanmurzak.unity.mcp- (see docs/openupm-signing.md)\n' "$archive_basename" >&2
exit 1
fi

archive_entries="$(tar -tzf "$archive")"
grep -qx 'package/package.json' <<<"$archive_entries"
grep -qx 'package/.attestation.p7m' <<<"$archive_entries"

echo "PACKAGE_ARCHIVE=$archive" >> "$GITHUB_ENV"
printf 'Signed archive: %s\n' "$archive_basename"
tar -xOzf "$archive" package/package.json | jq '{name, version}'

- name: Attach signed package to release
uses: softprops/action-gh-release@v2
with:
files: ${{ env.PACKAGE_ARCHIVE }}
tag_name: ${{ needs.release-unity-plugin.outputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish_discord:
runs-on: ubuntu-latest
needs:
Expand Down
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- **Development**
- [Development Guide](/dev/Development.md)
- [Version Management](/version-management.md)
- [OpenUPM Package Signing](/openupm-signing.md)
- [Architecture](/#how-unity-mcp-architecture-works)

- **Languages**
Expand Down
136 changes: 136 additions & 0 deletions docs/openupm-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# OpenUPM Package Signing

Unity 6.3 introduced a package-signature check that surfaces a trust warning for
unsigned UPM packages installed from third-party registries (including OpenUPM).
This document describes how `IvanMurzak/Unity-MCP` signs its
`com.ivanmurzak.unity.mcp` package so the warning no longer appears in Unity 6.3+.

Tracks issue [#414](https://github.com/IvanMurzak/Unity-MCP/issues/414).

## How signing works

OpenUPM does **not** sign packages on behalf of authors — each package author runs
the signing flow in their own CI using a Unity organization's service account. The
signed `.tgz` is uploaded as a GitHub Release asset, and OpenUPM picks it up when
the package's listing has `trackingMode: githubRelease`.

References:
- <https://openupm.com/docs/signing-upm-packages.html>
- <https://openupm.com/blog/signing-upm-packages-with-openupm/>
- Reference workflow / repo layout: <https://github.com/openupm/com.example.signed-upm>

## What this repo ships

The signing step is implemented as the `sign-and-publish-upm` job in
[`.github/workflows/release.yml`](../.github/workflows/release.yml). It runs on
every successful version-bump release (the same trigger as `publish-mcp-server`
and `publish-unity-installer`), packs the package at
`Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/` with Unity's UPM CLI, and
attaches the resulting signed `.tgz` to the release tag.

The job is declared `continue-on-error: true` and exits early with a warning if
the required Unity-org secrets are not configured — so until the secrets are in
place the existing release flow continues to ship unchanged.

## One-time setup (repository owner)

These steps land outside this PR — they are operational, not code changes.

### 1. Create a Unity organization service account

A Unity organization is required to obtain UPM signing credentials (the
individual / personal Unity license cannot sign packages).

1. Go to the [Unity Cloud Dashboard](https://cloud.unity.com/) and either create
an organization or use an existing one you own.
2. Inside the organization settings, create a service account dedicated to
package signing.
3. Grant the service account the **package signing** permission for the
organization.
4. Generate a service-account key — record the `Key ID`, the `Key Secret`, and
the organization's `Org ID`. The secret is shown only once.

### 2. Add the three GitHub repository secrets

In this repo's Settings → Secrets and variables → Actions, add:

| Secret name | Value |
| --------------------------------- | ------------------------------------ |
| `UPM_SERVICE_ACCOUNT_KEY_ID` | Service account key ID |
| `UPM_SERVICE_ACCOUNT_KEY_SECRET` | Service account key secret |
| `UPM_ORG_ID` | Unity organization ID |

CLI equivalent:

```bash
gh secret set UPM_SERVICE_ACCOUNT_KEY_ID --repo IvanMurzak/Unity-MCP
gh secret set UPM_SERVICE_ACCOUNT_KEY_SECRET --repo IvanMurzak/Unity-MCP
gh secret set UPM_ORG_ID --repo IvanMurzak/Unity-MCP
```

### 3. File the OpenUPM listing change

OpenUPM's package listing for `com.ivanmurzak.unity.mcp` currently has
`trackingMode: git`, which makes OpenUPM pack and serve unsigned tarballs from
the repository's git tags. To make OpenUPM serve the signed tarball that the
workflow now uploads, the listing must be flipped to `trackingMode: githubRelease`.

The listing lives in the [openupm/openupm](https://github.com/openupm/openupm)
repository at `data/packages/com.ivanmurzak.unity.mcp.yml`. Open a PR there
changing:

```yaml
trackingMode: git
```

to:

```yaml
trackingMode: githubRelease
```

Per the OpenUPM blog, switch `trackingMode` to `githubRelease` **before** the
first signed release ships, so OpenUPM does not race-publish the unsigned git
tag in parallel.

Also set `githubReleaseAssetName` so OpenUPM picks the signed tarball by
filename prefix rather than guessing from the asset list. The release already
ships `.unitypackage` and multiple server `.zip` assets, and may add more
`.tgz` assets later (e.g. signing a second package), so the prefix guard
prevents a future-breaking failure mode:

```yaml
githubReleaseAssetName: 'com.ivanmurzak.unity.mcp-'
```

## Verifying signing worked

After the next release that runs with the secrets in place:

1. Go to the [release page](https://github.com/IvanMurzak/Unity-MCP/releases)
for the new version and confirm a `com.ivanmurzak.unity.mcp-<version>.tgz`
asset is attached alongside the existing `.unitypackage` and server `.zip`s.
2. Inspect the tarball locally to confirm it contains the signing attestation:

```bash
curl -fsSL -o package.tgz \
https://github.com/IvanMurzak/Unity-MCP/releases/download/<version>/com.ivanmurzak.unity.mcp-<version>.tgz
tar -tzf package.tgz | grep '\.attestation\.p7m$'
# expected: package/.attestation.p7m
```

3. Once the OpenUPM listing change merges, install the package in Unity 6.3+
from OpenUPM and confirm the unsigned-package warning no longer appears.

## Troubleshooting

- **Job is skipped with warning "UPM signing secrets are not configured"** —
expected when the three secrets are not yet set. Complete the
"One-time setup" steps above.
- **`upm pack` fails with an authentication error** — the service account key
is invalid or lacks the package-signing permission. Regenerate the key in the
Unity org dashboard and re-set the GitHub secrets.
- **The release contains the `.tgz` but Unity 6.3 still shows the warning** —
the OpenUPM listing is still on `trackingMode: git` (OpenUPM is serving the
unsigned git-packed version, not the release asset). File the
`openupm/openupm` PR described above.
Loading