feat(workspace): add package installation validation
Validate ShrinkSDK Workspace / unity (push) Failing after 8s

This commit is contained in:
2026-08-26 04:50:27 +08:00
parent dcf1099b9c
commit 2dd7f5b14a
7 changed files with 751 additions and 8 deletions
+55
View File
@@ -0,0 +1,55 @@
name: Validate ShrinkSDK Workspace
on:
push:
branches:
- main
workflow_dispatch:
jobs:
unity:
runs-on: unity-2022.3.62f3
container:
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
volumes:
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
permissions:
contents: read
env:
UNITY_VERSION: 2022.3.62f3
steps:
- name: Fetch exact Workspace revision and submodules
shell: bash
run: |
set -euo pipefail
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
git submodule sync --recursive
git submodule update --init --recursive
module_count="$(git submodule status --recursive | wc -l | tr -d ' ')"
test "$module_count" = "21"
test -z "$(git submodule status --recursive | grep '^-')"
git submodule status --recursive
- name: Compile Workspace and validate package graph
shell: bash
run: |
set -euo pipefail
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin"
mkdir -p Artifacts
"$unity_bin" -version | head -n 1 | grep -F "$UNITY_VERSION"
status=0
"$unity_bin" \
-batchmode \
-nographics \
-quit \
-projectPath "$PWD" \
-executeMethod ShrinkSDK.WorkspaceValidation.ShrinkSdkWorkspaceValidation.Run \
-logFile "$PWD/Artifacts/unity-workspace-validation.log" || status=$?
tail -n 240 "$PWD/Artifacts/unity-workspace-validation.log" || true
exit "$status"
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fa5cd8919ec4f544b89255c3da6b4ba0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,190 @@
#if UNITY_EDITOR
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace ShrinkSDK.WorkspaceValidation
{
public static class ShrinkSdkWorkspaceValidation
{
private static readonly ExpectedPackage[] ExpectedPackages =
{
new ExpectedPackage("ShrinkApp.Core", "com.cneicy.shrink-app-core", "0.1.1"),
new ExpectedPackage("ShrinkApp.Starter.Basic", "com.cneicy.shrink-app-starter-basic", "0.1.0"),
new ExpectedPackage("ShrinkCommand", "com.cneicy.shrink-command", "0.2.0"),
new ExpectedPackage("ShrinkCommand.Integration.App", "com.cneicy.shrink-command-integration-app", "0.1.0"),
new ExpectedPackage("ShrinkCommand.Integration.EventBus", "com.cneicy.shrink-command-integration-eventbus", "0.1.1"),
new ExpectedPackage("ShrinkCommand.Integration.Network", "com.cneicy.shrink-command-integration-network", "0.1.0"),
new ExpectedPackage("ShrinkContext.AppAdapter", "com.cneicy.shrink-context-app-adapter", "0.1.0"),
new ExpectedPackage("ShrinkContext.Core", "com.cneicy.shrink-context-core", "0.1.0"),
new ExpectedPackage("ShrinkContext.EventBusAdapter", "com.cneicy.shrink-context-eventbus-adapter", "0.1.0"),
new ExpectedPackage("ShrinkDataSaver", "com.cneicy.shrink-datasaver", "2.2.0"),
new ExpectedPackage("ShrinkDataSaver.Integration.App", "com.cneicy.shrink-datasaver-integration-app", "0.1.0"),
new ExpectedPackage("ShrinkDataSaver.Integration.EventBus", "com.cneicy.shrink-datasaver-integration-eventbus", "2.1.0"),
new ExpectedPackage("ShrinkEventBus", "com.cneicy.shrink-eventbus", "2.0.0"),
new ExpectedPackage("ShrinkEventBus.Entities", "com.cneicy.shrink-eventbus-entities", "0.1.0"),
new ExpectedPackage("ShrinkInstaller", "com.cneicy.shrink-installer", "0.1.2"),
new ExpectedPackage("ShrinkModFramework", "com.cneicy.shrink-mod-framework", "0.2.1"),
new ExpectedPackage("ShrinkNetwork", "com.cneicy.shrink-network", "0.2.0"),
new ExpectedPackage("ShrinkNetwork.Integration.App", "com.cneicy.shrink-network-integration-app", "0.1.0"),
new ExpectedPackage("ShrinkNetwork.Integration.EventBus", "com.cneicy.shrink-network-integration-eventbus", "0.1.1"),
new ExpectedPackage("ShrinkShared.CodeGen", "com.cneicy.shrink-shared-codegen", "0.1.0"),
new ExpectedPackage("ShrinkTutorial", "com.cneicy.shrink-tutorial", "0.1.0")
};
public static void Run()
{
try
{
var workspaceRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
var packages = ReadPackages(workspaceRoot);
ValidateGraph(packages);
Debug.Log($"ShrinkSDK Workspace validation passed: packages={packages.Count}");
EditorApplication.Exit(0);
}
catch (Exception exception)
{
Debug.LogException(exception);
EditorApplication.Exit(1);
}
}
private static Dictionary<string, PackageDefinition> ReadPackages(string workspaceRoot)
{
var result = new Dictionary<string, PackageDefinition>(StringComparer.Ordinal);
foreach (var expected in ExpectedPackages)
{
var manifestPath = Path.Combine(workspaceRoot, "Assets", "Modules", expected.Directory, "package.json");
if (!File.Exists(manifestPath))
{
throw new FileNotFoundException($"Required package manifest is missing: {expected.Directory}", manifestPath);
}
var manifest = JObject.Parse(File.ReadAllText(manifestPath));
var packageName = manifest.Value<string>("name");
var version = manifest.Value<string>("version");
if (packageName == null || version == null || packageName.Length == 0 || version.Length == 0)
{
throw new InvalidOperationException($"{expected.Directory} has no valid package name or version.");
}
if (!string.Equals(packageName, expected.Name, StringComparison.Ordinal) ||
!string.Equals(version, expected.Version, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"{expected.Directory} must be {expected.Name}@{expected.Version}, found {packageName}@{version}.");
}
if (result.ContainsKey(packageName))
{
throw new InvalidOperationException($"Duplicate package manifest name: {packageName}");
}
result.Add(packageName, new PackageDefinition(packageName, version, manifest));
}
return result;
}
private static void ValidateGraph(IReadOnlyDictionary<string, PackageDefinition> packages)
{
var dependencies = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
foreach (var package in packages.Values)
{
var localDependencies = new HashSet<string>(StringComparer.Ordinal);
var manifestDependencies = package.Manifest["dependencies"] as JObject;
if (manifestDependencies != null)
{
foreach (var property in manifestDependencies.Properties())
{
if (!packages.TryGetValue(property.Name, out var dependency))
{
continue;
}
var expectedVersion = property.Value.Value<string>();
if (!string.Equals(expectedVersion, dependency.Version, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"{package.Name} requires {dependency.Name} {expectedVersion}, but the Workspace provides {dependency.Version}.");
}
var isIntegrationOrStarter =
package.Name.IndexOf("-integration-", StringComparison.Ordinal) >= 0 ||
package.Name.IndexOf("-starter-", StringComparison.Ordinal) >= 0;
if (dependency.Name.IndexOf("-integration-", StringComparison.Ordinal) >= 0 && !isIntegrationOrStarter)
{
throw new InvalidOperationException(
$"{package.Name} must not depend on integration package {dependency.Name}.");
}
localDependencies.Add(dependency.Name);
}
}
dependencies.Add(package.Name, localDependencies);
}
var resolved = new HashSet<string>(StringComparer.Ordinal);
var madeProgress = true;
while (madeProgress)
{
madeProgress = false;
foreach (var packageName in dependencies.Keys.OrderBy(name => name, StringComparer.Ordinal))
{
if (resolved.Contains(packageName) || dependencies[packageName].Any(dependency => !resolved.Contains(dependency)))
{
continue;
}
resolved.Add(packageName);
madeProgress = true;
}
}
if (resolved.Count != packages.Count)
{
var blocked = dependencies
.Where(pair => !resolved.Contains(pair.Key))
.Select(pair => pair.Key + " -> " + string.Join(", ", pair.Value.Where(dependency => !resolved.Contains(dependency))))
.OrderBy(value => value, StringComparer.Ordinal);
throw new InvalidOperationException("Circular internal package dependencies detected: " + string.Join("; ", blocked));
}
}
private sealed class PackageDefinition
{
public PackageDefinition(string name, string version, JObject manifest)
{
Name = name;
Version = version;
Manifest = manifest;
}
public string Name { get; }
public string Version { get; }
public JObject Manifest { get; }
}
private readonly struct ExpectedPackage
{
public ExpectedPackage(string directory, string name, string version)
{
Directory = directory;
Name = name;
Version = version;
}
public string Directory { get; }
public string Name { get; }
public string Version { get; }
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 508c3ec513ec5194cb44d97137cf0a85
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+13 -8
View File
@@ -1,11 +1,11 @@
# ShrinkSDK 当前架构
> 当前基线:2026-08-18
> 当前基线:2026-08-26
> 本文是仓库唯一的当前架构文档。实现与本文冲突时,以源码、`package.json`、asmdef 和自动化验证结果为准,并应在同一变更中修正文档。迁移过程与旧代码地图只保存在 `Docs/Archive/`,不再作为当前设计依据。
## 1. 项目定位
ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK 单仓库,不是单个游戏项目。仓库直接拥有 `Assets/Modules/` 下全部包源码;模块目录不再是嵌套 Git 仓库或 gitlink。一次干净克隆应能获得完整源码,Unity 只需要恢复外部 UPM 依赖。
ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK Workspace,不是单个游戏项目。`Workspace` 根仓库保留集成工程、场景、跨包验证和文档;`Assets/Modules/` 下每个包是同路径 Git submodule,相邻目录 `.meta` 仍由根仓库追踪,避免 Unity GUID 和既有引用变化。一次干净克隆必须使用递归 submodule 初始化,Unity 只需要恢复外部 UPM 依赖。
它提供四层能力:
@@ -14,13 +14,14 @@ ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK 单仓库,不
3. 应用与扩展宿主:ShrinkApp、Starter、ModFramework。
4. 边界适配与生成工具:`*.Integration.*`、共享 IL 后处理器、独立服务器生成器。
当前默认应用主路径是 `ContextLoader``ClassicHost` 和旧 installer 只作为兼容面保留,不再承载新架构能力
当前默认应用主路径是 `ContextLoader``ClassicHost` 仅是既有运行时兼容面;包分发与安装只使用公开 registry、精确 Git 标签或 `Installer` 引导包,不保留旧安装入口
## 2. 开发原则
### 2.1 包是发布边界,asmdef 是编译边界
- 每个 `Assets/Modules/<Module>/package.json` 都必须能被独立 UPM 消费。
- 公开发布先走 `https://git.crash.work/api/packages/ShrinkSDK/npm/``com.cneicy` scoped registryGit 安装只能固定到对应 `vX.Y.Z` 标签,禁止浮动分支。
- 主包不反向依赖集成包;跨模块行为放入 `*.Integration.*` 或 Context adapter。
- asmdef 只声明实际编译依赖,不依靠根工程中偶然存在的程序集。
- 内部包依赖版本必须等于被依赖包自身的 `version`,不接受“根工程能编译所以先放着”的漂移。
@@ -63,14 +64,16 @@ ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK 单仓库,不
## 3. 仓库结构
```text
ShrinkSDK/
|-- Assets/Modules/ 20根仓库直接跟踪的 UPM 包
ShrinkSDK Workspace/
|-- Assets/Modules/ 21同路径 Git submodule UPM 包
|-- Assets/Modules/*.meta 根仓库追踪的 Unity 目录 GUID
|-- Assets/Scenes/ 示例与验收场景
|-- Assets/Resources/ 当前应用配置与组合 Profile
|-- GeneratedServers/ 独立 .NET 宿主、生成合同与烟测工程
|-- GeneratedModSdk/ Mod SDK 导出物
|-- Packages/ 根 Unity 工程依赖
|-- Tools/UpmConsumerValidation/ 干净 UPM 消费工程验证
|-- Tools/RepositoryMigration/ 子模块、发布仓库与独立宿主初始化脚本
|-- Docs/Archive/ 已完成迁移与过期地图,仅供追溯
|-- DESIGN.md 唯一当前架构文档
|-- NETWORK_PITFALLS.md 网络实现经验记录,不是架构基线
@@ -86,7 +89,7 @@ ShrinkSDK/
| `com.cneicy.shrink-datasaver` | 2.2.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 |
| `com.cneicy.shrink-command` | 0.2.0 | 路径式命令、权限与同步/异步执行 |
| `com.cneicy.shrink-network` | 0.2.0 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 |
| `com.cneicy.shrink-mod-framework` | 0.2.0 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease |
| `com.cneicy.shrink-mod-framework` | 0.2.1 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease |
| `com.cneicy.shrink-tutorial` | 0.1.0 | 数据驱动引导、遮罩、锚点、触发与持久化 |
| `com.cneicy.shrink-context-core` | 0.1.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 |
| `com.cneicy.shrink-app-core` | 0.1.1 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 |
@@ -101,6 +104,7 @@ ShrinkSDK/
| `com.cneicy.shrink-network-integration-eventbus` | 0.1.1 | 网络事件广播、裁决结果与 delta 去重 |
| `com.cneicy.shrink-network-integration-app` | 0.1.0 | Network App installer/原生 Context 组件 |
| `com.cneicy.shrink-shared-codegen` | 0.1.0 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 |
| `com.cneicy.shrink-installer` | 0.1.2 | 安全合并公开 registry、显示诊断并固定版本安装 Starter 或选定模块的 Editor 引导包 |
`ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。
@@ -221,13 +225,13 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
1. 内部包图检查:全部 `com.cneicy.*` 依赖版本一致、无循环,普通主包不反向依赖 Integration 包。
2. Unity 编译:目标 asmdef 和根项目无编译错误。
3. EditMode 测试:功能测试、Context 生命周期、语义扫描和模板覆写保护。
4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 20 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。
4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 在递归 submodule 初始化后的 Workspace 中创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 21 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。发布后还必须分别验证 registry、精确 Git URL 和 Installer 三种空白消费者工程路径。
5. 独立宿主:生成工程与 RuntimeSmoke 按变更范围构建或运行。
6. 涉及真实生命周期时,仍需在目标场景执行 Play Mode 验收;源码检查和 EditMode 不能替代该路径。
## 10. 已知边界
- ClassicHost、旧 installer 和静态门面仍是兼容层,暂未删除
- ClassicHost 和静态门面仍是运行时兼容层;旧 installer 不再是受支持的包安装入口
- Context 的 intercept 不是进程级沙箱;外部程序集 revision 不可从 Mono 卸载。
- 某些第三方或领域副作用只能补偿,不能保证物理撤回。
- 服务器生成器输出可编译、可注册的合同和扩展点,不推断完整业务规则。
@@ -249,6 +253,7 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
- 当前架构只更新本文。
- 包级使用方式和 API 示例放在各包 README。
- 包源码、标签、独立开发宿主与包级 CI 位于 `https://git.crash.work/ShrinkSDK/<Package>`;发布版本只能由与 `package.json.version` 一致的 `vX.Y.Z` 标签产生。
- `NETWORK_PITFALLS.md` 记录实现经验,不描述当前模块清单。
- `Docs/Archive/CORDIS_MIGRATION.completed.md` 保存迁移论证、阶段记录和历史验收数据。
- `Docs/Archive/codebase-map-2026-05-23/` 保存迁移前代码地图,其中关于 Git 状态、模块规模和测试覆盖的描述均已过期。
+45
View File
@@ -0,0 +1,45 @@
# ShrinkSDK Workspace
ShrinkSDK 的 Workspace 用于跨包集成、场景验证、生成器验证和 SDK 文档。每个 `Assets/Modules/<Module>` 是独立公开仓库的 Git submodule;根仓库保留相邻 `.meta`,因此不要删除或重新生成这些文件。
## 获取 Workspace
```text
git clone --recurse-submodules https://git.crash.work/ShrinkSDK/Workspace.git
cd Workspace
git submodule update --init --recursive
```
所有模块都可以独立打开其 `Development~/UnityProject`。根 Workspace 仅用于跨包组合与验收。
## 安装 SDK
首选公开 Gitea NPM/UPM registry,在消费者项目的 `Packages/manifest.json` 中保留既有配置并加入:
```json
{
"scopedRegistries": [
{
"name": "ShrinkSDK",
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
"scopes": ["com.cneicy"]
}
]
}
```
然后按精确版本添加包,例如 `com.cneicy.shrink-app-starter-basic: 0.1.0`。Git 备用安装同样必须固定标签,例如:
```text
https://git.crash.work/ShrinkSDK/ShrinkEventBus.git#v2.0.0
```
也可先通过固定标签安装 Editor 引导包:
```text
https://git.crash.work/ShrinkSDK/Installer.git#v0.1.2
```
随后在 Unity 打开 `ShrinkSDK/Packages`。安装器只合并 `com.cneicy` registry 并安装内置目录中的固定版本,不写认证信息、不复制模板到 `Assets`
发布、子模块迁移和独立开发宿主生成规则位于 [Tools/RepositoryMigration/Initialize-ShrinkSdkPackageRepositories.ps1](Tools/RepositoryMigration/Initialize-ShrinkSdkPackageRepositories.ps1)。架构约束与跨包验证要求见 [DESIGN.md](DESIGN.md)。
@@ -0,0 +1,429 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$StagingRoot,
[string]$SourceRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
)
$ErrorActionPreference = 'Stop'
$RegistryUrl = 'https://git.crash.work/api/packages/ShrinkSDK/npm/'
$RegistryScope = 'com.cneicy'
$RepositoryBaseUrl = 'https://git.crash.work/ShrinkSDK'
$UnityVersion = '2022.3.62f3'
$UnityRevision = '96770f904ca7'
$PackageDirectories = @(
'ShrinkApp.Core',
'ShrinkApp.Starter.Basic',
'ShrinkCommand',
'ShrinkCommand.Integration.App',
'ShrinkCommand.Integration.EventBus',
'ShrinkCommand.Integration.Network',
'ShrinkContext.AppAdapter',
'ShrinkContext.Core',
'ShrinkContext.EventBusAdapter',
'ShrinkDataSaver',
'ShrinkDataSaver.Integration.App',
'ShrinkDataSaver.Integration.EventBus',
'ShrinkEventBus',
'ShrinkEventBus.Entities',
'ShrinkInstaller',
'ShrinkModFramework',
'ShrinkNetwork',
'ShrinkNetwork.Integration.App',
'ShrinkNetwork.Integration.EventBus',
'ShrinkShared.CodeGen',
'ShrinkTutorial'
)
function Set-JsonProperty {
param(
[Parameter(Mandatory)] [object]$Target,
[Parameter(Mandatory)] [string]$Name,
[Parameter(Mandatory)] [object]$Value
)
$property = $Target.PSObject.Properties[$Name]
if ($null -eq $property) {
$Target | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
return
}
$property.Value = $Value
}
function Write-Utf8File {
param(
[Parameter(Mandatory)] [string]$Path,
[Parameter(Mandatory)] [AllowEmptyString()] [string]$Content
)
$parent = Split-Path -Parent $Path
New-Item -ItemType Directory -Path $parent -Force | Out-Null
[System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false))
}
function Update-PackageMetadata {
param(
[Parameter(Mandatory)] [string]$PackageRoot,
[Parameter(Mandatory)] [string]$RepositoryName
)
$packagePath = Join-Path $PackageRoot 'package.json'
$package = Get-Content -LiteralPath $packagePath -Raw | ConvertFrom-Json
Set-JsonProperty -Target $package -Name 'documentationUrl' -Value "$RepositoryBaseUrl/$RepositoryName"
$changelogPath = Join-Path $PackageRoot 'CHANGELOG.md'
if (Test-Path -LiteralPath $changelogPath) {
Set-JsonProperty -Target $package -Name 'changelogUrl' -Value "$RepositoryBaseUrl/$RepositoryName/src/branch/main/CHANGELOG.md"
}
$licensePath = Join-Path $PackageRoot 'LICENSE'
if (Test-Path -LiteralPath $licensePath) {
Set-JsonProperty -Target $package -Name 'licensesUrl' -Value "$RepositoryBaseUrl/$RepositoryName/src/branch/main/LICENSE"
}
if ($null -eq $package.author) {
$package | Add-Member -MemberType NoteProperty -Name 'author' -Value ([PSCustomObject]@{})
}
Set-JsonProperty -Target $package.author -Name 'name' -Value 'cneicy'
Set-JsonProperty -Target $package.author -Name 'url' -Value $RepositoryBaseUrl
Write-Utf8File -Path $packagePath -Content (($package | ConvertTo-Json -Depth 16) + [Environment]::NewLine)
Get-ChildItem -LiteralPath $PackageRoot -Recurse -File -Filter '*.md' | ForEach-Object {
$content = Get-Content -LiteralPath $_.FullName -Raw
$updated = $content.Replace('https://github.com/cneicy/', "$RepositoryBaseUrl/")
if ($updated -ne $content) {
Write-Utf8File -Path $_.FullName -Content $updated
}
}
}
function Write-PublishFiles {
param(
[Parameter(Mandatory)] [string]$PackageRoot,
[Parameter(Mandatory)] [string]$PackageName,
[Parameter(Mandatory)] [string]$RepositoryName
)
$npmIgnore = @'
.git/
.gitea/
Development~/
Tools~/
*.csproj
*.sln
*.user
*.DotSettings.user
'@
Write-Utf8File -Path (Join-Path $PackageRoot '.npmignore') -Content ($npmIgnore + [Environment]::NewLine)
$repositoryGitIgnore = @'
/Development~/UnityProject/[Ll]ibrary/
/Development~/UnityProject/[Tt]emp/
/Development~/UnityProject/[Oo]bj/
/Development~/UnityProject/[Ll]ogs/
/Development~/UnityProject/[Uu]ser[Ss]ettings/
/Development~/UnityProject/TestResults/
/Tools~/**/[Bb]in/
/Tools~/**/[Oo]bj/
*.user
*.DotSettings.user
'@
$repositoryGitIgnorePath = Join-Path $PackageRoot '.gitignore'
if (Test-Path -LiteralPath $repositoryGitIgnorePath) {
$repositoryGitIgnore = (Get-Content -LiteralPath $repositoryGitIgnorePath -Raw).TrimEnd() + [Environment]::NewLine + $repositoryGitIgnore
}
Write-Utf8File -Path $repositoryGitIgnorePath -Content ($repositoryGitIgnore + [Environment]::NewLine)
$publishWorkflow = @'
name: Publish UPM package
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
steps:
- name: Fetch exact tagged release archive
env:
GITEA_REF: ${{ gitea.ref }}
shell: bash
run: |
set -eu
tag="${GITEA_REF#refs/tags/}"
case "$tag" in
v[0-9]*) ;;
*) echo "Expected a version tag ref, got: $GITEA_REF" >&2; exit 1 ;;
esac
export SHRINKSDK_ARCHIVE_URL="https://git.crash.work/ShrinkSDK/__REPOSITORY_NAME__/archive/${tag}.tar.gz"
node --input-type=module <<'NODE'
import { writeFile } from 'node:fs/promises';
const response = await fetch(process.env.SHRINKSDK_ARCHIVE_URL);
if (!response.ok) {
throw new Error(`Release archive download failed: ${response.status} ${response.statusText}`);
}
await writeFile('release.tar.gz', new Uint8Array(await response.arrayBuffer()));
NODE
mkdir release
tar -xzf release.tar.gz --strip-components=1 -C release
rm -f release.tar.gz
printf '%s' "$tag" > release/.shrink-sdk-release-tag
- name: Validate immutable release version
shell: bash
run: |
set -eu
cd release
tag="$(cat .shrink-sdk-release-tag)"
version="$(node -p "require('./package.json').version")"
test "$tag" = "v$version"
npm pack --dry-run
- name: Publish to ShrinkSDK registry
shell: bash
run: |
set -eu
: "${NODE_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
cd release
npmrc="$HOME/.npmrc"
cleanup() { rm -f "$npmrc"; }
trap cleanup EXIT
printf '%s\n' \
'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \
'//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc"
npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/
'@
$publishWorkflow = $publishWorkflow.Replace('__REPOSITORY_NAME__', $RepositoryName)
Write-Utf8File -Path (Join-Path $PackageRoot '.gitea/workflows/publish.yml') -Content $publishWorkflow
$verifyWorkflow = @'
name: Verify standalone Unity package
on:
workflow_dispatch:
jobs:
editmode:
runs-on: unity-2022.3.62f3
container:
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
volumes:
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
steps:
- name: Fetch selected revision
shell: bash
run: |
set -eu
ref="${{ gitea.sha }}"
git init .
git remote add origin "https://git.crash.work/ShrinkSDK/__REPOSITORY_NAME__.git"
git fetch --depth=1 origin "$ref"
git checkout --detach FETCH_HEAD
- name: Run package EditMode tests
shell: bash
run: |
set -eu
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
test -n "$unity_bin"
"$unity_bin" \
-batchmode \
-nographics \
-quit \
-projectPath "$PWD/Development~/UnityProject" \
-runTests \
-testPlatform EditMode \
-testResults "$PWD/TestResults/editmode.xml" \
-logFile "$PWD/TestResults/unity.log"
'@
$verifyWorkflow = $verifyWorkflow.Replace('__REPOSITORY_NAME__', $RepositoryName)
Write-Utf8File -Path (Join-Path $PackageRoot '.gitea/workflows/unity-verify.yml') -Content $verifyWorkflow
$manifest = [ordered]@{
scopedRegistries = @(
[ordered]@{
name = 'ShrinkSDK'
url = $RegistryUrl
scopes = @($RegistryScope)
}
)
dependencies = [ordered]@{
'com.unity.test-framework' = '1.1.33'
$PackageName = 'file:../../..'
}
}
$developmentRoot = Join-Path $PackageRoot 'Development~/UnityProject'
Write-Utf8File -Path (Join-Path $developmentRoot 'Packages/manifest.json') -Content (($manifest | ConvertTo-Json -Depth 12) + [Environment]::NewLine)
Write-Utf8File -Path (Join-Path $developmentRoot 'ProjectSettings/ProjectVersion.txt') -Content "m_EditorVersion: $UnityVersion`nm_EditorVersionWithRevision: $UnityVersion ($UnityRevision)`n"
Write-Utf8File -Path (Join-Path $developmentRoot 'Assets/.gitkeep') -Content ''
Write-Utf8File -Path (Join-Path $developmentRoot '.gitignore') -Content @'
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Ll]ogs/
[Uu]ser[Ss]ettings/
TestResults/
'@
}
function Add-EventBusDotNetTools {
param(
[Parameter(Mandatory)] [string]$PackageRoot,
[Parameter(Mandatory)] [string]$RepositorySourceRoot
)
$source = Join-Path $RepositorySourceRoot 'DotNet'
$destination = Join-Path $PackageRoot 'Tools~/DotNet'
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
Copy-Item -LiteralPath $source -Destination $destination -Recurse -Force
$coreProjectPath = Join-Path $destination 'ShrinkEventBus.Core/ShrinkEventBus.Core.csproj'
$coreProject = Get-Content -LiteralPath $coreProjectPath -Raw
$coreProject = $coreProject.Replace('..\..\Assets\Modules\ShrinkEventBus\Runtime\', '..\..\..\Runtime\')
Write-Utf8File -Path $coreProjectPath -Content $coreProject
$readmePath = Join-Path $destination 'README.md'
$readme = Get-Content -LiteralPath $readmePath -Raw
$readme = $readme.Replace('DotNet/', 'Tools~/DotNet/')
Write-Utf8File -Path $readmePath -Content $readme
}
function Add-ModFrameworkFixtureBuilder {
param(
[Parameter(Mandatory)] [string]$PackageRoot,
[Parameter(Mandatory)] [string]$RepositorySourceRoot
)
$source = Join-Path $RepositorySourceRoot 'Tools/ShrinkModFixtureBuilder'
$destination = Join-Path $PackageRoot 'Tools~/FixtureBuilder'
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
Copy-Item -LiteralPath $source -Destination $destination -Recurse -Force
$fixtureProject = @'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<AssemblyName>ExternalFixture.Mod</AssemblyName>
<RootNamespace>ExternalFixture</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="ExternalFixture.Mod.$(FixtureVariant).cs" />
<Reference Include="ShrinkModFramework.Runtime">
<HintPath>$(RuntimeAssemblyPath)</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
'@
Write-Utf8File -Path (Join-Path $destination 'ShrinkModFixtureBuilder.csproj') -Content $fixtureProject
$rebuildScript = @'
[CmdletBinding()]
param(
[string]$UnityProjectRoot
)
$ErrorActionPreference = 'Stop'
$packageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '../..')).Path
if (-not $UnityProjectRoot) {
$UnityProjectRoot = Join-Path $packageRoot 'Development~/UnityProject'
}
$resolvedUnityProjectRoot = (Resolve-Path -LiteralPath $UnityProjectRoot).Path
$fixtureProject = Join-Path $PSScriptRoot 'ShrinkModFixtureBuilder.csproj'
$fixtureOutputRoot = Join-Path $resolvedUnityProjectRoot 'Temp/ShrinkModFixtureBuilder'
$fixtureTargetRoot = Join-Path $packageRoot 'Tests/Fixtures'
$runtimeAssemblyPath = Join-Path $resolvedUnityProjectRoot 'Temp/Bin/Debug/ShrinkModFramework.Runtime/ShrinkModFramework.Runtime.dll'
if (-not (Test-Path -LiteralPath $runtimeAssemblyPath)) {
throw "ShrinkModFramework.Runtime.dll was not found. Compile the Unity development host first: $resolvedUnityProjectRoot"
}
foreach ($variant in 'V1', 'V2', 'V3') {
$outputDirectory = Join-Path $fixtureOutputRoot $variant
$intermediateDirectory = Join-Path $fixtureOutputRoot "obj/$variant/"
dotnet build $fixtureProject /m:1 --nologo `
-p:FixtureVariant=$variant `
-p:RuntimeAssemblyPath=$runtimeAssemblyPath `
-p:BaseIntermediateOutputPath=$intermediateDirectory `
-p:OutputPath=$outputDirectory
if ($LASTEXITCODE -ne 0) {
throw "External fixture $variant build failed with exit code $LASTEXITCODE."
}
$source = Join-Path $outputDirectory 'ExternalFixture.Mod.dll'
$targetName = switch ($variant) {
'V1' { 'ExternalFixture.Mod.V1.dll.bytes' }
'V2' { 'ExternalFixture.Mod.V2.Failing.dll.bytes' }
'V3' { 'ExternalFixture.Mod.V3.dll.bytes' }
}
Copy-Item -LiteralPath $source -Destination (Join-Path $fixtureTargetRoot $targetName) -Force
}
Write-Host 'ShrinkModFramework external fixture DLLs rebuilt.'
'@
Write-Utf8File -Path (Join-Path $destination 'Rebuild-ShrinkModFixtures.ps1') -Content $rebuildScript
}
$resolvedSourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path
if (Test-Path -LiteralPath $StagingRoot) {
throw "StagingRoot already exists: $StagingRoot"
}
New-Item -ItemType Directory -Path $StagingRoot -Force | Out-Null
$releaseMap = [System.Collections.Generic.List[object]]::new()
foreach ($directory in $PackageDirectories) {
$source = Join-Path $resolvedSourceRoot "Assets/Modules/$directory"
if (-not (Test-Path -LiteralPath $source)) {
throw "Package source was not found: $source"
}
$destination = Join-Path $StagingRoot $directory
Copy-Item -LiteralPath $source -Destination $destination -Recurse -Force
$repositoryName = if ($directory -eq 'ShrinkInstaller') { 'Installer' } else { $directory }
Update-PackageMetadata -PackageRoot $destination -RepositoryName $repositoryName
$package = Get-Content -LiteralPath (Join-Path $destination 'package.json') -Raw | ConvertFrom-Json
Write-PublishFiles -PackageRoot $destination -PackageName $package.name -RepositoryName $repositoryName
if ($directory -eq 'ShrinkEventBus') {
Add-EventBusDotNetTools -PackageRoot $destination -RepositorySourceRoot $resolvedSourceRoot
}
if ($directory -eq 'ShrinkModFramework') {
Add-ModFrameworkFixtureBuilder -PackageRoot $destination -RepositorySourceRoot $resolvedSourceRoot
}
$releaseMap.Add([PSCustomObject]@{
directory = $directory
repositoryName = $repositoryName
package = $package.name
version = $package.version
repository = "$RepositoryBaseUrl/$repositoryName.git"
})
}
Write-Utf8File -Path (Join-Path $StagingRoot 'package-map.json') -Content (($releaseMap | ConvertTo-Json -Depth 8) + [Environment]::NewLine)
Write-Host "Prepared $($releaseMap.Count) package repositories in $StagingRoot"