Skip to content

Redist Update

Redist Update #429

name: "Redist Update"
on:
schedule:
- cron: "*/15 * * * *" # Run every 15 minutes
workflow_dispatch:
inputs:
variant:
description: 'A variant name from .github/variants.json, or "all"'
required: true
default: 'all'
type: string
permissions:
contents: write
pull-requests: write
issues: write
concurrency:
group: unturned-redist-update-${{ github.ref }}
cancel-in-progress: false
jobs:
# Emits two matrices from .github/variants.json (the single source of truth):
# `sources` (one per Steam appId+branch, downloaded once) and `variants` (flat
# per-variant list). Add/remove variants by editing variants.json.
load:
name: "Load matrices"
runs-on: ubuntu-latest
outputs:
variants: ${{ steps.load.outputs.variants }}
sources: ${{ steps.load.outputs.sources }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Load variant + source matrices
id: load
env:
INPUT_VARIANT: ${{ github.event.inputs.variant }}
EVENT_NAME: ${{ github.event_name }}
# Opt-in 2nd Steam account (see ARCHITECTURE.md); empty/false = unchanged.
STEAM_SECOND_ACCOUNT_ENABLED: ${{ vars.STEAM_SECOND_ACCOUNT_ENABLED }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "${INPUT_VARIANT:-}" ] && [ "$INPUT_VARIANT" != "all" ]; then
if ! jq -e --arg v "$INPUT_VARIANT" 'any(.[]; .variant == $v)' .github/variants.json >/dev/null; then
echo "::error::Unknown variant '$INPUT_VARIANT'. Valid variants:"
jq -r '.[].variant' .github/variants.json
exit 1
fi
base=$(jq -c --arg v "$INPUT_VARIANT" '[.[] | select(.variant == $v)]' .github/variants.json)
else
base=$(jq -c '.' .github/variants.json)
fi
# Group variants by Steam source (appId+branch); the source label doubles as
# the artifact name linking download_sources -> update_variant. Also derive per
# source: steamAccount (0=anon, 1=primary, 2=2nd account for the preview client)
# and dlgroup (download concurrency group). See ARCHITECTURE.md.
second="${STEAM_SECOND_ACCOUNT_ENABLED:-false}"
sources=$(printf '%s' "$base" | jq -c --arg second "$second" '
group_by(.appId + "|" + .branch)
| map({
source: (.[0].appId + "-" + (if .[0].branch == "" then "default" else .[0].branch end)),
appId: .[0].appId,
depotId: .[0].depotId,
branch: .[0].branch,
anonymous: .[0].anonymous,
loginId: .[0].loginId,
variants: (map(.variant) | join(",")),
steamAccount: (if .[0].anonymous then 0
elif .[0].branch == "" then 1
elif ($second == "true") then 2
else 1 end),
dlgroup: (if ((.[0].anonymous) or ($second == "true"))
then (.[0].appId + "-" + (if .[0].branch == "" then "default" else .[0].branch end))
else "authenticated" end)
})')
echo "variants=$base" >> "$GITHUB_OUTPUT"
echo "sources=$sources" >> "$GITHUB_OUTPUT"
echo "Sources to probe:"
printf '%s' "$sources" | jq -r '.[] | " \(.source) (variants: \(.variants))"'
# One job per Steam source: probe the manifest once; if changed, download the game
# once and upload the bits the tool needs (Managed DLLs + appmanifest + Status.json)
# as an artifact every variant of this source reuses. The only Steam logins happen here.
download_sources:
name: "Download ${{ matrix.source }}"
runs-on: ubuntu-latest
needs: load
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.load.outputs.sources) }}
# Per-source download group (matrix.dlgroup): server sources run parallel; the two
# client sources share one account so they're serialized, unless a 2nd account is
# enabled (then each gets its own group). See ARCHITECTURE.md.
concurrency:
group: redist-dl-${{ matrix.dlgroup }}
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Download depot downloader
uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1
with:
repository: SteamRE/DepotDownloader
latest: true
fileName: DepotDownloader-linux-x64.zip
out-file-path: depot_downloader
tag: "DepotDownloader_3.4.0" # Pin its version to avoid breaking changes.
extract: true
- name: Probe source manifest
id: probe
env:
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
STEAM_USERNAME_2: ${{ secrets.STEAM_USERNAME_2 }}
STEAM_PASSWORD_2: ${{ secrets.STEAM_PASSWORD_2 }}
STEAM_ACCOUNT: ${{ matrix.steamAccount }}
APP_ID: ${{ matrix.appId }}
APP_DEPOT_ID: ${{ matrix.depotId }}
APP_BRANCH_NAME: ${{ matrix.branch }}
SOURCE: ${{ matrix.source }}
ANONYMOUS: ${{ matrix.anonymous }}
LOGIN_ID: ${{ matrix.loginId }}
VARIANTS: ${{ matrix.variants }}
run: |
set -euo pipefail
mkdir -p redist/temp_depots
chmod +x depot_downloader/DepotDownloader
beta_args=()
if [ -n "$APP_BRANCH_NAME" ]; then
beta_args=(-beta "$APP_BRANCH_NAME")
fi
# account 2 = optional 2nd Steam account (preview client); see ARCHITECTURE.md.
if [ "$STEAM_ACCOUNT" = "2" ]; then
STEAM_USERNAME="$STEAM_USERNAME_2"
STEAM_PASSWORD="$STEAM_PASSWORD_2"
if [ -z "$STEAM_USERNAME" ] || [ -z "$STEAM_PASSWORD" ]; then
echo "::error::Source ${SOURCE} is assigned Steam account 2 but STEAM_USERNAME_2 / STEAM_PASSWORD_2 are not set."
exit 1
fi
fi
# -loginid makes each source a distinct Steam session so the anonymous
# probes can run concurrently. Authenticated sources pass credentials;
# anonymous sources omit them (DepotDownloader defaults to anonymous).
auth_args=(-loginid "$LOGIN_ID")
if [ "$ANONYMOUS" != "true" ]; then
auth_args+=(-username "$STEAM_USERNAME" -password "$STEAM_PASSWORD")
fi
# DepotDownloader can drop its connection mid-probe and emit no "Manifest <id>"
# line — a known transient (SteamRE/DepotDownloader #223, #691). Retry, gate on
# a valid id, fail loud only when exhausted.
current_manifest=""
manifest_output=""
attempts=4
for i in $(seq 1 "$attempts"); do
# '|| true': DepotDownloader can exit non-zero even on a successful
# manifest fetch. The id-validation below is the real gate.
manifest_output=$(depot_downloader/DepotDownloader \
-app "$APP_ID" \
-depot "$APP_DEPOT_ID" \
-manifest-only \
"${auth_args[@]}" \
"${beta_args[@]}" \
-dir redist/temp_depots || true)
# Grab the number after "Manifest" (64-bit id, ~17-20 digits, variable width).
current_manifest=$(printf '%s\n' "$manifest_output" | grep -oiE 'manifest[[:space:]]+[0-9]+' | grep -oE '[0-9]+' | head -n 1 || true)
if [[ "$current_manifest" =~ ^[0-9]{10,20}$ ]]; then
break
fi
echo "::warning::Probe attempt ${i}/${attempts} for source ${SOURCE} produced no manifest id (likely a transient Steam connection drop); retrying after backoff..."
[ "$i" -lt "$attempts" ] && sleep $((i * 15))
done
if ! [[ "$current_manifest" =~ ^[0-9]{10,20}$ ]]; then
echo "::error::Could not extract a manifest id for source ${SOURCE} after ${attempts} attempts (Steam auth/network issue or output-format change?)."
echo "----- DepotDownloader output (first 50 lines) -----"
printf '%s\n' "$manifest_output" | head -n 50
exit 1
fi
echo "Current manifest: $current_manifest"
echo "current_manifest=$current_manifest" >> "$GITHUB_OUTPUT"
# Download if ANY variant's recorded manifest id differs from the live one.
changed=false
IFS=',' read -ra source_variants <<< "$VARIANTS"
for v in "${source_variants[@]}"; do
f="redist/redist-manifests/.manifest.redist-${v}.txt"
prev=""
[ -f "$f" ] && prev=$(cat "$f")
echo " ${v}: recorded=${prev:-<none>}"
[ "$prev" != "$current_manifest" ] && changed=true
done
echo "Source changed: $changed"
echo "source_changed=$changed" >> "$GITHUB_OUTPUT"
- name: Setup SteamCMD
if: steps.probe.outputs.source_changed == 'true'
uses: CyberAndrii/setup-steamcmd@afc45f145b95c175c3a3862d3ca84756537d48e8 # v1
- name: Download game files
if: steps.probe.outputs.source_changed == 'true'
env:
STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }}
STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }}
STEAM_USERNAME_2: ${{ secrets.STEAM_USERNAME_2 }}
STEAM_PASSWORD_2: ${{ secrets.STEAM_PASSWORD_2 }}
STEAM_ACCOUNT: ${{ matrix.steamAccount }}
APP_ID: ${{ matrix.appId }}
APP_BRANCH_NAME: ${{ matrix.branch }}
ANONYMOUS: ${{ matrix.anonymous }}
run: |
set -uo pipefail
# Pick the credential set for this source (see "Probe source manifest"):
# account 2 is the optional second account for the preview client source
# so it logs into a DIFFERENT account than the default client source.
if [ "$STEAM_ACCOUNT" = "2" ]; then
STEAM_USERNAME="$STEAM_USERNAME_2"
STEAM_PASSWORD="$STEAM_PASSWORD_2"
if [ -z "$STEAM_USERNAME" ] || [ -z "$STEAM_PASSWORD" ]; then
echo "::error::This source is assigned Steam account 2 but STEAM_USERNAME_2 / STEAM_PASSWORD_2 are not set."
exit 1
fi
fi
if [ "$ANONYMOUS" = "true" ]; then
login_args=(+login anonymous)
else
login_args=(+login "$STEAM_USERNAME" "$STEAM_PASSWORD")
fi
beta_args=()
if [ -n "$APP_BRANCH_NAME" ]; then
beta_args=(-beta "$APP_BRANCH_NAME")
fi
# steamcmd's exit code is unreliable, so retry and gate on the real artifact:
# appmanifest_<appid>.acf, written only on a completed update and needed by the tool.
manifest_present() {
find "$GITHUB_WORKSPACE" -name "appmanifest_${APP_ID}.acf" -print -quit 2>/dev/null | grep -q .
}
attempts=4
for i in $(seq 1 "$attempts"); do
rc=0
steamcmd +force_install_dir "$GITHUB_WORKSPACE" "${login_args[@]}" +app_update "$APP_ID" "${beta_args[@]}" -validate +quit || rc=$?
if manifest_present; then
echo "Download complete on attempt $i/$attempts (steamcmd rc=$rc)."
break
fi
echo "::warning::steamcmd attempt $i/$attempts did not produce appmanifest_${APP_ID}.acf (rc=$rc); retrying after backoff..."
[ "$i" -lt "$attempts" ] && sleep $((i * 15))
done
if ! manifest_present; then
echo "::error::steamcmd failed to download app $APP_ID after $attempts attempts (no appmanifest_${APP_ID}.acf). Likely a transient Steam/anonymous outage — re-run the workflow."
exit 1
fi
- name: Stash manifest id for consumers
if: steps.probe.outputs.source_changed == 'true'
run: echo "${{ steps.probe.outputs.current_manifest }}" > source-manifest.txt
# Upload only the small bits the tool reads (not the multi-GB game). The
# artifact's presence is also the "source changed" signal update_variant keys off.
- name: Upload source files
if: steps.probe.outputs.source_changed == 'true'
uses: actions/upload-artifact@v7
with:
name: source-${{ matrix.source }}
path: |
source-manifest.txt
Status.json
steamapps/appmanifest_${{ matrix.appId }}.acf
Unturned_Data/Managed/**
Unturned_Headless_Data/Managed/**
if-no-files-found: warn
retention-days: 1
# One job per variant, fully parallel: pull the source artifact, run the tool, open
# the rolling PR. No Steam login here, so nothing to serialize.
update_variant:
name: "Update ${{ matrix.variant }}"
runs-on: ubuntu-latest
needs: [load, download_sources]
# Job-level permissions REPLACE workflow defaults: contents (checkout) + actions:read
# (the artifacts gate). The PR is created with secrets.PAT, independent of these.
permissions:
contents: write
pull-requests: write
actions: read
# Run even if a source download failed (those variants find no artifact and skip);
# only skip if load itself failed.
if: ${{ !cancelled() && needs.load.result == 'success' }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.load.outputs.variants) }}
steps:
- name: Compute identifiers
id: ids
env:
APP_ID: ${{ matrix.appId }}
BRANCH: ${{ matrix.branch }}
run: |
set -euo pipefail
if [ -z "${BRANCH}" ]; then
key="${APP_ID}-default"
else
key="${APP_ID}-${BRANCH}"
fi
echo "source_key=$key" >> "$GITHUB_OUTPUT"
echo "BRANCH_NAME=redist-update/${{ matrix.variant }}" >> "$GITHUB_ENV"
# The source artifact exists only if download_sources saw a manifest change
# for this variant's source. No artifact => nothing to do this run.
- name: Check whether this source changed
id: gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
SOURCE_KEY: ${{ steps.ids.outputs.source_key }}
run: |
set -euo pipefail
# Fail LOUD on API error: only a SUCCESSFUL listing without our artifact means
# "unchanged". A failed gh call must NOT be read as unchanged. Retry, then fail.
ok=false
names=""
for attempt in 1 2 3; do
if names=$(gh api --paginate "repos/$REPO/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[].name'); then
ok=true
break
fi
echo "::warning::Listing run artifacts failed (attempt ${attempt}/3); retrying after backoff..."
sleep $((attempt * 5))
done
if [ "$ok" != "true" ]; then
echo "::error::Could not list this run's artifacts after 3 attempts — cannot determine whether source ${SOURCE_KEY} changed. Failing loudly instead of skipping."
exit 1
fi
if printf '%s\n' "$names" | grep -qx "source-${SOURCE_KEY}"; then
echo "Source ${SOURCE_KEY} changed (artifact present); processing ${{ matrix.variant }}."
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "Source ${SOURCE_KEY} unchanged (no artifact); skipping ${{ matrix.variant }}."
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout repository
if: steps.gate.outputs.changed == 'true'
uses: actions/checkout@v7
with:
ref: master
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
if: steps.gate.outputs.changed == 'true'
uses: actions/setup-dotnet@v5
env:
DOTNET_NOLOGO: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
with:
dotnet-version: 10.x
- name: Download update tool
if: steps.gate.outputs.changed == 'true'
uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1
with:
repository: RocketModFix/UnturnedRedistUpdateTool
latest: true
fileName: UnturnedRedistUpdateTool.zip
out-file-path: redist_tool
extract: true
- name: Download source game files
if: steps.gate.outputs.changed == 'true'
uses: actions/download-artifact@v8
with:
name: source-${{ steps.ids.outputs.source_key }}
path: ${{ github.workspace }}
- name: Run redist updater
if: steps.gate.outputs.changed == 'true'
env:
IS_PREVIEW: ${{ matrix.preview }}
DO_PUBLICIZE: ${{ matrix.publicize }}
APP_ID: ${{ matrix.appId }}
REDIST_DIR: ${{ matrix.dir }}
EVENT_NAME: ${{ github.event_name }}
VARIANT: ${{ matrix.variant }}
run: |
set -euo pipefail
flags=""
# Manual dispatch forces a rebuild even if the tool thinks nothing changed.
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
flags="$flags --force"
fi
# Only client-preview / server-preview emit the -preview<build> prerelease.
if [ "$IS_PREVIEW" = "true" ]; then
flags="$flags --preview"
fi
flags="$flags -update-files Assembly-CSharp.dll,Assembly-CSharp.xml,SDG.NetPak.Runtime.xml,UnturnedDat.dll,UnityEx.dll,SystemEx.dll,SDG.NetTransport.dll,SDG.NetPak.Runtime.dll,SDG.HostBans.Runtime.dll,SDG.Glazier.Runtime.dll,com.rlabrecque.steamworks.net.dll"
if [ "$DO_PUBLICIZE" = "true" ]; then
flags="$flags -publicize Assembly-CSharp.dll"
fi
echo "Variant: $VARIANT | Event: $EVENT_NAME | Flags: '$flags'"
# Capture output so a failure (or a success that doesn't write .commit)
# leaves diagnostics in the log.
if ! tool_output=$(dotnet redist_tool/UnturnedRedistUpdateTool.dll "$GITHUB_WORKSPACE" "$GITHUB_WORKSPACE/$REDIST_DIR" "$APP_ID" $flags 2>&1); then
echo "::error::Redist updater failed for ${VARIANT}."
echo "----- Tool output (first 80 lines) -----"
printf '%s\n' "$tool_output" | head -n 80
exit 1
fi
printf '%s\n' "$tool_output"
# Record the manifest id so the next probe sees "no change". Outside REDIST_DIR so
# the discard step never reverts it.
- name: Record source manifest id
if: steps.gate.outputs.changed == 'true'
env:
VARIANT: ${{ matrix.variant }}
run: |
set -euo pipefail
manifest_id=$(cat source-manifest.txt)
mkdir -p redist/redist-manifests
echo "$manifest_id" > "redist/redist-manifests/.manifest.redist-${VARIANT}.txt"
- name: Resolve commit message and discard unpublishable changes
if: steps.gate.outputs.changed == 'true'
id: generate_commit_message
env:
REDIST_DIR: ${{ matrix.dir }}
run: |
set -euo pipefail
if [ -s .commit ]; then
# The tool bumped the NuGet version -> a real, publishable update.
# Keep all the regenerated files (DLLs, version.json, manifest.sha256.json).
{
echo "message<<COMMIT_EOF"
printf '%s\n' "$(cat .commit)"
echo "COMMIT_EOF"
} >> "$GITHUB_OUTPUT"
else
# No .commit => version unchanged (Steam build moved but game version didn't).
# Can't republish the same NuGet version, so discard the regenerated DLLs and
# keep only the recorded manifest id; the DLLs publish on the next version bump.
git checkout -- "$REDIST_DIR" 2>/dev/null || true
echo "message=Record ${{ matrix.variant }} Steam manifest (game version unchanged; no package update)" >> "$GITHUB_OUTPUT"
fi
- name: Check for git changes
if: steps.gate.outputs.changed == 'true'
id: check_git_changes
run: |
if git diff --quiet; then
echo "No changes detected after processing for ${{ matrix.variant }}."
echo "has_git_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected after processing for ${{ matrix.variant }}."
echo "has_git_changes=true" >> "$GITHUB_OUTPUT"
fi
# Commits to a fixed-name branch and keeps the rolling PR updated until merged;
# delete-branch cleans up afterwards.
- name: Create Pull Request
if: steps.gate.outputs.changed == 'true' && steps.check_git_changes.outputs.has_git_changes == 'true'
id: create_pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
add-paths: |
redist/*
token: ${{ secrets.PAT }}
branch: ${{ env.BRANCH_NAME }}
delete-branch: true
base: master
commit-message: ${{ steps.generate_commit_message.outputs.message }}
title: "🤖 Auto-update ${{ matrix.variant }} redist files"
committer: rocketmodfixadmin <rocketmodfixadmin@users.noreply.github.com>
author: rocketmodfixadmin <rocketmodfixadmin@users.noreply.github.com>
body: |
## Automated Redist Update - ${{ matrix.variant }}
This PR contains automatically updated redist files for the **${{ matrix.variant }}** variant.
### Changes
${{ steps.generate_commit_message.outputs.message }}
### Validation
🔄 Validation (file presence, SHA-256 hashes, version monotonicity) runs automatically on this PR.
---
**Triggered by**: ${{ github.event_name == 'workflow_dispatch' && 'Manual dispatch' || 'Scheduled run' }}
**Variant**: ${{ matrix.variant }}
**Branch**: `${{ env.BRANCH_NAME }}`
> Automatically created by the Unturned Redist update workflow.
draft: false
labels: |
automated
redist-update
${{ matrix.variant }}
- name: Summary
if: always()
run: |
{
echo "## Update Summary for ${{ matrix.variant }}"
echo "- **Branch**: \`${{ env.BRANCH_NAME }}\`"
echo "- **Source Changed**: ${{ steps.gate.outputs.changed }}"
if [ "${{ steps.gate.outputs.changed }}" = "true" ]; then
echo "- **Git Changes**: ${{ steps.check_git_changes.outputs.has_git_changes }}"
if [ -n "${{ steps.create_pr.outputs.pull-request-number }}" ]; then
echo "- **PR**: #${{ steps.create_pr.outputs.pull-request-number }} (${{ steps.create_pr.outputs.pull-request-url }})"
fi
else
echo "- **Status**: Skipped (source unchanged)"
fi
} >> "$GITHUB_STEP_SUMMARY"
# On any scheduled-run failure, open (or comment on) a tracking issue so breakage
# isn't silent.
notify-failure:
name: "Notify on failure"
needs: [load, download_sources, update_variant]
if: failure() && github.event_name == 'schedule'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Open or update tracking issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
marker="Redist auto-update workflow failed"
body=$(printf '⚠️ The scheduled redist update workflow failed.\n\nRun: %s\n\nA source download (Steam auth/network, DepotDownloader, steamcmd) or a variant update (the redist tool or PR creation) errored. Check the run for the failing job.' "$RUN_URL")
# --repo is required here: no checkout, and gh doesn't read GITHUB_REPOSITORY.
gh label create update-failure --repo "$REPO" --color B60205 --description "Automated redist update failure" --force >/dev/null 2>&1 || true
existing=$(gh issue list --repo "$REPO" --state open --search "in:title \"$marker\"" --json number --jq '.[0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo "$REPO" --body "$body"
else
gh issue create --repo "$REPO" --title "⚠️ $marker" --body "$body" --label update-failure
fi
workflow-keepalive:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
# Re-enable this scheduled workflow (GitHub disables them after 60 days idle).
# Inlined gh-api call, no external dependency. continue-on-error: the default token
# occasionally 401s here; keepalive is non-critical and self-healing, so a blip
# must not redden the run.
continue-on-error: true
permissions:
actions: write
steps:
- name: Re-enable this scheduled workflow
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# GITHUB_WORKFLOW_REF is "<owner>/<repo>/.github/workflows/<file>@<ref>".
# Validate the shape, derive the workflow file path, and re-enable it.
case "${GITHUB_WORKFLOW_REF:?}" in
"${GITHUB_REPOSITORY:?}"/.github/workflows/*.y*ml@*) ;;
*) echo "::error::Unexpected GITHUB_WORKFLOW_REF: ${GITHUB_WORKFLOW_REF}"; exit 1 ;;
esac
workflow="${GITHUB_WORKFLOW_REF%%@*}"
workflow="${workflow#"${GITHUB_REPOSITORY}"/.github/workflows/}"
gh api -X PUT "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/enable"