From 4738e9ac7d6701d7182435b905c0fc183749f252 Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:14:50 +0900 Subject: [PATCH 1/9] ci(workflows): run Unity builds and tests with Unity CLI Why Use Unity CLI to manage the Editor and license directly, and focus CI on the supported Editor behavior and one player target. What Changed Replace GameCI steps with a shared Unity setup action and direct test, build, and package export commands. Keep EditMode coverage on Unity 2021.3 and 2023.2, reduce player builds to Linux 64-bit, retain logs and artifacts, and update the CI instructions. Impact UNITY_LICENSE is the only Unity secret consumed by the workflows. Fork pull requests continue to receive no repository secrets. Local validation passed both EditMode suites, the Linux player build, package export, and workflow linting; hosted-runner validation follows in the PR. --- .github/actions/setup-unity/action.yaml | 73 +++++++++++++++++++++++++ .github/workflows/build.yaml | 71 +++++++++++++----------- .github/workflows/package.yaml | 56 +++++++++++-------- .github/workflows/release.yaml | 49 ++++++++++------- .github/workflows/tests.yaml | 60 ++++++++++---------- AGENTS.md | 20 +++---- 6 files changed, 214 insertions(+), 115 deletions(-) create mode 100644 .github/actions/setup-unity/action.yaml diff --git a/.github/actions/setup-unity/action.yaml b/.github/actions/setup-unity/action.yaml new file mode 100644 index 0000000..6721880 --- /dev/null +++ b/.github/actions/setup-unity/action.yaml @@ -0,0 +1,73 @@ +name: Set up Unity +description: Install Unity CLI and the Editor, then activate the supplied license. + +inputs: + editor-version: + description: Exact Unity Editor version to install. + required: true + license: + description: Contents of a Unity license file valid for the runner. + required: true + +runs: + using: composite + steps: + - name: Check license input + shell: bash + env: + UNITY_LICENSE: ${{ inputs.license }} + run: | + if [[ -z "$UNITY_LICENSE" ]]; then + echo "::error::UNITY_LICENSE is required. GitHub does not provide repository secrets to pull requests from forks." + exit 1 + fi + + - name: Install Linux dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends \ + libasound2 libgbm1 libgconf-2-4 libglu1-mesa libgtk-3-0 \ + libncurses5 libnotify4 libnss3 libxss1 libxtst6 + + # Unity 2021's .NET tooling requires OpenSSL 1.1, absent from Ubuntu 22.04. + - name: Install OpenSSL 1.1 for Unity 2021 + if: startsWith(inputs.editor-version, '2021.') + shell: bash + run: | + curl --fail --silent --show-error --location \ + https://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.24_amd64.deb \ + --output "$RUNNER_TEMP/libssl1.1.deb" + echo "7cf39d70a639017d1dd7c8d36daa2258063608688e449fddf40ffdd46f992a78 $RUNNER_TEMP/libssl1.1.deb" | sha256sum --check + sudo apt-get install --yes --no-install-recommends "$RUNNER_TEMP/libssl1.1.deb" + + - name: Install Unity CLI + shell: bash + env: + UNITY_CLI_CHANNEL: beta + UNITY_CLI_VERSION: 1.0.0-beta.12 + UNITY_CLI_HOME: ${{ runner.temp }}/unity-cli + run: | + curl --fail --silent --show-error --location \ + https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh \ + --output "$RUNNER_TEMP/install-unity-cli.sh" + bash "$RUNNER_TEMP/install-unity-cli.sh" + echo "$UNITY_CLI_HOME/bin" >> "$GITHUB_PATH" + "$UNITY_CLI_HOME/bin/unity" --version + + - name: Install Unity Editor + shell: bash + env: + UNITY_EDITOR_VERSION: ${{ inputs.editor-version }} + run: unity --non-interactive install "$UNITY_EDITOR_VERSION" --yes --accept-eula + + - name: Activate Unity license + shell: bash + env: + UNITY_LICENSE: ${{ inputs.license }} + run: | + umask 077 + license_file="$(mktemp --suffix=.ulf "$RUNNER_TEMP/unity-license-XXXXXX")" + trap 'rm -f "$license_file"' EXIT + printf '%s' "$UNITY_LICENSE" > "$license_file" + unity --non-interactive license activate --file "$license_file" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3f31c6f..7233a9e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,47 +3,56 @@ name: Build on: pull_request: {} push: { branches: [main] } + workflow_dispatch: {} + +permissions: + contents: read jobs: build: - name: ${{ matrix.targetPlatform }} - if: "((github.event_name == 'push' && github.repository_owner == 'mackysoft') || startsWith(github.event.pull_request.head.label, 'mackysoft:'))" - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - targetPlatform: - - StandaloneOSX # Build a macOS standalone (Intel 64-bit). - - StandaloneWindows # Build a Windows standalone. - - StandaloneWindows64 # Build a Windows 64-bit standalone. - - StandaloneLinux64 # Build a Linux 64-bit standalone. - - iOS # Build an iOS player. - - Android # Build an Android .apk standalone app. - - WebGL # WebGL. - + name: StandaloneLinux64 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-22.04 + timeout-minutes: 45 steps: - # Checkout - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 lfs: true - # Cache - - name: Cache + - name: Set up Unity + uses: ./.github/actions/setup-unity + with: + editor-version: 2021.3.45f2 + license: ${{ secrets.UNITY_LICENSE }} + + - name: Cache Library uses: actions/cache@v4 with: path: Library - key: Library-${{ matrix.targetPlatform }} - restore-keys: Library- - - # Build - - name: Build - uses: game-ci/unity-builder@v4 - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + key: Library-${{ runner.os }}-2021.3.45f2-StandaloneLinux64-${{ hashFiles('Packages/manifest.json', 'Packages/packages-lock.json', 'ProjectSettings/ProjectVersion.txt') }} + restore-keys: Library-${{ runner.os }}-2021.3.45f2-StandaloneLinux64- + + - name: Build example player + run: | + unity --non-interactive build . \ + --editor-version 2021.3.45f2 \ + --target StandaloneLinux64 \ + --output-path Build/StandaloneLinux64/SerializeReferenceExtensions \ + --log-file Logs/build.log \ + --timeout 900 + tar -czf Build/StandaloneLinux64.tar.gz -C Build StandaloneLinux64 + + - name: Upload player + uses: actions/upload-artifact@v4 + with: + name: StandaloneLinux64 + path: Build/StandaloneLinux64.tar.gz + if-no-files-found: error + + - name: Upload build log + if: always() + uses: actions/upload-artifact@v4 with: - targetPlatform: ${{ matrix.targetPlatform }} - unityVersion: 2021.3.45f2 + name: Build-Logs + path: Logs/ diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index f83c6c3..b127aba 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -3,43 +3,53 @@ name: Export Package on: pull_request: {} push: { branches: [main] } + workflow_dispatch: {} -env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} +permissions: + contents: read jobs: build: name: Build UnityPackage - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + timeout-minutes: 45 steps: - # Checkout - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 with: lfs: true - # Cache - - name: Cache + - name: Set up Unity + uses: ./.github/actions/setup-unity + with: + editor-version: 2021.3.45f2 + license: ${{ secrets.UNITY_LICENSE }} + + - name: Cache Library uses: actions/cache@v4 with: path: Library - key: Library - restore-keys: Library- - - # Build - - name: Build .unitypackage - uses: game-ci/unity-builder@v4 - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - with: - unityVersion: 2021.3.45f2 - buildMethod: MackySoft.PackageTools.Editor.UnityPackageExporter.Export + key: Library-${{ runner.os }}-2021.3.45f2-Package-${{ hashFiles('Packages/manifest.json', 'Packages/packages-lock.json', 'ProjectSettings/ProjectVersion.txt') }} + restore-keys: Library-${{ runner.os }}-2021.3.45f2-Package- + + - name: Export unitypackage + run: | + unity --non-interactive run . \ + --editor-version 2021.3.45f2 \ + --log-file Logs/package.log \ + --timeout 900 \ + -- -nographics -executeMethod MackySoft.PackageTools.Editor.UnityPackageExporter.Export - # Upload - - name: Upload .unitypackage + - name: Upload unitypackage uses: actions/upload-artifact@v4 with: name: Unity Package - path: Build + path: Build/SerializeReference-Extensions.unitypackage + if-no-files-found: error + + - name: Upload export log + if: always() + uses: actions/upload-artifact@v4 + with: + name: Package-Logs + path: Logs/ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 57149e8..dbb22e7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -63,32 +63,28 @@ jobs: build: needs: [update-packagejson] - strategy: - matrix: - unity: ["2021.3.45f2"] - include: - - unityVersion: 2021.3.45f2 - license: UNITY_LICENSE - runs-on: ubuntu-latest - timeout-minutes: 15 + runs-on: ubuntu-22.04 + timeout-minutes: 45 steps: - run: echo ${{ needs.update-packagejson.outputs.sha }} - uses: actions/checkout@v4 with: ref: ${{ needs.update-packagejson.outputs.sha }} + lfs: true - - name: Export unitypackage - uses: game-ci/unity-builder@v4 - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + - name: Set up Unity + uses: ./.github/actions/setup-unity with: - projectPath: . - unityVersion: ${{ matrix.unityVersion }} - targetPlatform: StandaloneLinux64 - buildMethod: MackySoft.PackageTools.Editor.UnityPackageExporter.Export - versioning: None + editor-version: 2021.3.45f2 + license: ${{ secrets.UNITY_LICENSE }} + + - name: Export unitypackage + run: | + unity --non-interactive run . \ + --editor-version 2021.3.45f2 \ + --log-file Logs/package.log \ + --timeout 900 \ + -- -nographics -executeMethod MackySoft.PackageTools.Editor.UnityPackageExporter.Export - name: check all .meta is commited run: | @@ -105,16 +101,27 @@ jobs: with: name: SerializeReference-Extensions.unitypackage path: ./Build/SerializeReference-Extensions.unitypackage + if-no-files-found: error + + - name: Upload export log + if: always() + uses: actions/upload-artifact@v4 + with: + name: Release-Logs + path: Logs/ create-release: needs: [update-packagejson, build] runs-on: ubuntu-latest steps: - # Download(All) Artifacts to current directory + # Download the package artifact for the release. - uses: actions/download-artifact@v4 + with: + name: SerializeReference-Extensions.unitypackage + path: SerializeReference-Extensions.unitypackage # Create release and upload unitypackage. - - uses: softprops/action-gh-release@v1 + - uses: softprops/action-gh-release@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1a4225f..e182dea 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -6,23 +6,22 @@ on: - documentation - gh-pages pull_request: {} + workflow_dispatch: {} + +permissions: + contents: read jobs: test: - name: ${{ matrix.testMode }} on Unity ${{ matrix.unityVersion }} - - runs-on: ubuntu-latest - + name: EditMode on Unity ${{ matrix.unityVersion }} + runs-on: ubuntu-22.04 + timeout-minutes: 45 strategy: fail-fast: false matrix: - projectPath: - - . unityVersion: - 2021.3.45f2 - 2023.2.22f1 - testMode: - - editmode steps: - name: Checkout @@ -30,26 +29,31 @@ jobs: with: lfs: true - - name: Cache + - name: Set up Unity + uses: ./.github/actions/setup-unity + with: + editor-version: ${{ matrix.unityVersion }} + license: ${{ secrets.UNITY_LICENSE }} + + - name: Cache Library uses: actions/cache@v4 with: - path: ${{ matrix.projectPath }}/Library - key: Library-${{ runner.os }}-${{ matrix.projectPath }}-${{ matrix.unityVersion }}-${{ matrix.testMode }}-${{ hashFiles('**/Packages/manifest.json', '**/Packages/packages-lock.json', '**/ProjectSettings/ProjectVersion.txt') }} - restore-keys: | - Library-${{ runner.os }}-${{ matrix.projectPath }}-${{ matrix.unityVersion }}-${{ matrix.testMode }}- - Library-${{ runner.os }}-${{ matrix.projectPath }}-${{ matrix.unityVersion }}- - Library-${{ runner.os }}-${{ matrix.projectPath }}- - - - name: Tests - uses: game-ci/unity-test-runner@v4 - id: tests - env: - UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + path: Library + key: Library-${{ runner.os }}-${{ matrix.unityVersion }}-EditMode-${{ hashFiles('Packages/manifest.json', 'Packages/packages-lock.json', 'ProjectSettings/ProjectVersion.txt') }} + restore-keys: Library-${{ runner.os }}-${{ matrix.unityVersion }}-EditMode- + + - name: Run EditMode tests + run: | + unity --non-interactive test . \ + --editor-version "${{ matrix.unityVersion }}" \ + --mode EditMode \ + --output Logs/TestResults/editmode.xml \ + --timeout 900 \ + -- -nographics -logFile Logs/editmode.log + + - name: Upload test results and logs + if: always() + uses: actions/upload-artifact@v4 with: - projectPath: ${{ matrix.projectPath }} - unityVersion: ${{ matrix.unityVersion }} - testMode: ${{ matrix.testMode }} - artifactsPath: test-results - artifactsName: TestResults-${{ matrix.unityVersion }}-${{ matrix.testMode }} + name: TestResults-${{ matrix.unityVersion }}-EditMode + path: Logs/ diff --git a/AGENTS.md b/AGENTS.md index 8a4dc9d..768bb68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,26 +17,22 @@ Primary risk area: type discovery & filtering (what types appear in the selector - Unity 2023.2+ has enhanced generic type support (variance, etc.). Changes must not break 2021.3 behavior and guarded by `UNITY_2023_2_OR_NEWER`. ## CI (GitHub Actions) -I use GameCI `unity-test-runner`. +Use Unity CLI through `.github/actions/setup-unity`. - Always run EditMode tests. +- Run tests on Linux; the example player build targets Linux 64-bit. - Run a Unity matrix that includes: - 2021.3.x (minimum baseline) - 2023.2+ (generic/variance feature gate) ## How to run tests locally ### EditMode -Run Unity in batchmode: +Run tests through Unity CLI (which manages batch mode): ``` -PROJECT_ROOT="$(pwd)" -RESULT_XML="$PROJECT_ROOT/TestResults/editmode.xml" - -mkdir -p "$(dirname "$RESULT_XML")" - -"" -batchmode -nographics -quit \ - -projectPath "$PROJECT_ROOT" \ - -runTests -testPlatform editmode \ - -testResults "$RESULT_XML" +unity --non-interactive test . --editor-version 2021.3.45f2 \ + --mode EditMode --output Logs/TestResults/editmode.xml --timeout 900 \ + -- -nographics -logFile Logs/editmode.log ``` +Repeat with `--editor-version 2023.2.22f1` for the generic/variance feature gate. ## Architecture guardrails - Runtime surface area should remain minimal (mainly attributes / data structures). @@ -48,4 +44,4 @@ mkdir -p "$(dirname "$RESULT_XML")" - Keep allocations low in IMGUI paths (e.g., `OnGUI`). - Keep public API stable; if changing type filtering behavior, add/adjust tests. - As a general rule, you should follow the restrictions on SerializeReference in Unity's official documentation. - - https://docs.unity3d.com/ScriptReference/SerializeReference.html \ No newline at end of file + - https://docs.unity3d.com/ScriptReference/SerializeReference.html From 114ff0e4ca6e68425f6deabdc58ab3bae7b2198d Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:19:35 +0900 Subject: [PATCH 2/9] ci(workflows): build and export only during releases Why Routine pushes and pull requests need EditMode coverage without the cost of player builds and package exports. What Changed Remove the standalone Build and Export Package workflows. Run the Linux example build and package export together in the manually dispatched Release workflow, and align the CI instructions. Impact Push and pull-request workflows run EditMode tests only. Release creation still requires a successful player build and package export. --- .github/workflows/build.yaml | 58 ---------------------------------- .github/workflows/package.yaml | 55 -------------------------------- .github/workflows/release.yaml | 19 ++++++++++- AGENTS.md | 3 +- 4 files changed, 20 insertions(+), 115 deletions(-) delete mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/package.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 7233a9e..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: Build - -on: - pull_request: {} - push: { branches: [main] } - workflow_dispatch: {} - -permissions: - contents: read - -jobs: - build: - name: StandaloneLinux64 - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-22.04 - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - - - name: Set up Unity - uses: ./.github/actions/setup-unity - with: - editor-version: 2021.3.45f2 - license: ${{ secrets.UNITY_LICENSE }} - - - name: Cache Library - uses: actions/cache@v4 - with: - path: Library - key: Library-${{ runner.os }}-2021.3.45f2-StandaloneLinux64-${{ hashFiles('Packages/manifest.json', 'Packages/packages-lock.json', 'ProjectSettings/ProjectVersion.txt') }} - restore-keys: Library-${{ runner.os }}-2021.3.45f2-StandaloneLinux64- - - - name: Build example player - run: | - unity --non-interactive build . \ - --editor-version 2021.3.45f2 \ - --target StandaloneLinux64 \ - --output-path Build/StandaloneLinux64/SerializeReferenceExtensions \ - --log-file Logs/build.log \ - --timeout 900 - tar -czf Build/StandaloneLinux64.tar.gz -C Build StandaloneLinux64 - - - name: Upload player - uses: actions/upload-artifact@v4 - with: - name: StandaloneLinux64 - path: Build/StandaloneLinux64.tar.gz - if-no-files-found: error - - - name: Upload build log - if: always() - uses: actions/upload-artifact@v4 - with: - name: Build-Logs - path: Logs/ diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml deleted file mode 100644 index b127aba..0000000 --- a/.github/workflows/package.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: Export Package - -on: - pull_request: {} - push: { branches: [main] } - workflow_dispatch: {} - -permissions: - contents: read - -jobs: - build: - name: Build UnityPackage - runs-on: ubuntu-22.04 - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - - - name: Set up Unity - uses: ./.github/actions/setup-unity - with: - editor-version: 2021.3.45f2 - license: ${{ secrets.UNITY_LICENSE }} - - - name: Cache Library - uses: actions/cache@v4 - with: - path: Library - key: Library-${{ runner.os }}-2021.3.45f2-Package-${{ hashFiles('Packages/manifest.json', 'Packages/packages-lock.json', 'ProjectSettings/ProjectVersion.txt') }} - restore-keys: Library-${{ runner.os }}-2021.3.45f2-Package- - - - name: Export unitypackage - run: | - unity --non-interactive run . \ - --editor-version 2021.3.45f2 \ - --log-file Logs/package.log \ - --timeout 900 \ - -- -nographics -executeMethod MackySoft.PackageTools.Editor.UnityPackageExporter.Export - - - name: Upload unitypackage - uses: actions/upload-artifact@v4 - with: - name: Unity Package - path: Build/SerializeReference-Extensions.unitypackage - if-no-files-found: error - - - name: Upload export log - if: always() - uses: actions/upload-artifact@v4 - with: - name: Package-Logs - path: Logs/ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dbb22e7..16ddd62 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -78,6 +78,16 @@ jobs: editor-version: 2021.3.45f2 license: ${{ secrets.UNITY_LICENSE }} + - name: Build example player + run: | + unity --non-interactive build . \ + --editor-version 2021.3.45f2 \ + --target StandaloneLinux64 \ + --output-path Build/StandaloneLinux64/SerializeReferenceExtensions \ + --log-file Logs/build.log \ + --timeout 900 + tar -czf Build/StandaloneLinux64.tar.gz -C Build StandaloneLinux64 + - name: Export unitypackage run: | unity --non-interactive run . \ @@ -97,13 +107,20 @@ jobs: working-directory: . # Store artifacts. + - name: Upload example player + uses: actions/upload-artifact@v4 + with: + name: StandaloneLinux64 + path: Build/StandaloneLinux64.tar.gz + if-no-files-found: error + - uses: actions/upload-artifact@v4 with: name: SerializeReference-Extensions.unitypackage path: ./Build/SerializeReference-Extensions.unitypackage if-no-files-found: error - - name: Upload export log + - name: Upload Unity logs if: always() uses: actions/upload-artifact@v4 with: diff --git a/AGENTS.md b/AGENTS.md index 768bb68..73662b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ Primary risk area: type discovery & filtering (what types appear in the selector ## CI (GitHub Actions) Use Unity CLI through `.github/actions/setup-unity`. - Always run EditMode tests. -- Run tests on Linux; the example player build targets Linux 64-bit. +- Run tests on Linux. +- Build the example player for Linux 64-bit and export the package only in the Release workflow. - Run a Unity matrix that includes: - 2021.3.x (minimum baseline) - 2023.2+ (generic/variance feature gate) From bb74d90c8dcce034b8aaf519230871fd3f736a8a Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:24:17 +0900 Subject: [PATCH 3/9] ci(workflows): require successful tests in the final verification check Why Branch rulesets need one stable status check that succeeds only after every required verification job has passed. What Changed Add a final Verification job that always runs after the EditMode matrix and fails unless the matrix result is success. Keep the existing push branch exclusions and document the check's contract. Impact A failed, cancelled, or skipped EditMode matrix cannot produce a successful Verification check. Validated all four dependency-result cases and workflow linting. --- .github/workflows/tests.yaml | 15 +++++++++++++++ AGENTS.md | 1 + 2 files changed, 16 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e182dea..ba4ec2f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -57,3 +57,18 @@ jobs: with: name: TestResults-${{ matrix.unityVersion }}-EditMode path: Logs/ + + verify: + name: Verification + needs: test + if: always() + runs-on: ubuntu-22.04 + steps: + - name: Require all EditMode tests to pass + env: + TEST_RESULT: ${{ needs.test.result }} + run: | + if [[ "$TEST_RESULT" != success ]]; then + echo "::error::EditMode validation did not pass ($TEST_RESULT)." + exit 1 + fi diff --git a/AGENTS.md b/AGENTS.md index 73662b0..7421a6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ Primary risk area: type discovery & filtering (what types appear in the selector Use Unity CLI through `.github/actions/setup-unity`. - Always run EditMode tests. - Run tests on Linux. +- Keep the final `Verification` check successful only when every EditMode matrix job succeeds. - Build the example player for Linux 64-bit and export the package only in the Release workflow. - Run a Unity matrix that includes: - 2021.3.x (minimum baseline) From de35fbd90e7f994649dbc66bc7b012919b405f3b Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:32:07 +0900 Subject: [PATCH 4/9] ci(workflows): pass verification when Unity tests are not required Why Required checks must report a result for documentation-only pull requests without treating missing required test results as successful. What Changed Detect Unity project and test setup changes before the EditMode matrix, and always evaluate the detection result and applicable test outcomes in Verification. Remove the badge for the deleted Build workflow. Impact Documentation-only pull requests can pass without starting Unity. Detection failures and failed, cancelled, or unexpectedly skipped required tests still fail Verification. --- .github/workflows/tests.yaml | 51 +++++++++++++++++++++++++++++++++--- AGENTS.md | 4 +-- README.md | 2 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ba4ec2f..3e502e3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -12,8 +12,37 @@ permissions: contents: read jobs: + changes: + name: Determine required checks + runs-on: ubuntu-22.04 + permissions: + contents: read + pull-requests: read + outputs: + unity: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.unity == 'true' }} + steps: + - name: Checkout + if: github.event_name == 'push' + uses: actions/checkout@v4 + + - name: Detect Unity changes + id: filter + if: github.event_name != 'workflow_dispatch' + uses: dorny/paths-filter@v4 + with: + filters: | + unity: + - 'Assets/**' + - 'Packages/**' + - 'ProjectSettings/**' + - '.github/actions/setup-unity/**' + - '.github/workflows/tests.yaml' + - '.gitattributes' + test: name: EditMode on Unity ${{ matrix.unityVersion }} + needs: changes + if: needs.changes.outputs.unity == 'true' runs-on: ubuntu-22.04 timeout-minutes: 45 strategy: @@ -60,15 +89,29 @@ jobs: verify: name: Verification - needs: test + needs: [changes, test] if: always() runs-on: ubuntu-22.04 steps: - - name: Require all EditMode tests to pass + - name: Require all applicable checks to pass env: + CHANGES_RESULT: ${{ needs.changes.result }} + UNITY_REQUIRED: ${{ needs.changes.outputs.unity }} TEST_RESULT: ${{ needs.test.result }} run: | - if [[ "$TEST_RESULT" != success ]]; then - echo "::error::EditMode validation did not pass ($TEST_RESULT)." + if [[ "$CHANGES_RESULT" != success ]]; then + echo "::error::Could not determine required checks ($CHANGES_RESULT)." exit 1 fi + case "$UNITY_REQUIRED:$TEST_RESULT" in + true:success) + echo "All required EditMode tests passed." + ;; + false:skipped) + echo "No Unity changes require EditMode tests." + ;; + *) + echo "::error::EditMode validation did not pass (required=$UNITY_REQUIRED, result=$TEST_RESULT)." + exit 1 + ;; + esac diff --git a/AGENTS.md b/AGENTS.md index 7421a6e..4101f7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,9 +18,9 @@ Primary risk area: type discovery & filtering (what types appear in the selector ## CI (GitHub Actions) Use Unity CLI through `.github/actions/setup-unity`. -- Always run EditMode tests. +- Run EditMode tests when Unity source, assets, project settings, or the test setup changes. Documentation-only changes can skip Unity tests. - Run tests on Linux. -- Keep the final `Verification` check successful only when every EditMode matrix job succeeds. +- Always report the final `Verification` check on pull requests. It passes only when change detection succeeds and all required EditMode matrix jobs succeed, or Unity tests are not required and are skipped. - Build the example player for Linux 64-bit and export the package only in the Release workflow. - Run a Unity matrix that includes: - 2021.3.x (minimum baseline) diff --git a/README.md b/README.md index 56e4638..3aa5077 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Unity SerializeReferenceExtensions -[![Tests](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/actions/workflows/tests.yaml/badge.svg)](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/actions/workflows/tests.yaml) [![Build](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/actions/workflows/build.yaml/badge.svg)](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/actions/workflows/build.yaml) [![Release](https://img.shields.io/github/v/release/mackysoft/Unity-SerializeReferenceExtensions)](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/releases) [![openupm](https://img.shields.io/npm/v/com.mackysoft.serializereference-extensions?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.mackysoft.serializereference-extensions/) +[![Tests](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/actions/workflows/tests.yaml/badge.svg)](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/actions/workflows/tests.yaml) [![Release](https://img.shields.io/github/v/release/mackysoft/Unity-SerializeReferenceExtensions)](https://github.com/mackysoft/Unity-SerializeReferenceExtensions/releases) [![openupm](https://img.shields.io/npm/v/com.mackysoft.serializereference-extensions?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.mackysoft.serializereference-extensions/) This library provides an Inspector dropdown (`SubclassSelector`) for fields serialized by Unity's `[SerializeReference]`. From 5020bba1035252fd1ea5c7538f8d7f38ab1a5d05 Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:36:20 +0900 Subject: [PATCH 5/9] ci(workflows): fail early when validation prerequisites are missing Why A missing change-filter output must not be interpreted as tests being unnecessary, and an invalid license should fail before downloading an Editor. What Changed Preserve the exact filter output for Verification and activate the Unity license before installing the requested Editor. Impact Unknown validation requirements fail the final check. License failures avoid the expensive Editor download. --- .github/actions/setup-unity/action.yaml | 12 ++++++------ .github/workflows/tests.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/setup-unity/action.yaml b/.github/actions/setup-unity/action.yaml index 6721880..f997b9c 100644 --- a/.github/actions/setup-unity/action.yaml +++ b/.github/actions/setup-unity/action.yaml @@ -55,12 +55,6 @@ runs: echo "$UNITY_CLI_HOME/bin" >> "$GITHUB_PATH" "$UNITY_CLI_HOME/bin/unity" --version - - name: Install Unity Editor - shell: bash - env: - UNITY_EDITOR_VERSION: ${{ inputs.editor-version }} - run: unity --non-interactive install "$UNITY_EDITOR_VERSION" --yes --accept-eula - - name: Activate Unity license shell: bash env: @@ -71,3 +65,9 @@ runs: trap 'rm -f "$license_file"' EXIT printf '%s' "$UNITY_LICENSE" > "$license_file" unity --non-interactive license activate --file "$license_file" + + - name: Install Unity Editor + shell: bash + env: + UNITY_EDITOR_VERSION: ${{ inputs.editor-version }} + run: unity --non-interactive install "$UNITY_EDITOR_VERSION" --yes --accept-eula diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3e502e3..2a145e6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,7 +19,7 @@ jobs: contents: read pull-requests: read outputs: - unity: ${{ github.event_name == 'workflow_dispatch' || steps.filter.outputs.unity == 'true' }} + unity: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.filter.outputs.unity }} steps: - name: Checkout if: github.event_name == 'push' From 37adad3db59364cc75c0c07b07615d819db8882d Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:48:52 +0900 Subject: [PATCH 6/9] ci(workflows): activate Unity licenses on each CI runner Why Desktop license files cannot be imported on ephemeral GitHub runners because their machine bindings differ. What Changed Use the existing Unity account secrets with the Editor's licensing client, verify the resulting license state, and return active licenses after each job. Impact Unity CLI still installs Editors and runs tests, builds, and exports. CI activation no longer depends on a desktop-bound UNITY_LICENSE file, and failures continue to block Verification. --- .github/actions/setup-unity/action.yaml | 69 +++++++++++++++++++------ .github/workflows/release.yaml | 8 ++- .github/workflows/tests.yaml | 8 ++- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/.github/actions/setup-unity/action.yaml b/.github/actions/setup-unity/action.yaml index f997b9c..c2e058c 100644 --- a/.github/actions/setup-unity/action.yaml +++ b/.github/actions/setup-unity/action.yaml @@ -1,24 +1,28 @@ name: Set up Unity -description: Install Unity CLI and the Editor, then activate the supplied license. +description: Install Unity CLI and the Editor, then activate a license on this runner. inputs: editor-version: description: Exact Unity Editor version to install. required: true - license: - description: Contents of a Unity license file valid for the runner. + email: + description: Unity account email address. + required: true + password: + description: Unity account password. required: true runs: using: composite steps: - - name: Check license input + - name: Check Unity credentials shell: bash env: - UNITY_LICENSE: ${{ inputs.license }} + UNITY_EMAIL: ${{ inputs.email }} + UNITY_PASSWORD: ${{ inputs.password }} run: | - if [[ -z "$UNITY_LICENSE" ]]; then - echo "::error::UNITY_LICENSE is required. GitHub does not provide repository secrets to pull requests from forks." + if [[ -z "$UNITY_EMAIL" || -z "$UNITY_PASSWORD" ]]; then + echo "::error::UNITY_EMAIL and UNITY_PASSWORD are required. GitHub does not provide repository secrets to pull requests from forks." exit 1 fi @@ -55,19 +59,50 @@ runs: echo "$UNITY_CLI_HOME/bin" >> "$GITHUB_PATH" "$UNITY_CLI_HOME/bin/unity" --version - - name: Activate Unity license + - name: Install Unity Editor shell: bash env: - UNITY_LICENSE: ${{ inputs.license }} - run: | - umask 077 - license_file="$(mktemp --suffix=.ulf "$RUNNER_TEMP/unity-license-XXXXXX")" - trap 'rm -f "$license_file"' EXIT - printf '%s' "$UNITY_LICENSE" > "$license_file" - unity --non-interactive license activate --file "$license_file" + UNITY_EDITOR_VERSION: ${{ inputs.editor-version }} + run: unity --non-interactive install "$UNITY_EDITOR_VERSION" --yes --accept-eula - - name: Install Unity Editor + # Unity CLI cannot sign in with email/password. The bundled client can + # activate a license for this runner instead of importing a desktop's ULF. + - name: Activate Unity license shell: bash env: UNITY_EDITOR_VERSION: ${{ inputs.editor-version }} - run: unity --non-interactive install "$UNITY_EDITOR_VERSION" --yes --accept-eula + UNITY_EMAIL: ${{ inputs.email }} + UNITY_PASSWORD: ${{ inputs.password }} + run: | + editor_root="$(unity --non-interactive editors path "$UNITY_EDITOR_VERSION")" + licensing_client="$editor_root/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client" + test -x "$licensing_client" + + umask 077 + activation_log="$(mktemp "$RUNNER_TEMP/unity-activation-XXXXXX")" + trap 'rm -f "$activation_log"' EXIT + # Arm cleanup before activation, including a partially failed request. + echo "UNITY_LICENSE_ACTIVATION_ATTEMPTED=true" >> "$GITHUB_ENV" + result=0 + timeout 120 "$licensing_client" \ + --activate-all --include-personal \ + --username "$UNITY_EMAIL" --password "$UNITY_PASSWORD" \ + > "$activation_log" 2>&1 || result=$? + + if [[ "$result" != 0 ]]; then + if grep -qiE 'no (available )?seats|seat is not available|LICENSE_LIMIT|maximum number of' "$activation_log"; then + echo "::error::Unity reports no available license seats for this account." + elif grep -qiE 'two.?factor|2fa|verification code|unauthorized|invalid (username|password|credentials)' "$activation_log"; then + echo "::error::Unity rejected the credentials or requires interactive account verification." + else + echo "::error::Unity account activation failed (exit $result)." + fi + exit 1 + fi + + # The client can exit zero without granting a seat. Check license state. + if ! unity --non-interactive license status > "$activation_log" 2>&1; then + echo "::error::Unity account activation did not produce an active license." + exit 1 + fi + echo "Unity license is active on this runner." diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 16ddd62..4ed887f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -76,7 +76,8 @@ jobs: uses: ./.github/actions/setup-unity with: editor-version: 2021.3.45f2 - license: ${{ secrets.UNITY_LICENSE }} + email: ${{ secrets.UNITY_EMAIL }} + password: ${{ secrets.UNITY_PASSWORD }} - name: Build example player run: | @@ -106,6 +107,11 @@ jobs: fi working-directory: . + - name: Return Unity license + if: always() && env.UNITY_LICENSE_ACTIVATION_ATTEMPTED == 'true' + timeout-minutes: 3 + run: unity --non-interactive license return --yes + # Store artifacts. - name: Upload example player uses: actions/upload-artifact@v4 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2a145e6..c36f836 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -62,7 +62,8 @@ jobs: uses: ./.github/actions/setup-unity with: editor-version: ${{ matrix.unityVersion }} - license: ${{ secrets.UNITY_LICENSE }} + email: ${{ secrets.UNITY_EMAIL }} + password: ${{ secrets.UNITY_PASSWORD }} - name: Cache Library uses: actions/cache@v4 @@ -80,6 +81,11 @@ jobs: --timeout 900 \ -- -nographics -logFile Logs/editmode.log + - name: Return Unity license + if: always() && env.UNITY_LICENSE_ACTIVATION_ATTEMPTED == 'true' + timeout-minutes: 3 + run: unity --non-interactive license return --yes + - name: Upload test results and logs if: always() uses: actions/upload-artifact@v4 From 8524c12625e31a9dd8b4fcc9417961d7946dfcea Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:56:17 +0900 Subject: [PATCH 7/9] ci(workflows): return licenses through the matching Unity client Why Both hosted EditMode suites pass, but Unity CLI rejects returning the Personal license activated by the bundled client. What Changed Return the runner's license using the bundled licensing client and use the Editor's credentialed return for entitlement licenses without a ULF file. Impact Test and release jobs share bounded cleanup after activation, including failed runs. License return failures remain visible and prevent Verification from passing. --- .github/actions/setup-unity/return-license.sh | 38 +++++++++++++++++++ .github/workflows/release.yaml | 8 +++- .github/workflows/tests.yaml | 8 +++- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 .github/actions/setup-unity/return-license.sh diff --git a/.github/actions/setup-unity/return-license.sh b/.github/actions/setup-unity/return-license.sh new file mode 100644 index 0000000..9290e87 --- /dev/null +++ b/.github/actions/setup-unity/return-license.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +editor_root="$(unity --non-interactive editors path "$UNITY_EDITOR_VERSION")" +licensing_client="$editor_root/Editor/Data/Resources/Licensing/Client/Unity.Licensing.Client" + +umask 077 +return_directory="$(mktemp -d "$RUNNER_TEMP/unity-license-return-XXXXXX")" +return_log="$return_directory/return.log" +trap 'rm -rf "$return_directory"' EXIT + +result=0 +timeout 120 "$licensing_client" --return-ulf \ + --username "$UNITY_EMAIL" --password "$UNITY_PASSWORD" \ + > "$return_log" 2>&1 || result=$? + +# Entitlement licenses have no ULF file; the Editor returns those with the +# account credentials. A scratch project keeps cleanup out of the real project. +if [[ "$result" != 0 ]] && grep -qi 'Ulf license file not found' "$return_log"; then + result=0 + timeout 120 "$editor_root/Editor/Unity" \ + -batchmode -nographics -quit -returnlicense \ + -username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" \ + -projectPath "$return_directory/project" -logFile /dev/stdout \ + > "$return_log" 2>&1 || result=$? +fi + +# The Editor may return the entitlement and then exit nonzero while also +# attempting a ULF return. Its explicit success message confirms the return. +if grep -qE 'Successfully returned ULF license|Successfully returned the entitlement license|License has been returned' "$return_log"; then + result=0 +fi + +if [[ "$result" != 0 ]]; then + echo "::error::Unity license return failed (exit $result)." + exit 1 +fi +echo "Unity license returned." diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4ed887f..9adbfb7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -109,8 +109,12 @@ jobs: - name: Return Unity license if: always() && env.UNITY_LICENSE_ACTIVATION_ATTEMPTED == 'true' - timeout-minutes: 3 - run: unity --non-interactive license return --yes + timeout-minutes: 5 + env: + UNITY_EDITOR_VERSION: 2021.3.45f2 + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + run: bash .github/actions/setup-unity/return-license.sh # Store artifacts. - name: Upload example player diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c36f836..073b7c0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -83,8 +83,12 @@ jobs: - name: Return Unity license if: always() && env.UNITY_LICENSE_ACTIVATION_ATTEMPTED == 'true' - timeout-minutes: 3 - run: unity --non-interactive license return --yes + timeout-minutes: 5 + env: + UNITY_EDITOR_VERSION: ${{ matrix.unityVersion }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + run: bash .github/actions/setup-unity/return-license.sh - name: Upload test results and logs if: always() From 43a7ad1c5f98633b25b2067647a8e861a193d2b2 Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:40:25 +0900 Subject: [PATCH 8/9] ci(release): build committed versions without direct branch pushes Why The required pull request ruleset prevents Release from pushing a package version commit directly to main, blocking the Linux build and package export. What Changed Read the version and SHA from the dispatched commit, build that exact SHA, and create only its version tag after the build succeeds. Reject tags for different commits and document version updates through pull requests. Impact Release works with protected main. Maintainers merge the package version update before dispatching Release; write permission is limited to the release publisher. --- .github/scripts/release-tag.sh | 30 ++++++++++++ .github/workflows/release.yaml | 87 +++++++++++++--------------------- AGENTS.md | 1 + 3 files changed, 63 insertions(+), 55 deletions(-) create mode 100644 .github/scripts/release-tag.sh diff --git a/.github/scripts/release-tag.sh b/.github/scripts/release-tag.sh new file mode 100644 index 0000000..420ebea --- /dev/null +++ b/.github/scripts/release-tag.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +operation="${1:?Expected check or create}" +case "$operation" in + check|create) ;; + *) echo "::error::Expected check or create."; exit 1 ;; +esac + +git check-ref-format "refs/tags/$VERSION" +remote_tags="$(git ls-remote --tags origin "refs/tags/$VERSION" "refs/tags/$VERSION^{}")" +tag_commit="" +while read -r object ref; do + case "$ref" in + "refs/tags/$VERSION") tag_commit="$object" ;; + # Annotated tags identify a tag object; compare the peeled commit instead. + "refs/tags/$VERSION^{}") tag_commit="$object"; break ;; + esac +done <<< "$remote_tags" + +if [[ -n "$tag_commit" && "$tag_commit" != "$RELEASE_SHA" ]]; then + echo "::error::Tag $VERSION belongs to a different commit. Update package.json version through a pull request before releasing." + exit 1 +fi + +if [[ "$operation" == create && -z "$tag_commit" ]]; then + git tag -- "$VERSION" "$RELEASE_SHA" + # A non-forced tag push rejects a concurrent release for a different commit. + git push --no-follow-tags --recurse-submodules=no origin "refs/tags/$VERSION" +fi diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9adbfb7..51c6c85 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,75 +1,38 @@ name: Release on: - workflow_dispatch: - inputs: - tag: - description: "tag: git tag you want create. (sample 1.0.0)" - required: true + workflow_dispatch: {} -env: - GIT_TAG: ${{ github.event.inputs.tag }} permissions: - contents: write + contents: read jobs: - update-packagejson: + prepare: runs-on: ubuntu-latest env: TARGET_FILE: ./Assets/MackySoft/MackySoft.SerializeReferenceExtensions/package.json outputs: - sha: ${{ steps.commit.outputs.sha }} + sha: ${{ steps.metadata.outputs.sha }} + version: ${{ steps.metadata.outputs.version }} steps: - uses: actions/checkout@v4 - - name: Output package.json (Before) - run: cat ${{ env.TARGET_FILE}} - - - name: Update package.json to version ${{ env.GIT_TAG }} - run: sed -i -e "s/\(\"version\":\) \"\(.*\)\",/\1 \"${{ env.GIT_TAG }}\",/" ${{ env.TARGET_FILE }} - - - name: Check update - id: check_update - run: | - cat ${{ env.TARGET_FILE}} - if ! git diff --exit-code; then - echo "changed=1" >> "$GITHUB_OUTPUT" - fi - - - name: Commit files - id: commit - if: steps.check_update.outputs.changed == '1' + - name: Read committed release version + id: metadata run: | - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git commit -m "feat: Update package.json to ${{ env.GIT_TAG }}" -a - echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - - name: Check sha - run: echo "SHA ${SHA}" - env: - SHA: ${{ steps.commit.outputs.sha }} - - - name: Create Tag - if: steps.check_update.outputs.changed == '1' - run: git tag ${{ env.GIT_TAG }} - - - name: Push changes - if: steps.check_update.outputs.changed == '1' - uses: ad-m/github-push-action@v0.6.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ github.ref }} - tags: true + version="$(jq -er '.version | select(type == "string" and length > 0)' "$TARGET_FILE")" + sha="$(git rev-parse HEAD)" + VERSION="$version" RELEASE_SHA="$sha" bash .github/scripts/release-tag.sh check + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "sha=$sha" >> "$GITHUB_OUTPUT" build: - needs: [update-packagejson] + needs: prepare runs-on: ubuntu-22.04 timeout-minutes: 45 steps: - - run: echo ${{ needs.update-packagejson.outputs.sha }} - uses: actions/checkout@v4 with: - ref: ${{ needs.update-packagejson.outputs.sha }} + ref: ${{ needs.prepare.outputs.sha }} lfs: true - name: Set up Unity @@ -138,9 +101,23 @@ jobs: path: Logs/ create-release: - needs: [update-packagejson, build] + needs: [prepare, build] runs-on: ubuntu-latest + permissions: + contents: write steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.sha }} + + # Publish the exact commit that produced the package. Recheck the remote + # after the build, since another release may have created the tag meanwhile. + - name: Create release tag + env: + VERSION: ${{ needs.prepare.outputs.version }} + RELEASE_SHA: ${{ needs.prepare.outputs.sha }} + run: bash .github/scripts/release-tag.sh create + # Download the package artifact for the release. - uses: actions/download-artifact@v4 with: @@ -152,9 +129,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ env.GIT_TAG }} - name: ${{ env.GIT_TAG }} - target_commitish: ${{ needs.update-packagejson.outputs.sha }} + tag_name: ${{ needs.prepare.outputs.version }} + name: ${{ needs.prepare.outputs.version }} + target_commitish: ${{ needs.prepare.outputs.sha }} draft: true prerelease: false files: SerializeReference-Extensions.unitypackage/SerializeReference-Extensions.unitypackage diff --git a/AGENTS.md b/AGENTS.md index 4101f7e..493bded 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ Use Unity CLI through `.github/actions/setup-unity`. - Run tests on Linux. - Always report the final `Verification` check on pull requests. It passes only when change detection succeeds and all required EditMode matrix jobs succeed, or Unity tests are not required and are skipped. - Build the example player for Linux 64-bit and export the package only in the Release workflow. +- Update the package version through a pull request before releasing. Release uses the committed `package.json` version as its tag and builds that exact commit. - Run a Unity matrix that includes: - 2021.3.x (minimum baseline) - 2023.2+ (generic/variance feature gate) From 213f64cfd18f8e3578d7ac1859762bb8bee74f0f Mon Sep 17 00:00:00 2001 From: Makihiro <13536348+mackysoft@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:07:50 +0900 Subject: [PATCH 9/9] ci(release): automate version updates with the release app Why Release needs the GitHub App's pull request exception to update its package version on a protected branch. What Changed Restore the version tag input and use repository-scoped App tokens for version commits, tags, and draft releases. Validate SemVer before editing the manifest and require a conditional fast-forward from the checked-out source. Build and publication share the prepared commit SHA. Impact Maintainers can release through one manual workflow. Concurrent branch changes and conflicting tags fail without replacing remote state, and each writing job owns its token. --- .github/scripts/release-tag.sh | 30 --------------- .github/scripts/release.sh | 67 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yaml | 62 ++++++++++++++++++++++++------- AGENTS.md | 2 +- 4 files changed, 117 insertions(+), 44 deletions(-) delete mode 100644 .github/scripts/release-tag.sh create mode 100644 .github/scripts/release.sh diff --git a/.github/scripts/release-tag.sh b/.github/scripts/release-tag.sh deleted file mode 100644 index 420ebea..0000000 --- a/.github/scripts/release-tag.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -operation="${1:?Expected check or create}" -case "$operation" in - check|create) ;; - *) echo "::error::Expected check or create."; exit 1 ;; -esac - -git check-ref-format "refs/tags/$VERSION" -remote_tags="$(git ls-remote --tags origin "refs/tags/$VERSION" "refs/tags/$VERSION^{}")" -tag_commit="" -while read -r object ref; do - case "$ref" in - "refs/tags/$VERSION") tag_commit="$object" ;; - # Annotated tags identify a tag object; compare the peeled commit instead. - "refs/tags/$VERSION^{}") tag_commit="$object"; break ;; - esac -done <<< "$remote_tags" - -if [[ -n "$tag_commit" && "$tag_commit" != "$RELEASE_SHA" ]]; then - echo "::error::Tag $VERSION belongs to a different commit. Update package.json version through a pull request before releasing." - exit 1 -fi - -if [[ "$operation" == create && -z "$tag_commit" ]]; then - git tag -- "$VERSION" "$RELEASE_SHA" - # A non-forced tag push rejects a concurrent release for a different commit. - git push --no-follow-tags --recurse-submodules=no origin "refs/tags/$VERSION" -fi diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh new file mode 100644 index 0000000..d32b122 --- /dev/null +++ b/.github/scripts/release.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +operation="${1:?Expected prepare or create}" +case "$operation" in + prepare|create) ;; + *) echo "::error::Expected prepare or create."; exit 1 ;; +esac + +# Package versions follow SemVer, including prerelease and build identifiers. +number='(0|[1-9][0-9]*)' +prerelease='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)' +if [[ ! "$VERSION" =~ ^${number}\.${number}\.${number}(-${prerelease}(\.${prerelease})*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Release version must follow SemVer (for example 1.7.1 or 1.8.0-preview.1)." + exit 1 +fi +git check-ref-format "refs/tags/$VERSION" + +if [[ "$operation" == prepare ]]; then + if [[ "$RELEASE_REF" != refs/heads/* ]]; then + echo "::error::Dispatch Release from a branch." + exit 1 + fi + + source_sha="$(git rev-parse HEAD)" + current_version="$(jq -er '.version | select(type == "string" and length > 0)' "$TARGET_FILE")" + if [[ "$current_version" != "$VERSION" ]]; then + manifest="$(mktemp "$RUNNER_TEMP/release-package-XXXXXX")" + trap 'rm -f "$manifest"' EXIT + jq --arg version "$VERSION" '.version = $version' "$TARGET_FILE" > "$manifest" + cat "$manifest" > "$TARGET_FILE" + git add -- "$TARGET_FILE" + git commit --only -m "chore(release): update package version to $VERSION" -- "$TARGET_FILE" + fi + RELEASE_SHA="$(git rev-parse HEAD)" +fi + +remote_tags="$(git ls-remote --tags origin "refs/tags/$VERSION" "refs/tags/$VERSION^{}")" +tag_commit="" +while read -r object ref; do + case "$ref" in + "refs/tags/$VERSION") tag_commit="$object" ;; + # Annotated tags identify a tag object; compare the peeled commit instead. + "refs/tags/$VERSION^{}") tag_commit="$object"; break ;; + esac +done <<< "$remote_tags" + +if [[ -n "$tag_commit" && "$tag_commit" != "$RELEASE_SHA" ]]; then + echo "::error::Tag $VERSION belongs to a different commit. Choose a new release version." + exit 1 +fi + +if [[ "$operation" == prepare ]]; then + if [[ "$current_version" != "$VERSION" ]]; then + # Require a fast-forward from the checked-out commit and an unchanged remote + # branch. The lease also rejects deletion or rollback during preparation. + git merge-base --is-ancestor "$source_sha" "$RELEASE_SHA" + git push --no-follow-tags --recurse-submodules=no \ + --force-with-lease="$RELEASE_REF:$source_sha" origin "$RELEASE_SHA:$RELEASE_REF" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT" +elif [[ -z "$tag_commit" ]]; then + git tag -- "$VERSION" "$RELEASE_SHA" + # A non-forced tag push rejects a concurrent release for a different commit. + git push --no-follow-tags --recurse-submodules=no origin "refs/tags/$VERSION" +fi diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 51c6c85..4d257c6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,7 +1,16 @@ name: Release on: - workflow_dispatch: {} + workflow_dispatch: + inputs: + tag: + description: Package version and Git tag (for example 1.7.1). + required: true + type: string + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false permissions: contents: read @@ -15,15 +24,35 @@ jobs: sha: ${{ steps.metadata.outputs.sha }} version: ${{ steps.metadata.outputs.version }} steps: + - name: Create release app token + id: release_app + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write + - uses: actions/checkout@v4 - - name: Read committed release version - id: metadata + with: + ref: ${{ github.ref }} + token: ${{ steps.release_app.outputs.token }} + + - name: Configure release app identity + env: + APP_SLUG: ${{ steps.release_app.outputs.app-slug }} + GH_TOKEN: ${{ steps.release_app.outputs.token }} run: | - version="$(jq -er '.version | select(type == "string" and length > 0)' "$TARGET_FILE")" - sha="$(git rev-parse HEAD)" - VERSION="$version" RELEASE_SHA="$sha" bash .github/scripts/release-tag.sh check - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "sha=$sha" >> "$GITHUB_OUTPUT" + bot_login="${APP_SLUG}[bot]" + bot_id="$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq '.id')" + git config user.name "$bot_login" + git config user.email "${bot_id}+${bot_login}@users.noreply.github.com" + + - name: Prepare release version + id: metadata + env: + VERSION: ${{ inputs.tag }} + RELEASE_REF: ${{ github.ref }} + run: bash .github/scripts/release.sh prepare build: needs: prepare @@ -103,12 +132,20 @@ jobs: create-release: needs: [prepare, build] runs-on: ubuntu-latest - permissions: - contents: write steps: + # Each job owns a short-lived token; prepare's token is revoked before build. + - name: Create release app token + id: release_app + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + permission-contents: write + - uses: actions/checkout@v4 with: ref: ${{ needs.prepare.outputs.sha }} + token: ${{ steps.release_app.outputs.token }} # Publish the exact commit that produced the package. Recheck the remote # after the build, since another release may have created the tag meanwhile. @@ -116,7 +153,7 @@ jobs: env: VERSION: ${{ needs.prepare.outputs.version }} RELEASE_SHA: ${{ needs.prepare.outputs.sha }} - run: bash .github/scripts/release-tag.sh create + run: bash .github/scripts/release.sh create # Download the package artifact for the release. - uses: actions/download-artifact@v4 @@ -126,9 +163,8 @@ jobs: # Create release and upload unitypackage. - uses: softprops/action-gh-release@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: + token: ${{ steps.release_app.outputs.token }} tag_name: ${{ needs.prepare.outputs.version }} name: ${{ needs.prepare.outputs.version }} target_commitish: ${{ needs.prepare.outputs.sha }} diff --git a/AGENTS.md b/AGENTS.md index 493bded..c5e59e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Use Unity CLI through `.github/actions/setup-unity`. - Run tests on Linux. - Always report the final `Verification` check on pull requests. It passes only when change detection succeeds and all required EditMode matrix jobs succeed, or Unity tests are not required and are skipped. - Build the example player for Linux 64-bit and export the package only in the Release workflow. -- Update the package version through a pull request before releasing. Release uses the committed `package.json` version as its tag and builds that exact commit. +- Release accepts a version tag, updates `package.json` through the MackySoft Release GitHub App, and builds that exact commit. Configure `RELEASE_BOT_APP_ID`, `RELEASE_BOT_PRIVATE_KEY`, and the App's bypass for the pull request rule before releasing. - Run a Unity matrix that includes: - 2021.3.x (minimum baseline) - 2023.2+ (generic/variance feature gate)