How to Maintain UPM Package Part 2

Automating Releases with GitHub Actions

Favo Yang
OpenUPM
7 min readFeb 1, 2020

--

  • This article is part of a series that discussing best practices of managing a UPM repository on GitHub. See part 1, part 3, and part 4.
  • Feel free to check out openupm.com to discover more open-source UPM packages, and support OpenUPM at patreon.com/openupm.

Why use version control

Following part 1 for the series, we have created a UPM repository that can be consumed by the Unity Package Manager via git URL. It’s convenient, but with two drawbacks:

  • Lacking version control. When adding a package via git URL, the Unity Package Manager will resolve the package version as the latest commit, and save to the lock field of the project manifest.json. This is necessary to ensure consistency in a team project. But as of Unity 2019.3, the Package Manager didn’t provide a good way to update a git package, you have to manually edit the hash value, or delete the lock entry to resolve the package again.
{
"lock": {
"com.littlebigfun.upm-ci-example": {
"revision": "upm",
"hash": "82adc5b54eac98e6e5789e174788c9abe68530b7"
}
}
}
  • Lacking support for the package with git dependencies. In another word, your custom package can not depend on another git URL. Because a git URL which consists of a repository address and a version name (usually a branch name) is usually mutable. There’s no way to lock to a specific commit unless the version name is a git tag.

The alternative way is to publish your package on a registry and use the version field of the package.json to manage versions.

Understanding semantic versioning

Semantic versioning (also known as SemVer) is a popular way to version a software library. It’s simply a set of rules and conventions to help define the next meaningful version string.

[major].[minor].[patch]-[pre-release]# Examples
1.0.0
1.0.1-preview
1.0.2-preview.1
  • Major increases when the API breaks backward compatibility.
  • Minor increases when the API adds new features without breaking backward compatibility.
  • Patch increases when the API changes small things like fixing bugs or refactoring.
  • Pre-release is optional, as a label to specify a certain stage of development. The only pre-release label supported by Unity Package Manager is the preview, you can toggle preview packages in the list view.

Rules are cold. Semantic versioning is designed to be meaningful for other software, but not very human friendly. Humans are more comfortable with humanized version, which delivering extra marketing information to trick our brain:

  • There’s no iPhone 9 because 2017 was the 10th anniversary of the iPhone’s original unveiling by Steve Jobs in January 2007.
  • When software versioning with year number, it builds a strong connection with the subscription pricing model.

Well, semantic versioning is not a silver bullet, but Unity force packages to follow semantic versioning format anyway.

Introducing semantic-release

Version management is good, but it’s not as simple as bumping a version number. To make a good release you have to do quite a few painful jobs repeatedly:

  • Bump the version number in package.json
  • Update the changelog file
  • Commit to git
  • Create GitHub releases (git tags) with release notes
  • Publish to a UPM registry

If you make mistakes in any step, you have to go through the checklist again, seriously slowing down your performance. Semantic-release is a cure by providing fully automated version management.

Semantic-release analyzes commit messages to figure out the type of changes, then automatically determines the next semantic version number, generates a changelog and publishes the release. Sounds pretty cool, right? All we need caring about is to write good commit messages the tool can understand. By default, semantic-release uses Angular Commit Message Conventions.

<type>(<scope>): <subject># Examples
feat: create dynamic group based on settings
fix: memory leak on mobile
docs: add badges to README
chore(release): 0.5.0

The type must be one of the following:

  • feat: new feature
  • fix: bug fix
  • docs: documentation only changes
  • style: changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
  • refactor: code change that neither fixes a bug nor adds a feature
  • perf: code change that improves performance
  • test: adding missing or correcting existing tests
  • chore: changes to the build process or auxiliary tools and libraries such as documentation generation

The scope is an optional field to describe the related sub-system, which can be ignored for now.

Let’s deploy the wonderful tool to our repository via GitHub's actions.

Automating semantic-release

We’ll start with the simple case that assuming the package.json is located at the root path.

Time to go shopping. Let’s deploy Action For Semantic Release from the GitHub marketplace, which integrated semantic-release into an easy-to-use GitHub action, by adding just two files to a repository.

.github/workflows/ci.yml

.github/workflows/ci.yml

Let’s explain step by step

  • It checkouts the full git history (fetch-depth: 0).
  • It specifies three extra plugins to be installed with semantic-release-action on the GitHub action machine. We’ll explain later.
  • It limits the semantic-release tool to run on the master branch only.
  • It provides the GITHUB_TOKEN environment variable to handle extra git command.

Nothing special, Let’s move on to the configuration file of semantic-release.

.releaserc.json

The semantic-release configuration file in JSON format. You can also use another format like js or YAML.

.releaserc.json

Let’s explain step by step

  • tagFormat defines the git tag format used by semantic-release to identify releases. It should keep consistent with your existing git tags. Otherwise, your next release will be reset to 1.0.0. For example, if your git tags (run git tag to determine) use the version number without the prefix “v”, tagFormat should be “${version}”.
  • plugins define a list of semantic-release tasks and the execution order.
  • @semantic-release/commit-analyzer determines the type of release by analyzing commits with conventional-changelog.
  • @semantic-release/release-notes-generator generates release notes for the commits added since the last release to the CHANGELOG.md file.
  • @semantic-release/npm bumps the package.json version. The npmPublish is set to false to prevent to publish to the NPM registry,
  • @semantic-release/git commits the changed file (package.json, CHANGELOG.md) to git. This commit should skip the CI to avoid an endless loop by specifying “[skip ci]” text in the commit message.
  • @semantic-release/github creates a GitHub release (git tag).

That’s it. Committing these two files to your GitHub repository, and you will see a release commit right after your normal commit.

Please remember to update your local branch before pushing the next commit.

git pull --rebase 

Continuous releasing

Because almost each of your commit will get released immediately. The behavior changes your release strategy from manually releasing to continuous releasing. It’s a good thing that forces you to keep the master branch stable. WIP commits shall be organized in a separate branch, and merge back to master as a squashed commit with a clear commit message.

You can check out the example repository at https://github.com/favoyang/semantic-release-example

Automating semantic-release with upm branch

So what if the package.json is located at a sub-folder? In part 1 of the series, we already managed to create an up-to-date upm branch and handle the Samples folder as well. Let’s see how to integrate them with semantic-release.

.github/workflows/ci.yml

.github/workflows/ci.yml

Changed parts:

  • Step semantic release, specifies the id to semantic, to reference the action result later.
  • Step create upm branch, creates the upm branch and handles the Samples folder. The step is even with the action step of part 1.
  • Step create upm git tag, creates an additional versioned git tag based on the upm branch, naming as upm/vx.y.z. To make it possible to install a specific version via git URL. This step only executes when the semantic release step created a new release version.

.releaserc.json

.releaserc.json

Changed parts:

  • @semantic-release/npm, set pkgRoot to the package.json directory.
  • @semantic-release/git, specify the path of package.json.

Committing these two files to your GitHub repository, you will see:

  • A release commit right after your commit.
  • An up-to-date upm branch.
  • An additional versioned upm tag.

You can check out the example repository at https://github.com/favoyang/semantic-release-upm-example

Debugging semantic-release

To debug semantic-release or find out what the next release will be, you can install and dry-run semantic-release locally.

  • Install semantic-release.
npm install -g semantic-release @semantic-release/changelog @semantic-release/git
  • Request a personal access token (GitHub token).
  • Dry-run to verify the next release.
GH_TOKEN=YOUR_GITHUB_TOKEN semantic-release --dry-run

Conclusion

This tutorial went through the concept of semantic version, and how to use the semantic-release tool to automate the release process to achieve continuous releasing. The next step is to submit your package to the OpenUPM platform, which will publish packages based on git tags.

Repositories that using this (or a similar) approach:

Feel free to check out openupm.com to discover more open-source UPM packages, and support OpenUPM at patreon.com/openupm

--

--