Beginners guide on how to publish unity package via OpenUPM

Tanim-ul Haque Khan
9 min readNov 14, 2023

If you are anything like me, then you probably are looking for a proper guideline where you can develop your unity package via Unity and also publish package via OpenUPM with the same repository following their Guidelines. You are in the right place.

Firstly, what’s the problem? What’s a unity package? Well, that’s out of the scope of this article. If you are here, I expect you to already know why you are here. This is a guide on how to not an explanation of why.

Package Structure

Here’s how the folder structure of an ideal unity package looks like.

<package-root>
├── package.json
├── README.md
├── CHANGELOG.md
├── LICENSE.md
├── Third Party Notices.md
├── Editor
│ ├── <company-name>.<package-name>.Editor.asmdef
│ └── EditorExample.cs
├── Runtime
│ ├── <company-name>.<package-name>.asmdef
│ └── RuntimeExample.cs
├── Tests
│ ├── Editmode
│ │ ├── <company-name>.<package-name>.Editor.Tests.asmdef
│ │ └── EditorExampleTest.cs
│ └── Playmode
│ ├── <company-name>.<package-name>.Tests.asmdef
│ └── RuntimeExampleTest.cs
├── Samples~
│ ├── SampleFolder1
│ ├── SampleFolder2
│ └── ...
└── Documentation~
└── <package-name>.md

In this hierarchy, any script that is used during runtime, any monobehaviour, any data classes will be placed inside the Runtime folder and sub-folders if necessary.

The runtime and editor scripts have to be under their own Assembly Definition. For the example, there must be an assembly definition at the root of Runtime folder named,

‘com.<company-name>.<project-name>.<package-name>.asmdef’

Similar for the Editor assembly definition, it will be like the following,

‘com.<company-name>.<project-name>.<package-name>.editor.asmdef’

Important Note

When setting up the assembly definition for the Editor, you must make sure that the assembly definition has the following parameter correctly setup. This change also applies to the Editor Test Assembly Definition properties. You surely do not want editor script to be working on anywhere but editor mode.

Under Platforms label in asmdef, make sure that “Any Platform” is unticked and the “Editor” checkbox is checked under “Include Platforms” section. Make sure to press apply after making changes to the assembly definition file.

The Package Manifest

Next comes up preparing the package.json file. This is what everyone will be reading. Here’s a template of the Json file. Modify it according to your package.

{
"name": "com.studio23.ss2.inventorysystem",
"version": "1.0.0",
"displayName": "Inventory System",
"description": "Short description for the package.",
"unity": "2022.3",
"unityRelease": "9f1",
"documentationUrl": "https://openupm.com/packages/com.studio23.ss2.{package_name}/",
"changelogUrl": "https://openupm.com/packages/com.studio23.ss2.{package_name}/",
"licensesUrl": "https://opensource.org/license/mit/",
"scopedRegistries": [
{
"name": "Studio 23",
"url": "https://package.openupm.com",
"scopes": [
"com.studio23.ss2"
]
}],
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
},
"keywords": [
"Inventory",
"Inventory System",
"System"
],
"samples": [
{
"displayName": "Functional Example",
"description": "A small sample to demonstrate the abilities of the package.",
"path": "Samples~/Demo1"
}
],
"repository": {
"type": "git",
"url": "https://github.com/Studio-23-xyz/{package_name}.git"
},
"license": "MIT",
"author": {
"name": "Studio 23",
"email": "contact@studio-23.xyz",
"url": "https://studio-23.xyz"
}
}

IMPORTANT

The version attribute of the package is very important. It needs to be updated every time you wish to update your package.

TOO LONG DIDN’T READ

Well let’s admit it, you probably skipped most of it. I would have. What’s all this structure crap. Isn’t there any template to work with? well yeah you can google it. Or you could use this UPM Package we made to make our lives easy to develop packages.

📦 UPM Assistant com.studio23.ss2.upmassistant | OpenUPM

Thank me later. It’s not perfect but it should allow you to skip most of the annoying setup part. Hopefully we will be updating it to make it even better. For now, this is all the ASSIST we can offer. It’s Open Source by the way under MIT license so do whatever you want.

So yeah, using a template or a tool will allow you to skip everything we were talking on the top.

Next Step

Develop your package and test it. That’s definitely not part of this scope. You probably already have something. Just fit it to this structure to get started.

Trouble in Paradise

We successfully setup our package format inside unity. However, this is exactly where our problems begin. In unity we start from assets folder. But publishing in OpenUPM we need to make the package the root of the repository. Now we are in trouble. What do we do?

  1. We have a repo where we develop the project and another repo where we keep the updated library source only. (Manual work)
  2. We create a branch where we only keep the package files. (Still manual work as we need to delete other files and change root)
  3. We use something to automate the 2nd task. (Makes some sense)
  4. We use git subtree command to create a subtree. of the package folder and push it to a separate branch. (This is how we should be doing it)

So, what does this subtree command do? To put it simply we will be creating a new branch with the package folder as root.

My good colleague Md. Nurusshafi Evan figured these out and here’s his guidelines.

Setting up upm Branch

Once the development of the project is done, we will need to prepare the GitHub branch to keep the package portion on a separate branch. Since the last commit message will be used in the new branch, we will make an additional git commit and push at this stage stating Initial Release for the sake of keeping track. After that’s done, we will open up ‘gitbash’ where the Git repository of this project is initialized.

Step — 1 : Split upm Branch

We will be using the following command to separate the package contents to a branch named ‘upm’.

git subtree split -P {package. Directory} -b {branch.name}

For example, the package folder is at ‘Assets/Packages/com.studio23.ss2.SettingsManager’ and I want to create a branch name, so the command would be.

git subtree split -P Assets/Packages/com.studio23.ss2.SettingsManager -b upm

Upon successful execution, this is what we should see in the gitBash window.

With that, we are done separating the package contents.

Step — 2 : Create SemVer Tag and Sync With Remote

Now to maintain the package in the future with feature updates and/or patch fixes, we will be following Semantic Versioning. You can read more about the topic here. In short, this is the versioning practice that we will be following,

“v{Major}.{Minor}.{Patch/Bug}”

We now first need to check if there are any prior tags both locally and remote. First we will sync remote tags with our local stats. For that we will use the following commands.

git fetch --tags #fetches tags available on remote repo
git tags -l #lists all available tags at this point in alphabetical order.

Assuming that there are no prior tags and we are the releasing our package for the first time, we will version our tag as the first version.

git tag v1.0.0 upm #We specify upm after the SemVer to effectively let git know about where are creating the tag.

At this point, we have the package separated on ‘upm’ branch and have also created the initial release tag for the package. We will now sync the local branch with remote.

git push -u origin upm --tags

A successful execution of the command will yield something like the following.

Note: It is important that we maintain proper tag and versioning of the package. Take extra care when creating new tags.

With that done, we now have the package contents separated as well as pushed to remote git and synced locally, we have also created a tag for the new release that we are about to do.

TOO LONG DIDN’T READ AGAIN

While my good colleague has the patience to do all of that manually, I do not. Instead, I wrote this GitHub Action Script that does all of that for me. You can just copy paste it to your actions folder and it should work. Thank me later.

name: Update and Publish UPM Branch
on:
push:
branches:
- main
jobs:
update-and-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Find package.json
id: find-package-json
run: echo ::set-output name=package-json-path::$(find . -name "package.json" | head -n 1)
- name: Get Package Name from package.json
id: get-package-name
run: echo ::set-output name=package-name::$(jq -r .name ${{ steps.find-package-json.outputs.package-json-path }})
- name: Git Subtree Split
run: git subtree split -P Assets/Packages/${{ steps.get-package-name.outputs.package-name }}/ -b upm
- name: Get Version from package.json
id: get-version
run: echo ::set-output name=version::$(jq -r .version ${{ steps.find-package-json.outputs.package-json-path }})
- name: Create Git Tag
if: steps.get-version.outputs.version != ''
run: git tag v${{ steps.get-version.outputs.version }} upm
- name: Push Branch and Tags
run: |
git push -f -u origin upm
git push --tags

Just in case you want to know what this does please copy paste this to ChatGPT and ask what it means. I could have copied and pasted from there too but a bit too lazy. Please do this much in case you are curious. I even linked it to the proper website. Don’t be as lazy as me.

Adding Samples to your Unity Package

If you saw other packages, you must have noticed that they can add samples and it can be installed nicely. Awesome we want that too. But unfortunately, so far, we haven’t been able to figure out a way we think that’s proper. So, we won't be sharing about how you can too, but let's discuss a few concepts here.

Unity doesn’t import folders that has ~ at the end of its name.

So, the way it works is you ship your samples folder with ~ at the end of samples folder name. And make sure you fill up the package.manifest properly in the samples section.

"samples": [
{
"displayName": "Functional Example",
"description": "A small sample to demonstrate the abilities of the package.",
"path": "Samples~/Demo1"
},
{
"displayName": "Example - 2",
"description": "Yet another description of another sample.",
"path": "Samples~/Demo2"
}
],

So basically, before you commit to your main branch or push to upm branch you need to manually rename the Samples folder to Samples~. And when developing you need to rename it back. Annoying right? Should automate it Right? for whatever reason we were not able to figure out the proper way. I mean we did but it didn’t work. Didn’t bother as we don’t need it asap. So good luck. Hint: Need to add a few lines in the CI workflow something like this.

- name: Push Branch and Tags
run: |
git checkout upm
if [[ -d "Samples" ]]; then
git mv Samples Samples~
rm -f Samples.meta
fi
git push -f -u origin upm
git push --tags

It’s not going to be as easy as you think.

I mean it works if you keep samples as is without that ~ sign. So, for now, you can keep it just like that and it should work. But hey I shared how it should be not how it is.

Adding to OpenUPM

Okay we actually have to add our package repo to the OpenUPM Registry. to be honest it’s pretty straight forward.

  1. Make sure it’s public.
  2. Go to Package Add | OpenUPM
  3. Follow their instructions. (Obviously the branch you should select would be upm not main if you were following the guide)
  4. Done.

Updating you package

Boy oh boy, now this is where things get complicated. Once you update your version you need to run those commands again or just use the CI flow. Of course, make sure you update your package.json version property.

1) Make sure your repo is now showing a new tag after running the commands and all.

2) Wait a while

3) After 10–15 mins you should be able to see your package updated to OpenUPM via Black Magic.

Some Awesome Opensource UPM Packages to observe.

--

--

Tanim-ul Haque Khan

Author — “How to Make A Game” / Apress, Springer Nature | Head Of Unity Department at Brain Station 23 Limited | Co-Founder of Capawcino Cat Cafe