Add release automation#266
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new release automation pipeline that prepares a semver release PR, tags the merged release commit, and publishes GitHub Release assets + an npm package + a .unitypackage, with additional guardrails to prevent dev/CI artifacts from entering release payloads.
Changes:
- Added GitHub Actions workflows for Release Prepare (manual), Release Tag (merge-triggered), and Release Publish (tag-triggered).
- Added PowerShell release helper tooling + tests for semver bumping, changelog rotation, and release-notes generation.
- Hardened npm packaging by adding a
fileswhitelist and expanding npm package validation/ignore rules.
Reviewed changes
Copilot reviewed 13 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/validate-npm-package.ps1 | Adds stricter allow/deny-list validation of packed npm contents (top-level entries, scripts, and C# file roots). |
| scripts/unity/export-unitypackage.sh.meta | Unity .meta for the new export script. |
| scripts/unity/export-unitypackage.sh | Adds a Docker-Unity based .unitypackage export pipeline that stages from npm pack. |
| scripts/tests/test-release-tools.ps1.meta | Unity .meta for the new test script. |
| scripts/tests/test-release-tools.ps1 | Adds a lightweight PowerShell test runner for the release helper scripts. |
| scripts/release-tools/write-release-notes.ps1.meta | Unity .meta for the new release-notes writer. |
| scripts/release-tools/write-release-notes.ps1 | Adds a CLI wrapper that writes release notes derived from CHANGELOG.md. |
| scripts/release-tools/release-helpers.ps1.meta | Unity .meta for the new helper module. |
| scripts/release-tools/release-helpers.ps1 | Implements semver parsing/comparison, changelog rotation, and fence-aware changelog section extraction. |
| scripts/release-tools/prepare-release.ps1.meta | Unity .meta for the new prepare script. |
| scripts/release-tools/prepare-release.ps1 | Adds a CLI entrypoint to bump version + rotate changelog (supports dry-run). |
| scripts/release-tools.meta | Adds Unity folder .meta for the new scripts/release-tools directory. |
| package.json | Adds npm files whitelist and wires test:release-tools into validation. |
| docs/project/contributing.md | Updates contributor docs to describe the new release process (replacing release-drafter). |
| .npmignore | Expands ignore rules to exclude more dev/CI/editor artifacts from npm publishes. |
| .markdownlintignore | Ignores PLAN.md and its .meta file for markdown linting. |
| .github/workflows/release.yml | Adds tag-triggered Release Publish pipeline (validate/pack/export/publish). |
| .github/workflows/release-tag.yml | Adds merge-triggered tagging workflow guarded by commit subject + changelog. |
| .github/workflows/release-prepare.yml | Adds manual workflow to prepare and open a release PR (with dry-run option). |
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| ARTIFACTS_ROOT="$(realpath -m "${REPO_ROOT}/.artifacts")" | ||
| PROJECT_DIR="$(realpath -m "${PROJECT_DIR}")" | ||
| if [[ "${PROJECT_DIR}" != "${ARTIFACTS_ROOT}" && "${PROJECT_DIR}" != "${ARTIFACTS_ROOT}/"* ]]; then | ||
| echo "ERROR: Refusing to create the export project outside ${ARTIFACTS_ROOT}: ${PROJECT_DIR}" >&2 | ||
| exit 1 | ||
| fi |
| escaped_version="${package_version//./\\.}" | ||
| if ! grep -Eq "^## \[${escaped_version}\]( - [0-9]{4}-[0-9]{2}-[0-9]{2})?$" CHANGELOG.md; then | ||
| echo "::error::CHANGELOG.md has no exact heading for ${package_version}." | ||
| exit 1 | ||
| fi |
| if ! grep -Eq "^## \[${escaped_version}\]( - [0-9]{4}-[0-9]{2}-[0-9]{2})?$" CHANGELOG.md; then | ||
| echo "::error::Release commit for ${version} has no matching CHANGELOG.md heading." | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 19 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| throw new InvalidOperationException("Missing -exportOutput argument."); | ||
| } | ||
|
|
||
| Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); |
| throw "Expected exactly one package.json version property for '$CurrentVersion'; found $($matches.Count)." | ||
| } | ||
|
|
||
| $updated = [regex]::Replace($Content, $pattern, "`${1}$NextVersion`${2}", 1) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| throw new InvalidOperationException("Missing -exportOutput argument."); | ||
| } | ||
|
|
||
| Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); |
| $inFence = $false | ||
| $fenceMarker = '' | ||
|
|
||
| for ($index = 0; $index -lt $Lines.Count; $index++) { | ||
| $line = $Lines[$index] | ||
| $trimmed = $line.TrimStart() | ||
| $isFenceLine = $false | ||
|
|
||
| if ($trimmed -match '^(?<marker>`{3,}|~{3,})') { | ||
| $marker = $Matches['marker'] | ||
| $markerPrefix = $marker.Substring(0, 1) | ||
| if (-not $inFence) { | ||
| $inFence = $true | ||
| $fenceMarker = $markerPrefix | ||
| $isFenceLine = $true | ||
| } elseif ($fenceMarker -eq $markerPrefix) { | ||
| $isFenceLine = $true | ||
| $mask[$index] = $true | ||
| $inFence = $false | ||
| $fenceMarker = '' | ||
| continue | ||
| } | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 4 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| "files": [ | ||
| "CHANGELOG.md", | ||
| "CHANGELOG.md.meta", | ||
| "Editor", | ||
| "Editor.meta", | ||
| "LICENSE", | ||
| "LICENSE.meta", | ||
| "README.md", | ||
| "README.md.meta", | ||
| "Runtime", | ||
| "Runtime.meta", | ||
| "Samples~", | ||
| "docs", | ||
| "docs.meta", | ||
| "package.json.meta", | ||
| "scripts/postinstall-hooks.js" | ||
| ], |
| $allowedTopLevelEntries = @( | ||
| 'CHANGELOG.md', | ||
| 'CHANGELOG.md.meta', | ||
| 'Editor', | ||
| 'Editor.meta', | ||
| 'LICENSE', | ||
| 'LICENSE.meta', | ||
| 'README.md', | ||
| 'README.md.meta', | ||
| 'Runtime', | ||
| 'Runtime.meta', | ||
| 'Samples~', | ||
| 'docs', | ||
| 'docs.meta', | ||
| 'package.json', | ||
| 'package.json.meta', | ||
| 'scripts' | ||
| ) |
| } | ||
| } | ||
|
|
||
| $allowedCsRoots = @('Runtime/', 'Editor/', 'Samples~/') |
| for entry in \ | ||
| package.json \ | ||
| package.json.meta \ | ||
| README.md \ | ||
| README.md.meta \ | ||
| LICENSE \ | ||
| LICENSE.meta \ | ||
| CHANGELOG.md \ | ||
| CHANGELOG.md.meta \ | ||
| Runtime \ | ||
| Runtime.meta \ | ||
| Editor \ | ||
| Editor.meta | ||
| do | ||
| copy_package_entry "${entry}" required | ||
| done |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 1 comment.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| $scriptsDir = Join-Path $packageDir 'scripts' | ||
| if (Test-Path -LiteralPath $scriptsDir) { | ||
| $allowedScriptsEntries = @('postinstall-hooks.js') | ||
| $scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File | ForEach-Object { | ||
| $_.FullName.Replace("$scriptsDir\", "").Replace("$scriptsDir/", "") -replace '\\', '/' | ||
| } | ||
| foreach ($entry in $scriptEntries) { | ||
| if ($entry -notin $allowedScriptsEntries) { | ||
| $errors += "Unexpected script included in npm package: scripts/$entry" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 3 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| # enforces exact X.Y.Z; these exclusions avoid noisy prerelease/build runs. | ||
| - "[0-9]*.[0-9]*.[0-9]*" | ||
| - "![0-9]*.[0-9]*.[0-9]*-*" | ||
| - '![0-9]*.[0-9]*.[0-9]*\+*' |
| $publishTriggerExcludesPrereleaseTags = ( | ||
| $publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and | ||
| $publishWorkflowContent.Contains('- "![0-9]*.[0-9]*.[0-9]*-*"') -and | ||
| $publishWorkflowContent.Contains("- '![0-9]*.[0-9]*.[0-9]*\+*'") -and |
| } | ||
| } | ||
|
|
||
| # Get all files and subdirectories in this folder (recursively) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| # GitHub tag filters are globs, not regex. The verify job below still | ||
| # enforces no-leading-zero X.Y.Z semver. | ||
| - "[0-9]+.[0-9]+.[0-9]+" |
| $publishTriggerNarrowlyMatchesReleaseTags = ( | ||
| $publishWorkflowContent.Contains('- "[0-9]+.[0-9]+.[0-9]+"') -and | ||
| -not $publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and | ||
| $publishWorkflowContent.Contains('Release tags must use unprefixed X.Y.Z semver.') | ||
| ) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| PACKAGE_NAME="$(jq -r '.name' "${REPO_ROOT}/package.json")" | ||
| PACKAGE_VERSION="$(jq -r '.version' "${REPO_ROOT}/package.json")" | ||
| if [[ -z "${OUTPUT_PATH}" ]]; then | ||
| OUTPUT_PATH="${REPO_ROOT}/.artifacts/release/${PACKAGE_NAME}-${PACKAGE_VERSION}.unitypackage" | ||
| fi |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| push: | ||
| tags: | ||
| # GitHub tag filters are globs, not regex; their filter-pattern cheat | ||
| # sheet defines + as one-or-more of the preceding character. The verify | ||
| # job below still enforces no-leading-zero X.Y.Z semver. | ||
| - "[0-9]+.[0-9]+.[0-9]+" |
| $publishTriggerNarrowlyMatchesReleaseTags = ( | ||
| $publishWorkflowContent.Contains('- "[0-9]+.[0-9]+.[0-9]+"') -and | ||
| -not $publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and | ||
| $publishWorkflowContent.Contains('Release tags must use unprefixed X.Y.Z semver.') | ||
| ) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| 'postinstall-hooks.js', | ||
| 'postinstall-hooks.js.meta' | ||
| ) | ||
| $scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File | ForEach-Object { |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 3 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
Comments suppressed due to low confidence (1)
scripts/validate-npm-package.ps1:300
- Folder meta validation builds $relativePath via string Replace on the absolute path. This can produce incorrect relative paths (and therefore confusing errors) if the extracted path format differs. GetRelativePath is safer and consistent with git-style paths.
# Get relative path for better error messages
$relativePath = $item.FullName.Replace("$packageDir\", "").Replace("$packageDir/", "")
$relativePath = $relativePath -replace '\\', '/'
| Get-ChildItem -LiteralPath $PackageDir -Recurse -File -Force | | ||
| ForEach-Object { | ||
| $_.FullName.Replace("$PackageDir\", "").Replace("$PackageDir/", "") -replace '\\', '/' | ||
| } | | ||
| Sort-Object |
| $scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File -Force | ForEach-Object { | ||
| $_.FullName.Replace("$scriptsDir\", "").Replace("$scriptsDir/", "") -replace '\\', '/' | ||
| } |
| $allowedCsRoots = @('Runtime/', 'Editor/', 'Samples~/', 'Styles/') | ||
| $packedCsFiles = Get-ChildItem -LiteralPath $packageDir -Recurse -File -Filter '*.cs' -Force | ForEach-Object { | ||
| $_.FullName.Replace("$packageDir\", "").Replace("$packageDir/", "") -replace '\\', '/' | ||
| } |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a25d75b. Configure here.
| } | ||
| '; then | ||
| echo "::error::Release commit for ${version} has no matching CHANGELOG.md heading." | ||
| exit 1 |
There was a problem hiding this comment.
Tag skips changelog content check
Medium Severity
The Release Tag job only uses Test-ChangelogVersionHeading before pushing the semver tag. Publish later builds release notes with Get-ChangelogSection, which rejects a version section that has a heading but no actual release-note lines. A release-titled merge can therefore get a tag while Release Publish fails after the tag already exists.
Reviewed by Cursor Bugbot for commit a25d75b. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 26 changed files in this pull request and generated no new comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file


Summary
.unitypackageexport.Validation
npm run validate:prepushnpm run validate:npm-packagebash scripts/unity/export-unitypackage.sh --stage-only --project-dir .artifacts/unity/unitypackage-stage-smokeactionlint .github/workflows/release-prepare.yml .github/workflows/release-tag.yml .github/workflows/release.ymlNote
High Risk
Automates tagging, npm publish, and GitHub Release asset upload with production secrets and Unity license usage; mistakes or credential gaps could ship wrong versions or block releases.
Overview
Replaces the maintainer release-drafter flow in contributing with a three-stage pipeline: manual Release Prepare (semver bump or explicit version, optional dry-run), merge-triggered Release Tag, and tag-triggered Release Publish.
Release Prepare runs
prepare-release.ps1, syncs banner/issue-template versions, runs release and npm validation, then opens arelease/X.Y.ZPR via a GitHub App token (with collision checks on tags/branches and release notes validated before push).Release Tag tags default-branch commits whose message matches
release: X.Y.Z(or squash-merge variant) whenCHANGELOG.mdhas a fence-aware## [version]heading; non-releasepackage.json/CHANGELOG.mdpushes no-op cleanly.Release Publish verifies strict
X.Y.Ztags againstpackage.jsonand changelog, packs npm (checksums + notes), exports.unitypackagevia newexport-unitypackage.sh(npm pack → staged Unity project → Docker export under org Unity lock), then publishes npm (skip if exists) and a GitHub Release with tarball,.unitypackage, and.sha256assets.Supporting changes tighten the shipped artifact: explicit
package.jsonfilesallowlist, expanded.npmignore, and a strictervalidate-npm-package.ps1(full tracked payload parity, case-sensitive paths, forbidden dev roots). Addsscripts/release-tools/*,test:release-tools, and workflow contract tests. unity-tests-single-threaded now waits for the main and standalone Unity jobs to reduce org-lock contention within the same workflow.Reviewed by Cursor Bugbot for commit 8b2a360. Bugbot is set up for automated code reviews on this repo. Configure here.