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:
- •Project config:
.upm-publish.jsonin the package root - •User config:
~/.config/unity-upm/config.json - •Defaults: Fall back to prompting the user
Configuration file format (.upm-publish.json)
{
"registry": {
"url": "https://cicd.entropyreductionservices.com/",
"name": "Entropy Reduction Services",
"scope": "com.entropyreductionservices"
},
"author": {
"name": "Author Name"
},
"github": {
"owner": "MatthewMaker"
}
}
Configuration options
| Field | Description | Example |
|---|---|---|
registry.url | npm registry URL for publishing | https://cicd.entropyreductionservices.com/ |
registry.name | Display name for scoped registry in manifest.json | "Entropy Reduction Services" |
registry.scope | Package scope prefix | com.entropyreductionservices |
registry.tokenSecret | GitHub secret name for auth token | VERDACCIO_TOKEN (default) |
author.name | Default author name for packages | "Matt Maker" |
github.owner | Default GitHub owner/org for repos | MatthewMaker |
If no config is found, prompt the user for:
- •Registry URL
- •Registry display name
- •Package scope (derived from company/org name)
Package Structure
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
{
"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):
{
"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):
{
"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):
{
"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):
fileFormatVersion: 2
guid: <32-char-hex-guid>
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
C# script meta (e.g., Script.cs.meta):
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):
fileFormatVersion: 2
guid: <32-char-hex-guid>
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Text file meta (e.g., package.json.meta, README.md.meta):
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:
echo -n "relative/path/to/file" | md5 | cut -c1-32
GitHub Actions
CI Workflow (.github/workflows/ci.yml)
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.
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
- •Update version in
package.json - •Update CHANGELOG.md with new version entry
- •Commit and push changes
- •Create GitHub release with tag matching version (e.g.,
v1.0.0) - •Publish workflow triggers automatically on release
Creating a release
gh release create v1.0.0 --title "v1.0.0" --notes "Release notes here"
Deprecating a version
GitHub:
gh release edit v1.0.0 --notes "⚠️ **DEPRECATED** - Use vX.X.X or later. Reason here."
npm registry:
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:
{
"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.jsonwith registry config (or use global config) - • Create
package.jsonwith correct naming and metadata - • Add
publishConfig.registryto package.json - • Create appropriate assembly definitions (
.asmdef) - • Add namespace to all C# files
- • Generate
.metafiles for ALL files and folders - • Create
README.mdwith installation instructions - • Create
CHANGELOG.mdwith initial version entry - • Create
LICENSE.md - • Create
.npmignoreto 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
.metafiles - runscripts/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
ArgumentExceptionfor paths with invalid characters