🤖 Auto-update server-publicized redist files #87
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Redist Verify" | |
| # Uses `pull_request` (NOT `pull_request_target`): validation runs only PR-supplied | |
| # data (no repo secrets on fork PRs). Auto-merge/approve are gated on the trusted bot | |
| # author (`rocketmodfixadmin`), which a fork cannot spoof. | |
| on: | |
| pull_request: | |
| branches: | |
| - master | |
| paths: | |
| - 'redist/**' | |
| types: | |
| - opened | |
| - synchronize | |
| - reopened | |
| - ready_for_review | |
| jobs: | |
| verify: | |
| name: "Verify Redist Update" | |
| runs-on: ubuntu-latest | |
| env: | |
| ALLOW_AUTO_MERGE_REDIST_PR: ${{ vars.ALLOW_AUTO_MERGE_REDIST_PR }} | |
| permissions: | |
| pull-requests: write | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Resolve variant and redist directory | |
| run: | | |
| set -euo pipefail | |
| # Automated redist PRs use the branch name redist-update/<variant>. | |
| if [[ "${GITHUB_HEAD_REF}" != redist-update/* ]]; then | |
| echo "::error::This validator only handles automated 'redist-update/<variant>' branches (got '${GITHUB_HEAD_REF}')." | |
| exit 1 | |
| fi | |
| VARIANT="${GITHUB_HEAD_REF##*/}" | |
| echo "Detected variant: $VARIANT" | |
| # Single source of truth: map variant -> dir via .github/variants.json. | |
| REDIST_DIR=$(jq -r --arg v "$VARIANT" '.[] | select(.variant == $v) | .dir' .github/variants.json) | |
| IS_PREVIEW=$(jq -r --arg v "$VARIANT" '.[] | select(.variant == $v) | .preview' .github/variants.json) | |
| if [ -z "$REDIST_DIR" ] || [ "$REDIST_DIR" = "null" ]; then | |
| echo "::error::Unknown variant '$VARIANT' (not found in .github/variants.json)." | |
| exit 1 | |
| fi | |
| if [ ! -d "$REDIST_DIR" ]; then | |
| echo "::error::Redist directory does not exist: $REDIST_DIR" | |
| exit 1 | |
| fi | |
| echo "✅ Variant=$VARIANT Dir=$REDIST_DIR Preview=$IS_PREVIEW" | |
| echo "VARIANT=$VARIANT" >> "$GITHUB_ENV" | |
| echo "REDIST_DIR=$REDIST_DIR" >> "$GITHUB_ENV" | |
| # Preview variants track version.preview.json; everyone else version.json. | |
| if [ "$IS_PREVIEW" = "true" ]; then | |
| echo "VERSION_FILE=$REDIST_DIR/version.preview.json" >> "$GITHUB_ENV" | |
| else | |
| echo "VERSION_FILE=$REDIST_DIR/version.json" >> "$GITHUB_ENV" | |
| fi | |
| - name: Validate required DLL and XML files | |
| run: | | |
| set -euo pipefail | |
| echo "📋 Validating required DLL and XML documentation files in $REDIST_DIR..." | |
| REQUIRED_DLLS=( | |
| "Assembly-CSharp.dll" | |
| "SDG.NetPak.Runtime.dll" | |
| "com.rlabrecque.steamworks.net.dll" | |
| "SDG.Glazier.Runtime.dll" | |
| "SDG.HostBans.Runtime.dll" | |
| "SDG.NetTransport.dll" | |
| "SystemEx.dll" | |
| "UnityEx.dll" | |
| "UnturnedDat.dll" | |
| ) | |
| # DLLs that must ship with a matching XML doc. | |
| XML_FOR=("Assembly-CSharp.dll" "SDG.NetPak.Runtime.dll") | |
| missing=() | |
| for dll in "${REQUIRED_DLLS[@]}"; do | |
| if [ -f "$REDIST_DIR/$dll" ]; then echo "✅ $dll"; else echo "❌ $dll"; missing+=("$dll"); fi | |
| done | |
| for dll in "${XML_FOR[@]}"; do | |
| xml="${dll%.dll}.xml" | |
| if [ -f "$REDIST_DIR/$xml" ]; then echo "✅ $xml"; else echo "❌ $xml"; missing+=("$xml"); fi | |
| done | |
| if [ ${#missing[@]} -ne 0 ]; then | |
| echo "::error::Missing required files: ${missing[*]}" | |
| exit 1 | |
| fi | |
| echo "✅ All required files present." | |
| - name: Validate file hashes against manifest.sha256.json | |
| run: | | |
| set -euo pipefail | |
| HASH_FILE="$REDIST_DIR/manifest.sha256.json" | |
| if [ ! -f "$HASH_FILE" ]; then | |
| echo "::error::Missing $HASH_FILE — cannot verify file integrity." | |
| exit 1 | |
| fi | |
| echo "🔒 Verifying SHA-256 of each tracked file against manifest.sha256.json..." | |
| # manifest.sha256.json: { "<filename>": "<sha256>" }. sha256sum -c wants | |
| # "<hash> <path>"; run it from inside the redist dir so names resolve. | |
| ( | |
| cd "$REDIST_DIR" | |
| jq -r 'to_entries[] | "\(.value) \(.key)"' manifest.sha256.json | sha256sum -c - | |
| ) | |
| echo "✅ All file hashes match the manifest." | |
| - name: Validate this is a newer upstream build | |
| run: | | |
| set -euo pipefail | |
| if [ ! -f "$VERSION_FILE" ]; then | |
| echo "::error::Missing $VERSION_FILE." | |
| exit 1 | |
| fi | |
| NEW_VER=$(jq -r '.NuGetVersion' "$VERSION_FILE") | |
| NEW_BUILD=$(jq -r '.BuildId' "$VERSION_FILE") | |
| echo "PR: version=$NEW_VER buildId=$NEW_BUILD ($VERSION_FILE)" | |
| # Compare against the same file on the base branch (self-contained; no | |
| # network dependency on nuget.org). | |
| git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}" >/dev/null 2>&1 || true | |
| BASE_JSON=$(git show "origin/${{ github.event.pull_request.base.ref }}:$VERSION_FILE" 2>/dev/null || echo "") | |
| if [ -z "$BASE_JSON" ]; then | |
| echo "No baseline on '${{ github.event.pull_request.base.ref }}' (new file) — skipping the check." | |
| exit 0 | |
| fi | |
| OLD_VER=$(printf '%s' "$BASE_JSON" | jq -r '.NuGetVersion') | |
| OLD_BUILD=$(printf '%s' "$BASE_JSON" | jq -r '.BuildId') | |
| echo "Base: version=$OLD_VER buildId=$OLD_BUILD" | |
| # Gate on the Steam build id (monotonic per app+branch, rises even on a | |
| # rollback); a LOWER id means an older build = regression. The version can dip | |
| # on a preview rollback, so we don't hard-fail on version. | |
| # See ARCHITECTURE.md ("Why the build id, not the version"). | |
| if [[ "$NEW_BUILD" =~ ^[0-9]+$ ]] && [[ "$OLD_BUILD" =~ ^[0-9]+$ ]]; then | |
| if [ "$NEW_BUILD" -lt "$OLD_BUILD" ]; then | |
| echo "::error::Build id went backwards ($OLD_BUILD -> $NEW_BUILD): this is an older upstream build than what is already published." | |
| exit 1 | |
| fi | |
| else | |
| echo "::warning::Non-numeric build id ('$OLD_BUILD' -> '$NEW_BUILD'); skipping build-id gate." | |
| fi | |
| # Informational only: surface a version dip without blocking (build id already gated). | |
| result=$(python3 .github/scripts/compare_nuget_version.py "$NEW_VER" "$OLD_VER" 2>/dev/null || echo "unknown") | |
| case "$result" in | |
| lt) echo "::warning::NuGet version decreased ($OLD_VER -> $NEW_VER) but the build id advanced — likely an upstream rollback. Allowing (newer build)." ;; | |
| eq) echo "::warning::NuGet version unchanged ($NEW_VER); the publish step will fail on a duplicate version by design." ;; | |
| gt) echo "✅ Newer build and higher version: $OLD_VER ($OLD_BUILD) -> $NEW_VER ($NEW_BUILD)." ;; | |
| *) echo "Build id advanced; version comparison skipped." ;; | |
| esac | |
| # Auto-merge for the trusted bot's PRs when opted in: GITHUB_TOKEN approves, | |
| # PAT enables the merge (it can merge to a protected base). | |
| - name: Auto-approve PR | |
| if: success() && env.ALLOW_AUTO_MERGE_REDIST_PR == 'true' && github.event.pull_request.user.login == 'rocketmodfixadmin' | |
| uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pull-request-number: ${{ github.event.pull_request.number }} | |
| review-message: "All checks passed (files, hashes, version). Auto-approved automated PR." | |
| # Inline `gh pr merge --auto` (was peter-evans/enable-pull-request-automerge): | |
| # when a PR is already mergeable it merges immediately, so sibling redist PRs | |
| # race on master ("Base branch was modified"). Retry that benign race; fail | |
| # loud otherwise. See ARCHITECTURE.md. | |
| - name: Enable PR automerge | |
| if: success() && env.ALLOW_AUTO_MERGE_REDIST_PR == 'true' && github.event.pull_request.user.login == 'rocketmodfixadmin' | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -euo pipefail | |
| attempts=6 | |
| for i in $(seq 1 "$attempts"); do | |
| if out=$(gh pr merge "$PR_NUMBER" --repo "$REPO" --squash --auto 2>&1); then | |
| printf '%s\n' "$out" | |
| echo "Auto-merge enabled / PR #${PR_NUMBER} merged (attempt ${i}/${attempts})." | |
| exit 0 | |
| fi | |
| printf '%s\n' "$out" | |
| if printf '%s' "$out" | grep -qi 'Base branch was modified'; then | |
| echo "::warning::PR #${PR_NUMBER} merge raced a sibling variant merge (attempt ${i}/${attempts}); retrying after backoff..." | |
| [ "$i" -lt "$attempts" ] && sleep $((i * 10)) | |
| continue | |
| fi | |
| echo "::error::Enabling auto-merge for PR #${PR_NUMBER} failed (non-transient)." | |
| exit 1 | |
| done | |
| echo "::error::PR #${PR_NUMBER} could not be merged after ${attempts} attempts (base branch kept moving)." | |
| exit 1 |