Last active
November 27, 2025 01:00
-
-
Save jacobmix/62153185eeb9eec2d03052b9cd76c608 to your computer and use it in GitHub Desktop.
Workflow example for Archipelago RAC1
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: 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