Skip to content

Instantly share code, notes, and snippets.

@jacobmix
Last active November 27, 2025 01:00
Show Gist options
  • Select an option

  • Save jacobmix/62153185eeb9eec2d03052b9cd76c608 to your computer and use it in GitHub Desktop.

Select an option

Save jacobmix/62153185eeb9eec2d03052b9cd76c608 to your computer and use it in GitHub Desktop.
Workflow example for Archipelago RAC1
name: Build and Release APWorld
on:
push:
branches:
- main
- staging
workflow_dispatch:
inputs:
prerelease:
description: "Is this a pre-release?"
type: boolean
required: true
default: false
custom_version:
description: "Optional exact version (leave blank to auto-generate)"
required: false
repository_dispatch:
types: [trigger-build-apworld]
concurrency:
group: "build-release-apworld"
cancel-in-progress: false
jobs:
build:
runs-on: windows-latest
outputs:
branch: ${{ steps.branch.outputs.branch }}
should_build: ${{ steps.changes.outputs.should_build }}
steps:
- name: Determine branch to build
id: branch
shell: bash
run: |
# 1. workflow_dispatch: use the branch the user selected in the UI
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# github.ref always contains refs/heads/<branch>
branch="${GITHUB_REF#refs/heads/}"
echo "branch=$branch" >> $GITHUB_OUTPUT
echo "🏗️ Manual trigger — using branch: $branch"
exit 0
fi
# 2. repository_dispatch override
if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then
echo "branch=${{ github.event.client_payload.branch }}" >> $GITHUB_OUTPUT
echo "🏗️ External trigger — using payload branch: ${{ github.event.client_payload.branch }}"
exit 0
fi
# 3. push event default
branch="${GITHUB_REF#refs/heads/}"
echo "🏗️ Push event — using branch: $branch"
echo "branch=$branch" >> $GITHUB_OUTPUT
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ steps.branch.outputs.branch }}
fetch-depth: 0
- name: Check if build should run (skip yml/md-only changes)
id: changes
shell: bash
run: |
branch="${{ steps.branch.outputs.branch }}"
event_name="${{ github.event_name }}"
# Always build when manually or externally triggered
if [[ "$event_name" == "workflow_dispatch" || "$event_name" == "repository_dispatch" ]]; then
echo "🔧 Manual or external trigger detected — building regardless of changes."
echo "should_build=true" >> $GITHUB_OUTPUT
exit 0
fi
echo "🔍 Checking for relevant changes since last commit..."
changed_files=$(git diff --name-only HEAD~1 HEAD || true)
echo "Changed files:"
echo "$changed_files"
if [[ -z "$changed_files" ]]; then
echo "🟡 No changes detected. Skipping build."
echo "should_build=false" >> $GITHUB_OUTPUT
exit 0
fi
# Skip if only yml or md files changed
if echo "$changed_files" | grep -Eq '\.(md|ya?ml)$'; then
non_workflow_changes=$(echo "$changed_files" | grep -Ev '\.(md|ya?ml)$' || true)
if [[ -z "$non_workflow_changes" ]]; then
echo "🟡 Only yml/md file changes detected — skipping build."
echo "should_build=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "✅ Relevant changes detected — proceed with build/release."
echo "should_build=true" >> $GITHUB_OUTPUT
- name: Set up Python 3.12
if: steps.changes.outputs.should_build == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Python dependencies
if: steps.changes.outputs.should_build == 'true'
run: |
python -m pip install --upgrade pip setuptools wheel factorio-rcon-py nest-asyncio pyinstaller pillow requests zstandard pyevermizer dolphin-memory-engine Pymem --disable-pip-version-check || true # ignore failures for other worlds
pip install -r requirements.txt
pip install maseya-z3pr>=1.0.0rc1 xxtea>=3.0.0
# pip install zilliandomizer==0.9.1
- name: 🧪 Run Archipelago generation and create YAML
if: steps.changes.outputs.should_build == 'true'
shell: pwsh
env:
AP_NO_UPDATE: "1"
SKIP_REQUIREMENTS_UPDATE: "1"
run: |
$ErrorActionPreference = "Stop"
$rootDir = "${{ github.workspace }}"
Set-Location $rootDir
Write-Host "🧪 Generating template options..."
python Launcher.py "Generate Template Options" || true
Write-Host "📂 Checking Players/Templates folder..."
Get-ChildItem -Recurse Players/Templates | Write-Host
# Ensure Players folder exists
if (-Not (Test-Path Players)) { New-Item -ItemType Directory -Path Players }
# Copy the RaC template YAML
$yamlSource = Join-Path $rootDir 'Players/Templates/Ratchet & Clank.yaml'
$yamlDest = Join-Path $rootDir 'Players/Ratchet & Clank.yaml'
$yamlRoot = Join-Path $rootDir 'rac1.yaml'
if (-Not (Test-Path $yamlSource)) { Write-Error "Template YAML not found: $yamlSource" }
Copy-Item -LiteralPath $yamlSource -Destination $yamlDest -Force
# Read YAML
$yamlContent = Get-Content -LiteralPath $yamlDest -Raw
# Detect existing version
if ($yamlContent -match 'version:\s*(\d+)\.(\d+)\.(\d+)') {
$major = [int]$matches[1]
$minor = [int]$matches[2]
$patch = [int]$matches[3]
Write-Host "Found template version: $major.$minor.$patch"
if ($patch -gt 0) {
$patch -= 1
} elseif ($minor -gt 0) {
$minor -= 1
$patch = 0
} else {
Write-Warning "Version is already at 0.0.0 — cannot decrement"
}
$newVersion = "$major.$minor.$patch"
Write-Host "Lowered version for build: $newVersion"
# Replace version in YAML
$yamlContent = $yamlContent -replace 'version:\s*\d+\.\d+\.\d+', "version: $newVersion"
} else {
Write-Warning "No version line found in YAML — skipping version edit"
}
# Write YAML back
Set-Content -LiteralPath $yamlDest -Value $yamlContent -Force
Copy-Item -LiteralPath $yamlDest -Destination $yamlRoot -Force
Write-Host "✅ YAML edited successfully."
# Optional: test seed generation
Write-Host "🧪 Testing seed generation..."
python Generate.py || true
Write-Host "✅ Test step completed."
- name: Build .apworld for RAC1
if: steps.changes.outputs.should_build == 'true'
shell: bash
run: |
echo "📦 Building RAC1.apworld..."
python3 - <<'EOF'
import sys, os, zipfile
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0, os.path.abspath(".")) # Ensure local imports work
from worlds import Files
from Utils import Version
class MyWorldContainer(Files.APWorldContainer):
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
# Write archipelago.json at root (handled by base class)
super().write_contents(opened_zipfile)
# Include all files from world_folder inside RAC1/
for root, dirs, files in os.walk(self.world_folder):
for file in files:
if file.endswith(".pyc") or file == "archipelago.json":
continue
full_path = os.path.join(root, file)
rel_path = os.path.relpath(full_path, self.world_folder)
zip_path = os.path.join("RAC1", rel_path).replace("\\", "/") # POSIX paths
opened_zipfile.write(full_path, zip_path)
src_folder = "worlds/RAC1"
dest_file = "RAC1.apworld"
# Create container
world = MyWorldContainer(dest_file)
world.game = "RaC1"
world.world_folder = src_folder
world.world_version = Version(1, 0, 0)
world.minimum_ap_version = Version(0, 5, 0)
# Write the .apworld
world.write()
print(f"Successfully created {dest_file} with all files under RAC1/")
EOF
- name: Upload build artifact
if: steps.changes.outputs.should_build == 'true'
uses: actions/upload-artifact@v4
with:
name: RAC1.apworld
path: ./RAC1.apworld
release:
needs: build
runs-on: ubuntu-latest
if: needs.build.outputs.should_build == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ needs.build.outputs.branch }}
fetch-depth: 0
- name: Download template artifact
uses: actions/download-artifact@v4
with:
name: rac1.yaml
path: .
- name: Download built artifact
uses: actions/download-artifact@v4
with:
name: RAC1.apworld
path: .
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Determine next semantic version
id: semver
shell: bash
run: |
set -e
branch="${{ needs.build.outputs.branch }}"
git fetch --tags
echo "🔢 Determining next semantic version for branch: $branch"
# Get latest tag (any tag, pre or stable)
latest_tag=$(git tag --sort=-v:refname | head -n 1 || true)
[[ -z "$latest_tag" ]] && latest_tag="v0.0.0"
echo "Latest tag: $latest_tag"
# Parse version
tag_stripped="${latest_tag#v}"
IFS='.' read -r major minor patch <<< "$tag_stripped"
major=${major:-0}; minor=${minor:-0}; patch=${patch:-0}
# Manual inputs
manual="${{ github.event_name == 'workflow_dispatch' }}"
manual_pre="${{ github.event.inputs.prerelease || false }}"
custom_version="${{ github.event.inputs.custom_version }}"
# Custom version overrides everything
if [[ "$manual" == "true" && -n "$custom_version" ]]; then
custom_version="$(echo -n "$custom_version" | xargs)"
echo "📝 Custom version provided: $custom_version"
if [[ "$custom_version" == *"-pre" ]]; then
is_pre="true"
else
is_pre="false"
fi
echo "version=$custom_version" >> $GITHUB_OUTPUT
echo "is_pre=$is_pre" >> $GITHUB_OUTPUT
echo "branch=$branch" >> $GITHUB_OUTPUT
exit 0
fi
# Determine prerelease
if [[ "$manual" == "true" ]]; then
is_pre="$manual_pre"
else
# Auto pre-release for non-main branches by default
is_pre="$([[ "$branch" != "main" ]] && echo "true" || echo "false")"
fi
# Auto bump version
if [[ "$is_pre" == "true" ]]; then
# Pre-release: increment patch from latest tag
patch=$((patch + 1))
version="${major}.${minor}.${patch}-pre"
else
# Stable: increment minor, reset patch
minor=$((minor + 1))
patch=0
version="${major}.${minor}.${patch}"
fi
echo "✅ Next version: $version"
echo "version=$version" >> $GITHUB_OUTPUT
echo "branch=$branch" >> $GITHUB_OUTPUT
echo "is_pre=$is_pre" >> $GITHUB_OUTPUT
- name: Add manifest to .apworld and set CLIENT_VERSION
shell: bash
run: |
set -e
echo "🧾 Updating archipelago.json manifest & set CLIENT_VERSION"
version="${{ steps.semver.outputs.version }}"
version="${version#v}"
version="${version%-pre}"
# Get authors from git history
git fetch origin ${{ needs.build.outputs.branch }}
authors=$(git log --format='%aN' -- worlds/RAC1 | sort -u)
# Add custom authors. Example: ("NewContributor" "AnotherDev")
custom_authors=()
for a in "${custom_authors[@]}"; do
if ! echo "$authors" | grep -Fxq "$a"; then
authors="${authors}"$'\n'"${a}"
fi
done
# Default if no authors found
if [ -z "$authors" ]; then
authors="Unknown"
fi
# Convert authors to JSON array
authors_json=$(echo "$authors" | jq -R . | jq -s .)
# Extract AP version
ap_version=$(grep '^__version__ = ' Utils.py | cut -d'"' -f2)
echo "Maximum AP version: $ap_version"
# Unzip the built .apworld
mkdir -p tmp_unzip
unzip -o -q RAC1.apworld -d tmp_unzip
# Update only root archipelago.json
jq --arg world_version "$version" \
--argjson authors "$authors_json" \
--arg min_ap "0.5.0" \
--arg max_ap "$ap_version" \
'.world_version = $world_version
| .authors = $authors
| .minimum_ap_version = $min_ap' \
tmp_unzip/archipelago.json > tmp_unzip/archipelago.json.tmp
# Maximum AP not set. If needed can use the below to set to current used AP version
# | .maximum_ap_version = $max_ap
# Replace the old file with the updated one
mv tmp_unzip/archipelago.json.tmp tmp_unzip/archipelago.json
# Remove nested archipelago.json but keep the root one
find tmp_unzip/RAC1 -type f -name 'archipelago.json' -exec rm {} \;
echo "🗑️ Removed extra archipelago.json"
# Update CLIENT_VERSION in RAC1Client.py
#client_file="tmp_unzip/RAC1/RAC1Client.py"
#old_version=$(grep '^CLIENT_VERSION = ' "$client_file" | cut -d'"' -f2 || echo "<none>")
#echo "🔧 Updating CLIENT_VERSION in $client_file from $old_version to $version"
#sed -i -E "s/^(CLIENT_VERSION = ).*/\1\"$version\"/" "$client_file"
# Remove backup if any (from sed -i on some platforms)
#rm -f "${client_file}.bak"
# Rezip the .apworld
cd tmp_unzip
zip -qr ../RAC1.apworld *
cd ..
echo "✅ Updated archipelago.json at root"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
RAC1.apworld
rac1.yaml
tag_name: v${{ steps.semver.outputs.version }}
name: "RAC1 APWorld v${{ steps.semver.outputs.version }}"
body: "Generating release notes..."
prerelease: ${{ steps.semver.outputs.is_pre == 'true' }}
target_commitish: ${{ needs.build.outputs.branch }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate release notes
shell: bash
run: |
set -e
repo_url="https://github.com/${{ github.repository }}"
branch="${{ steps.semver.outputs.branch }}"
new_tag="v${{ steps.semver.outputs.version }}"
git fetch --tags
echo "🔍 All recent tags (descending):"
git tag --sort=-v:refname | head -n 10 || true
# Get previous tag (latest before new_tag)
if [[ "$branch" == "main" ]]; then
# previous stable release (ignore -pre)
prev_tag=$(git tag --sort=-v:refname | grep -v -- '-pre' | grep -v "^$new_tag\$" | head -n 1)
else
# previous tag of any type for pre-release
prev_tag=$(git tag --sort=-v:refname | grep -v "^$new_tag\$" | head -n 1)
fi
# Full compare link
if [[ -z "$prev_tag" ]]; then
echo "⚠️ No previous relevant tag found — showing full history"
changelog="${repo_url}/commits/${branch}"
else
changelog="${repo_url}/compare/${prev_tag}...${new_tag}"
fi
echo "Previous tag: ${prev_tag:-<none>}"
echo "New tag: $new_tag"
echo "Changelog link: $changelog"
# Only show commits between the two tags
if [[ -n "$prev_tag" ]]; then
commit_list=$(git log "${prev_tag}..${new_tag}" --pretty=format:"- %s (%h)")
total_commits=$(git rev-list "${prev_tag}..${new_tag}" --count)
else
commit_list=$(git log "${new_tag}" --pretty=format:"- %s (%h)")
total_commits=$(git rev-list "${new_tag}" --count)
fi
if [[ $total_commits -eq 0 ]]; then
commit_list="- (no new commits since last relevant release)"
echo "🟡 No new commits since last release"
elif [[ $total_commits -gt 200 ]]; then
extra_commits=$((total_commits - 200))
commit_list="${commit_list}\n- ...and ${extra_commits} more commits."
fi
cat <<EOF > release_notes.md
## RAC1 APWorld ${new_tag}
**Branch:** ${branch}
**Date:** $(date -u +"%Y-%m-%d %H:%M UTC")
<details>
<summary><strong>📜 Summary (${total_commits} changes since ${prev_tag:-initial commit})</strong></summary>
${commit_list}
</details>
### Notes
This release was automatically generated from the **${branch}** branch.
Latest (main) releases are recommended, as pre-releases (staging) may contain experimental or in-progress changes.
**Full changelog and compare**: ${changelog}
EOF
- name: Update GitHub Release with generated notes
shell: bash
run: |
new_tag="v${{ steps.semver.outputs.version }}"
gh release edit "$new_tag" --notes-file release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment