AgentSkillsCN

Skill

技能

SKILL.md

Unity UPM Package Creator

Create a Unity Package Manager (UPM) package from existing code, with GitHub Actions for CI/CD and publishing to a private npm registry (Verdaccio or similar).

Configuration

This skill uses configurable settings. When creating a package, check for configuration in this order:

  1. Project config: .upm-publish.json in the package root
  2. User config: ~/.config/unity-upm/config.json
  3. Defaults: Fall back to prompting the user

Configuration file format (.upm-publish.json)

json
{
  "registry": {
    "url": "https://cicd.entropyreductionservices.com/",
    "name": "Entropy Reduction Services",
    "scope": "com.entropyreductionservices"
  },
  "author": {
    "name": "Author Name"
  },
  "github": {
    "owner": "MatthewMaker"
  }
}

Configuration options

FieldDescriptionExample
registry.urlnpm registry URL for publishinghttps://cicd.entropyreductionservices.com/
registry.nameDisplay name for scoped registry in manifest.json"Entropy Reduction Services"
registry.scopePackage scope prefixcom.entropyreductionservices
registry.tokenSecretGitHub secret name for auth tokenVERDACCIO_TOKEN (default)
author.nameDefault author name for packages"Matt Maker"
github.ownerDefault GitHub owner/org for reposMatthewMaker

If no config is found, prompt the user for:

  • Registry URL
  • Registry display name
  • Package scope (derived from company/org name)

Package Structure

code
package-name/
├── package.json              # UPM manifest (required)
├── package.json.meta
├── README.md
├── README.md.meta
├── CHANGELOG.md
├── CHANGELOG.md.meta
├── LICENSE.md
├── LICENSE.md.meta
├── .upm-publish.json         # Optional: publishing config
├── .npmignore                # Excludes dev files from npm publish
├── .github/
│   └── workflows/
│       ├── ci.yml            # Validates package on push/PR
│       └── publish.yml       # Publishes to registry on release
├── scripts/
│   └── generate-meta-files.sh  # Helper to generate .meta files
├── Editor/                   # Editor-only code (optional)
│   ├── Editor.meta
│   ├── PackageName.Editor.asmdef
│   ├── PackageName.Editor.asmdef.meta
│   ├── *.cs
│   └── *.cs.meta
├── Runtime/                  # Runtime code (optional)
│   ├── Runtime.meta
│   ├── PackageName.Runtime.asmdef
│   ├── PackageName.Runtime.asmdef.meta
│   ├── *.cs
│   └── *.cs.meta
└── Tests/
    └── Editor/
        ├── Tests.meta
        ├── Editor.meta
        ├── PackageName.Tests.asmdef
        ├── PackageName.Tests.asmdef.meta
        ├── *Tests.cs
        └── *Tests.cs.meta

Required Files

package.json

json
{
  "name": "com.companyname.package-name",
  "displayName": "Package Display Name",
  "version": "1.0.0",
  "unity": "2021.3",
  "description": "Package description here.",
  "keywords": ["keyword1", "keyword2"],
  "author": {
    "name": "Author Name"
  },
  "repository": {
    "type": "git",
    "url": "git@github.com:Owner/repo-name.git"
  },
  "license": "MIT",
  "publishConfig": {
    "registry": "{{REGISTRY_URL}}"
  }
}

Package naming convention: com.companyname.package-name (lowercase, hyphens for multi-word names)

Assembly Definitions

Editor assembly (Editor/PackageName.Editor.asmdef):

json
{
    "name": "PackageName.Editor",
    "rootNamespace": "CompanyName.PackageName",
    "references": [],
    "includePlatforms": ["Editor"],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}

Runtime assembly (Runtime/PackageName.Runtime.asmdef):

json
{
    "name": "PackageName.Runtime",
    "rootNamespace": "CompanyName.PackageName",
    "references": [],
    "includePlatforms": [],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}

Test assembly (Tests/Editor/PackageName.Tests.asmdef):

json
{
    "name": "PackageName.Tests",
    "rootNamespace": "CompanyName.PackageName.Tests",
    "references": [
        "PackageName.Editor",
        "UnityEngine.TestRunner",
        "UnityEditor.TestRunner"
    ],
    "includePlatforms": ["Editor"],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": true,
    "precompiledReferences": ["nunit.framework.dll"],
    "autoReferenced": false,
    "defineConstraints": ["UNITY_INCLUDE_TESTS"],
    "versionDefines": [],
    "noEngineReferences": false
}

Unity .meta Files

CRITICAL: Unity requires .meta files for every file and folder in a package. Without them, the package will fail to import.

Meta file formats

Folder meta (e.g., Editor.meta):

yaml
fileFormatVersion: 2
guid: <32-char-hex-guid>
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:

C# script meta (e.g., Script.cs.meta):

yaml
fileFormatVersion: 2
guid: <32-char-hex-guid>
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:

Assembly definition meta (e.g., *.asmdef.meta):

yaml
fileFormatVersion: 2
guid: <32-char-hex-guid>
AssemblyDefinitionImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:

Text file meta (e.g., package.json.meta, README.md.meta):

yaml
fileFormatVersion: 2
guid: <32-char-hex-guid>
TextScriptImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:

Generating GUIDs

GUIDs should be deterministic based on the file path. Use MD5 hash truncated to 32 hex characters:

bash
echo -n "relative/path/to/file" | md5 | cut -c1-32

GitHub Actions

CI Workflow (.github/workflows/ci.yml)

yaml
name: CI

on:
  push:
    branches: [main]
    paths:
      - '**.cs'
      - 'package.json'
      - '.github/workflows/**'
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Validate package.json
        run: |
          jq -e '.name' package.json
          jq -e '.version' package.json
          jq -e '.unity' package.json
          echo "package.json validation passed"

      - name: Verify package name format
        run: |
          NAME=$(jq -r '.name' package.json)
          if [[ ! "$NAME" =~ ^com\.[a-z]+\.[a-z0-9-]+$ ]]; then
            echo "Error: Package name doesn't follow UPM convention"
            exit 1
          fi

      - name: Check CHANGELOG has entry for current version
        run: |
          VERSION=$(jq -r '.version' package.json)
          if grep -q "## \[$VERSION\]" CHANGELOG.md; then
            echo "CHANGELOG entry found for version $VERSION"
          else
            echo "Warning: No CHANGELOG entry for version $VERSION"
          fi

      - name: Verify meta files exist
        run: |
          MISSING=0
          for file in package.json README.md CHANGELOG.md LICENSE.md; do
            if [ -f "$file" ] && [ ! -f "${file}.meta" ]; then
              echo "Error: Missing ${file}.meta"
              MISSING=1
            fi
          done
          # Check directories and source files...
          if [ "$MISSING" -eq 1 ]; then exit 1; fi

Publish Workflow (.github/workflows/publish.yml)

The registry URL is read from package.json's publishConfig.registry field.

yaml
name: Publish to Registry

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Dry run (validate without publishing)'
        type: boolean
        default: false

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Get registry URL from package.json
        id: registry
        run: |
          REGISTRY=$(jq -r '.publishConfig.registry // "https://registry.npmjs.org/"' package.json)
          echo "url=$REGISTRY" >> $GITHUB_OUTPUT
          echo "Publishing to: $REGISTRY"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: ${{ steps.registry.outputs.url }}

      - name: Verify version matches tag
        if: github.event_name == 'release'
        run: |
          TAG_VERSION="${{ github.event.release.tag_name }}"
          TAG_VERSION="${TAG_VERSION#v}"
          PKG_VERSION=$(jq -r '.version' package.json)
          if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
            echo "Error: Tag version doesn't match package.json"
            exit 1
          fi

      - name: Publish to registry
        if: ${{ !inputs.dry_run }}
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}

Note: The secret name NPM_REGISTRY_TOKEN can be customized. Common names:

  • VERDACCIO_TOKEN - for Verdaccio registries
  • NPM_TOKEN - for npmjs.com
  • NPM_REGISTRY_TOKEN - generic name

Publishing Workflow

  1. Update version in package.json
  2. Update CHANGELOG.md with new version entry
  3. Commit and push changes
  4. Create GitHub release with tag matching version (e.g., v1.0.0)
  5. Publish workflow triggers automatically on release

Creating a release

bash
gh release create v1.0.0 --title "v1.0.0" --notes "Release notes here"

Deprecating a version

GitHub:

bash
gh release edit v1.0.0 --notes "⚠️ **DEPRECATED** - Use vX.X.X or later. Reason here."

npm registry:

bash
npm deprecate com.company.package@1.0.0 "Reason here - use vX.X.X or later" --registry={{REGISTRY_URL}}

Installation in Unity Projects

Add to Packages/manifest.json:

json
{
  "scopedRegistries": [
    {
      "name": "{{REGISTRY_NAME}}",
      "url": "{{REGISTRY_URL}}",
      "scopes": ["{{SCOPE}}"]
    }
  ],
  "dependencies": {
    "com.companyname.package-name": "1.0.0"
  }
}

Checklist

When creating a new UPM package:

  • Create package directory structure
  • Create .upm-publish.json with registry config (or use global config)
  • Create package.json with correct naming and metadata
  • Add publishConfig.registry to package.json
  • Create appropriate assembly definitions (.asmdef)
  • Add namespace to all C# files
  • Generate .meta files for ALL files and folders
  • Create README.md with installation instructions
  • Create CHANGELOG.md with initial version entry
  • Create LICENSE.md
  • Create .npmignore to exclude dev files
  • Create .github/workflows/ci.yml
  • Create .github/workflows/publish.yml
  • Initialize git repository
  • Create GitHub repository
  • Add registry token secret to repository (e.g., VERDACCIO_TOKEN)
  • Create initial release to trigger publish
  • Verify package on registry: npm view com.company.package --registry={{REGISTRY_URL}}

Common Issues

Package fails to import in Unity

  • Missing .meta files - run scripts/generate-meta-files.sh
  • Invalid GUID in meta file - regenerate with deterministic hash

Publish fails with 401

  • Registry token expired or invalid
  • For Verdaccio with GitHub OAuth: log in to web UI to generate new token
  • For npmjs.com: generate token at https://www.npmjs.com/settings/tokens

ArgumentException on paths

  • Validate paths before processing (check for null, empty, or non-Assets paths)
  • Catch ArgumentException for paths with invalid characters