Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
405baca
[codex] Add release recovery and trim Random noise-map CI
wallstop Jul 3, 2026
ef9bf39
[codex] Address release recovery review feedback
wallstop Jul 3, 2026
e321e97
[codex] Fail closed on release publication lookup errors
wallstop Jul 3, 2026
f15e37d
[codex] Isolate release tag recovery permissions
wallstop Jul 3, 2026
7329642
[codex] Restore thorough random noise-map sweep
wallstop Jul 3, 2026
187bb4e
[codex] Align Random thorough coverage comment
wallstop Jul 3, 2026
f683e0a
[codex] Recheck release artifacts before tag recovery
wallstop Jul 3, 2026
e16fb19
[codex] Remove unused Random thorough sample constant
wallstop Jul 3, 2026
e2d5f4a
[codex] Use typed recovery boolean guard
wallstop Jul 3, 2026
6bfd3c8
[codex] Tighten Random bounds and permission contract
wallstop Jul 3, 2026
e39ed0c
[codex] Pin Node for release recovery guards
wallstop Jul 3, 2026
a69b743
[codex] Fail closed release publish artifact lookups
wallstop Jul 3, 2026
220e1e1
[codex] Collapse release publish flow and trim EditMode batch tests
wallstop Jul 3, 2026
3cad00c
[codex] Address release source ref review
wallstop Jul 3, 2026
5040678
[codex] Fix Unity UPM retry warning helper
wallstop Jul 3, 2026
82d55b6
[codex] Cancel superseded PR workflow runs
wallstop Jul 3, 2026
be46a0c
[codex] Harden release publish tag verification
wallstop Jul 3, 2026
c42aec3
[codex] Accept JSON 404s for release tag lookup
wallstop Jul 3, 2026
17d4f24
[codex] Trim hull test runtime in PR suite
wallstop Jul 4, 2026
0d90bf7
[codex] Move heavy batch-scope cases to stress
wallstop Jul 4, 2026
28e17ee
[codex] Move redundant editmode scans to stress
wallstop Jul 4, 2026
fe11415
[codex] Keep fast noise-map dimensions deterministic
wallstop Jul 4, 2026
85bc88b
[codex] Report release tag mutation failures clearly
wallstop Jul 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 216 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@ on:
# Keep the trigger as an unambiguous digit-start glob. The verify job below
# rejects anything except exact no-leading-zero X.Y.Z semver before publish.
- "[0-9]*.[0-9]*.[0-9]*"
workflow_dispatch:
inputs:
version:
description: "Release version/tag to publish, as strict X.Y.Z semver."
type: string
required: true
source_ref:
description: "Branch, tag, or commit to build release assets from."
type: string
required: true
default: main
allow_tag_recovery:
description: "Create or retarget the tag only if npm and GitHub Release are both still unpublished."
type: boolean
default: false

permissions:
contents: read

concurrency:
group: release-${{ github.ref_name }}
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
cancel-in-progress: false

jobs:
Expand All @@ -20,26 +35,54 @@ jobs:
if: github.repository == 'wallstop/unity-helpers' || github.repository == 'Ambiguous-Interactive/unity-helpers'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
Comment thread
wallstop marked this conversation as resolved.
package-name: ${{ steps.verify.outputs.package-name }}
package-version: ${{ steps.verify.outputs.package-version }}
source-ref: ${{ steps.verify.outputs.source-ref }}
source-sha: ${{ steps.verify.outputs.source-sha }}
tag: ${{ steps.verify.outputs.tag }}
tag-action: ${{ steps.verify.outputs.tag-action }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref || github.ref_name }}
persist-credentials: false

- name: Verify tag matches package metadata
Comment thread
Copilot marked this conversation as resolved.
id: verify
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_ALLOW_TAG_RECOVERY: ${{ inputs.allow_tag_recovery }}
INPUT_SOURCE_REF: ${{ inputs.source_ref }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
tag="${INPUT_VERSION}"
source_ref="${INPUT_SOURCE_REF}"
allow_tag_recovery="${INPUT_ALLOW_TAG_RECOVERY}"
else
tag="${GITHUB_REF_NAME}"
source_ref="${GITHUB_REF_NAME}"
allow_tag_recovery="false"
fi

if ! printf '%s\n' "${tag}" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'; then
echo "::error::Release tags must use unprefixed X.Y.Z semver."
exit 1
fi
Comment thread
Copilot marked this conversation as resolved.
if [ -z "${source_ref}" ]; then
echo "::error::Release source ref is required."
exit 1
fi

source_sha="$(git rev-parse HEAD)"
package_name="$(jq -r '.name // empty' package.json)"
package_version="$(jq -r '.version // empty' package.json)"
if [ -z "${package_name}" ] || [ -z "${package_version}" ]; then
Expand All @@ -60,22 +103,179 @@ jobs:
echo "::error::CHANGELOG.md has no exact section with release-note content for ${package_version}."
exit 1
fi

tag_action="none"
tag_ref_file="${RUNNER_TEMP}/release-tag-ref.json"
tag_ref_error="${RUNNER_TEMP}/release-tag-ref.err"
set +e
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" > "${tag_ref_file}" 2> "${tag_ref_error}"
tag_lookup_exit=$?
set -e
if [ "${tag_lookup_exit}" -eq 0 ]; then
tag_object_sha="$(jq -r '.object.sha // empty' "${tag_ref_file}")"
tag_object_type="$(jq -r '.object.type // empty' "${tag_ref_file}")"
if [ "${tag_object_type}" = "tag" ]; then
tag_target="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_object_sha}" --jq '.object.sha')"
elif [ "${tag_object_type}" = "commit" ]; then
tag_target="${tag_object_sha}"
else
echo "::error::Tag ${tag} points at unsupported object type '${tag_object_type}'."
exit 1
fi

if [ "${tag_target}" != "${source_sha}" ]; then
if [ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ] || [ "${allow_tag_recovery}" != "true" ]; then
echo "::error::Tag ${tag} already points at ${tag_target}, not selected source ${source_sha}."
exit 1
fi
tag_action="retarget"
fi
else
tag_lookup_output="$(cat "${tag_ref_error}")"
if ! printf '%s\n' "${tag_lookup_output}" | grep -E '(HTTP 404|Not Found)' >/dev/null; then
echo "::error::Failed to check whether tag ${tag} exists."
printf '%s\n' "${tag_lookup_output}"
exit "${tag_lookup_exit}"
fi
Comment thread
cursor[bot] marked this conversation as resolved.
if [ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ] || [ "${allow_tag_recovery}" != "true" ]; then
echo "::error::Tag ${tag} does not exist. Use Release Tag or rerun this workflow with allow_tag_recovery after confirming the version is unpublished."
exit 1
fi
tag_action="create"
fi

if [ "${tag_action}" != "none" ]; then
npm_published="false"
npm_view_file="${RUNNER_TEMP}/release-npm-view.txt"
npm_view_error="${RUNNER_TEMP}/release-npm-view.err"
set +e
npm view "${package_name}@${package_version}" version --registry "https://registry.npmjs.org" > "${npm_view_file}" 2> "${npm_view_error}"
npm_view_exit=$?
set -e
if [ "${npm_view_exit}" -eq 0 ]; then
npm_published="true"
else
npm_view_output="$(cat "${npm_view_error}")"
if ! printf '%s\n' "${npm_view_output}" | grep -E '(E404|404 Not Found|is not in this registry)' >/dev/null; then
echo "::error::Failed to verify npm publication state for ${package_name}@${package_version}."
printf '%s\n' "${npm_view_output}"
exit "${npm_view_exit}"
fi
fi

github_release_exists="false"
release_view_file="${RUNNER_TEMP}/release-view.json"
release_view_error="${RUNNER_TEMP}/release-view.err"
set +e
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "${release_view_file}" 2> "${release_view_error}"
release_lookup_exit=$?
set -e
if [ "${release_lookup_exit}" -eq 0 ]; then
github_release_exists="true"
else
release_lookup_output="$(cat "${release_view_error}")"
if ! printf '%s\n' "${release_lookup_output}" | grep -E '(HTTP 404|Not Found|"status":"404")' >/dev/null; then
echo "::error::Failed to verify GitHub Release state for ${tag}."
printf '%s\n' "${release_lookup_output}"
exit "${release_lookup_exit}"
fi
fi

if [ "${npm_published}" = "true" ] || [ "${github_release_exists}" = "true" ]; then
echo "::error::Published artifacts already exist; refusing to create or retarget tag ${tag}. npm=${npm_published}, github_release=${github_release_exists}."
exit 1
fi
Comment thread
cursor[bot] marked this conversation as resolved.
fi
Comment thread
wallstop marked this conversation as resolved.

{
echo "source-ref=${source_ref}"
echo "source-sha=${source_sha}"
echo "tag-action=${tag_action}"
} >> "${GITHUB_OUTPUT}"
Comment thread
wallstop marked this conversation as resolved.
Comment on lines +220 to +224
{
echo "package-name=${package_name}"
echo "package-version=${package_version}"
echo "tag=${tag}"
} >> "${GITHUB_OUTPUT}"

recover-tag:
name: Recover release tag
needs: verify-tag
if: ${{ github.event_name == 'workflow_dispatch' && inputs.allow_tag_recovery == true && needs.verify-tag.outputs.tag-action != 'none' }}
Comment thread
Copilot marked this conversation as resolved.
Outdated
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- name: Create or retarget release tag for manual recovery
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SOURCE_SHA: ${{ needs.verify-tag.outputs.source-sha }}
TAG: ${{ needs.verify-tag.outputs.tag }}
TAG_ACTION: ${{ needs.verify-tag.outputs.tag-action }}
run: |
set -euo pipefail
created_tag_sha="$(
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/tags" \
--field tag="${TAG}" \
--field message="Release ${TAG}" \
Comment thread
wallstop marked this conversation as resolved.
Outdated
--field object="${SOURCE_SHA}" \
--field type="commit" \
--jq '.sha'
)"
if [ "${TAG_ACTION}" = "create" ]; then
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
--field ref="refs/tags/${TAG}" \
--field sha="${created_tag_sha}" >/dev/null
echo "::notice::Created release tag ${TAG} at ${SOURCE_SHA}."
elif [ "${TAG_ACTION}" = "retarget" ]; then
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/${TAG}" \
--field sha="${created_tag_sha}" \
--field force=true >/dev/null
echo "::notice::Retargeted release tag ${TAG} to ${SOURCE_SHA}."
else
echo "::error::Unsupported tag recovery action '${TAG_ACTION}'."
exit 1
fi
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

release-ready:
name: Confirm release publish inputs
needs:
- verify-tag
- recover-tag
if: ${{ always() && needs.verify-tag.result == 'success' && (needs.verify-tag.outputs.tag-action == 'none' || needs.recover-tag.result == 'success') }}
runs-on: ubuntu-latest
timeout-minutes: 1
outputs:
package-name: ${{ needs.verify-tag.outputs.package-name }}
package-version: ${{ needs.verify-tag.outputs.package-version }}
source-ref: ${{ needs.verify-tag.outputs.source-ref }}
source-sha: ${{ needs.verify-tag.outputs.source-sha }}
tag: ${{ needs.verify-tag.outputs.tag }}
steps:
- name: Confirm release inputs
env:
PACKAGE_NAME: ${{ needs.verify-tag.outputs.package-name }}
PACKAGE_VERSION: ${{ needs.verify-tag.outputs.package-version }}
SOURCE_REF: ${{ needs.verify-tag.outputs.source-ref }}
SOURCE_SHA: ${{ needs.verify-tag.outputs.source-sha }}
TAG: ${{ needs.verify-tag.outputs.tag }}
run: |
set -euo pipefail
echo "Release ${TAG} will publish ${PACKAGE_NAME}@${PACKAGE_VERSION} from ${SOURCE_REF} (${SOURCE_SHA})."

validate-package:
name: Validate and pack npm package
needs: verify-tag
needs: release-ready
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ needs.release-ready.outputs.source-sha }}
persist-credentials: false

- name: Setup Node.js
Expand Down Expand Up @@ -117,7 +317,7 @@ jobs:
mv "${pack_dir}/${package_file}" ".artifacts/release/${package_file}"
(cd .artifacts/release && sha256sum "${package_file}" > "${package_file}.sha256")
pwsh -NoProfile -File scripts/release-tools/write-release-notes.ps1 \
-Version "${{ needs.verify-tag.outputs.package-version }}" \
-Version "${{ needs.release-ready.outputs.package-version }}" \
-Footer \
-OutputPath .artifacts/release/release-notes.md
echo "package-file=${package_file}" >> "${GITHUB_OUTPUT}"
Expand All @@ -133,7 +333,7 @@ jobs:
unitypackage:
name: Export .unitypackage
needs:
- verify-tag
- release-ready
- validate-package
runs-on: ubuntu-latest
timeout-minutes: 360
Expand All @@ -142,6 +342,7 @@ jobs:
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ needs.release-ready.outputs.source-sha }}
persist-credentials: false

- name: Setup Node.js
Expand All @@ -160,7 +361,7 @@ jobs:
uses: Ambiguous-Interactive/ambiguous-organization-build-lock/.github/actions/acquire-build-lock@v1
with:
lock-name: wallstop-organization-builds
holder-id-suffix: release-${{ needs.verify-tag.outputs.package-version }}
holder-id-suffix: release-${{ needs.release-ready.outputs.package-version }}
timeout-minutes: "210"
env:
BUILD_LOCK_TOKEN: ${{ secrets.ORG_BUILD_LOCK_TOKEN }}
Expand All @@ -177,7 +378,7 @@ jobs:
UNITY_VERSION="$(jq -r '.release' .github/unity-versions.json)"
export UNITY_VERSION
fi
output=".artifacts/unitypackage/${{ needs.verify-tag.outputs.package-name }}-${{ needs.verify-tag.outputs.package-version }}.unitypackage"
output=".artifacts/unitypackage/${{ needs.release-ready.outputs.package-name }}-${{ needs.release-ready.outputs.package-version }}.unitypackage"
bash scripts/unity/export-unitypackage.sh --output "${output}"

- name: Return Unity license
Expand All @@ -192,7 +393,7 @@ jobs:
uses: Ambiguous-Interactive/ambiguous-organization-build-lock/.github/actions/release-build-lock@v1
with:
lock-name: wallstop-organization-builds
holder-id-suffix: release-${{ needs.verify-tag.outputs.package-version }}
holder-id-suffix: release-${{ needs.release-ready.outputs.package-version }}
env:
BUILD_LOCK_TOKEN: ${{ secrets.ORG_BUILD_LOCK_TOKEN }}

Expand Down Expand Up @@ -228,7 +429,7 @@ jobs:
publish:
name: Publish npm and GitHub Release
needs:
- verify-tag
- release-ready
- validate-package
- unitypackage
runs-on: ubuntu-latest
Expand Down Expand Up @@ -258,8 +459,8 @@ jobs:
- name: Verify downloaded artifacts
id: artifacts
env:
EXPECTED_PACKAGE_NAME: ${{ needs.verify-tag.outputs.package-name }}
EXPECTED_PACKAGE_VERSION: ${{ needs.verify-tag.outputs.package-version }}
EXPECTED_PACKAGE_NAME: ${{ needs.release-ready.outputs.package-name }}
EXPECTED_PACKAGE_VERSION: ${{ needs.release-ready.outputs.package-version }}
run: |
set -euo pipefail
mapfile -t packages < <(find .artifacts/release -maxdepth 1 -type f -name '*.tgz' | sort)
Expand Down Expand Up @@ -297,8 +498,8 @@ jobs:
- name: Publish npm package
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_NAME: ${{ needs.verify-tag.outputs.package-name }}
PACKAGE_VERSION: ${{ needs.verify-tag.outputs.package-version }}
PACKAGE_NAME: ${{ needs.release-ready.outputs.package-name }}
PACKAGE_VERSION: ${{ needs.release-ready.outputs.package-version }}
PACKAGE_FILE: ${{ steps.artifacts.outputs.package-file }}
run: |
set -euo pipefail
Expand All @@ -317,7 +518,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.verify-tag.outputs.tag }}
TAG: ${{ needs.release-ready.outputs.tag }}
PACKAGE_FILE: ${{ steps.artifacts.outputs.package-file }}
UNITYPACKAGE_FILE: ${{ steps.artifacts.outputs.unitypackage-file }}
run: |
Expand All @@ -341,7 +542,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.verify-tag.outputs.tag }}
TAG: ${{ needs.release-ready.outputs.tag }}
PACKAGE_FILE: ${{ steps.artifacts.outputs.package-file }}
UNITYPACKAGE_FILE: ${{ steps.artifacts.outputs.unitypackage-file }}
run: |
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/unity-benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,11 @@ jobs:
# PR suite at the REDUCED default sample count (Tests/Runtime/Random/
# RandomTestBase.cs). This dedicated leg re-runs the Random assembly at the
# FULL statistical sample count so the thorough bias-detection coverage the
# fast default trades away is recovered weekly in CI. editmode-only (the
# Random tests are pure C# EditMode); runs inside the org-lock section.
# This is the ONLY place UH_RANDOM_SAMPLE_COUNT is consumed.
# fast default trades away is recovered weekly in CI. The same lane restores
# the historical broad NextNoiseMap sweep; PR CI keeps representative fixed
# dimensions only. editmode-only (the Random tests are pure C# EditMode);
# runs inside the org-lock section. This is the ONLY CI lane that sets
# UH_RANDOM_SAMPLE_COUNT / UH_RANDOM_NOISE_MAP_ITERATIONS.
- name: Run Random suite at full sample count
id: run_random_thorough
if: ${{ matrix.test-mode == 'editmode' && steps.compute.outputs.is-empty != 'true' }}
Expand All @@ -328,6 +330,7 @@ jobs:
# No perf-category filter: run the Fast-tagged Random tests by assembly.
UH_UNITY_TEST_CATEGORY: ""
UH_RANDOM_SAMPLE_COUNT: "12750000"
UH_RANDOM_NOISE_MAP_ITERATIONS: "1000"
run: |
./scripts/unity/run-ci-tests.ps1 `
-UnityVersion '${{ matrix.unity-version }}' `
Expand Down
Loading
Loading