Bump version to 13.0.0-alpha.25 #22
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
| # Unified Foundry VTT release workflow for ShadowFoundry projects. | |
| # Works for both systems (system.json) and modules (module.json). | |
| # | |
| # Per-project configuration — update these env vars when copying to a new repo: | |
| # PROJECT_NAME — archive directory and zip prefix (e.g. sr6, sr5, sr6-compendiums) | |
| # PROJECT_TITLE — human-readable name for release notes | |
| # PROJECT_TYPE — "system" or "module" (determines manifest filename) | |
| # RELEASE_CONTENTS — space-separated list of files/dirs to include in the archive | |
| # For packs, use "packs" (the _source dirs are excluded automatically) | |
| name: Foundry VTT ShadowFoundry Release Builder | |
| on: | |
| push: | |
| tags: | |
| - '**' | |
| permissions: | |
| contents: write | |
| env: | |
| PROJECT_NAME: sr5 | |
| PROJECT_TITLE: Shadowrun 5 System | |
| PROJECT_TYPE: system | |
| RELEASE_CONTENTS: system.json modules css templates lang assets README.md LICENCE.md BUG_REPORT.md CONTRIBUTING.md | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # ── 1. Detect release type ────────────────────────────────────────── | |
| - name: Detect release type | |
| id: vars | |
| run: | | |
| TAG="${GITHUB_REF_NAME}" | |
| echo "tag=${TAG}" >> $GITHUB_OUTPUT | |
| if echo "$TAG" | grep -qP '^[0-9]+\.[0-9]+\.[0-9]+-([a-zA-Z]+)\.[0-9]+$'; then | |
| IDENTIFIER=$(echo "$TAG" | grep -oP '(?<=-)[a-zA-Z]+(?=\.[0-9]+$)') | |
| UPPER_ID=$(echo "$IDENTIFIER" | tr '[:lower:]' '[:upper:]') | |
| echo "type=prerelease" >> $GITHUB_OUTPUT | |
| echo "identifier=${IDENTIFIER}" >> $GITHUB_OUTPUT | |
| echo "title=[${UPPER_ID} Release] ${TAG}" >> $GITHUB_OUTPUT | |
| echo "prerelease_flag=--prerelease" >> $GITHUB_OUTPUT | |
| echo "Detected pre-release with identifier: ${IDENTIFIER}" | |
| elif echo "$TAG" | grep -qP '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "type=stable" >> $GITHUB_OUTPUT | |
| echo "identifier=" >> $GITHUB_OUTPUT | |
| echo "title=[Release] ${TAG}" >> $GITHUB_OUTPUT | |
| echo "prerelease_flag=" >> $GITHUB_OUTPUT | |
| echo "Detected stable release" | |
| else | |
| echo "Tag '$TAG' is not a valid semver tag, skipping" | |
| echo "type=skip" >> $GITHUB_OUTPUT | |
| fi | |
| # ── 2. Setup ──────────────────────────────────────────────────────── | |
| - name: Install Node | |
| if: steps.vars.outputs.type != 'skip' | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: '24.13.1' | |
| - name: Check out repository code | |
| if: steps.vars.outputs.type != 'skip' | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify tag is on correct branch | |
| if: steps.vars.outputs.type != 'skip' | |
| run: | | |
| BRANCHES=$(git branch -r --contains HEAD | sed 's|origin/||' | tr -d ' ') | |
| if [ "${{ steps.vars.outputs.type }}" = "stable" ]; then | |
| if ! echo "$BRANCHES" | grep -qE '^(main|master)$'; then | |
| echo "::error::Stable release tag must be on the main branch" | |
| exit 1 | |
| fi | |
| else | |
| if echo "$BRANCHES" | grep -qE '^(main|master)$'; then | |
| echo "::error::Pre-release tags must not be on the main/master branch" | |
| exit 1 | |
| fi | |
| fi | |
| # ── 3. Validate ───────────────────────────────────────────────────── | |
| - name: Install dependencies | |
| if: steps.vars.outputs.type != 'skip' | |
| run: npm ci | |
| - name: Run checks | |
| if: steps.vars.outputs.type != 'skip' | |
| run: npm run check | |
| # ── 4. Detect manifest file ───────────────────────────────────────── | |
| - name: Detect manifest | |
| if: steps.vars.outputs.type != 'skip' | |
| id: manifest | |
| run: | | |
| if [ "${{ env.PROJECT_TYPE }}" = "system" ]; then | |
| echo "file=system.json" >> $GITHUB_OUTPUT | |
| else | |
| echo "file=module.json" >> $GITHUB_OUTPUT | |
| fi | |
| # ── 5. Update manifest for release ────────────────────────────────── | |
| - name: Update manifest for release | |
| if: steps.vars.outputs.type != 'skip' | |
| run: | | |
| TAG="${{ steps.vars.outputs.tag }}" | |
| REPO="${{ github.repository }}" | |
| TYPE="${{ steps.vars.outputs.type }}" | |
| IDENTIFIER="${{ steps.vars.outputs.identifier }}" | |
| MANIFEST_FILE="${{ steps.manifest.outputs.file }}" | |
| if [ "$TYPE" = "prerelease" ]; then | |
| MANIFEST_URL="https://github.com/${REPO}/releases/download/pre-release-${IDENTIFIER}/${MANIFEST_FILE}" | |
| else | |
| MANIFEST_URL="https://github.com/${REPO}/releases/latest/download/${MANIFEST_FILE}" | |
| fi | |
| jq \ | |
| --arg ver "$TAG" \ | |
| --arg manifest "$MANIFEST_URL" \ | |
| --arg download "https://github.com/${REPO}/releases/download/${TAG}/${{ env.PROJECT_NAME }}_${TAG}.zip" \ | |
| '.version = $ver | .manifest = $manifest | .download = $download' \ | |
| "$MANIFEST_FILE" > manifest_updated.json | |
| mv manifest_updated.json "$MANIFEST_FILE" | |
| cp "$MANIFEST_FILE" "/tmp/${MANIFEST_FILE}" | |
| # ── 6. Package ────────────────────────────────────────────────────── | |
| - name: Build compendiums | |
| if: steps.vars.outputs.type != 'skip' && env.PROJECT_TYPE == 'module' | |
| run: npm run build | |
| - name: Prepare archive | |
| if: steps.vars.outputs.type != 'skip' | |
| run: | | |
| # Save Discord scripts before packaging | |
| cp -r scripts/push_to_discord /tmp/push_to_discord | |
| # Copy only the declared release contents into the archive directory | |
| mkdir -p "${{ env.PROJECT_NAME }}" | |
| for item in ${{ env.RELEASE_CONTENTS }}; do | |
| [ ! -e "$item" ] && continue | |
| if [ "$item" = "packs" ]; then | |
| # Copy pack data excluding _source directories | |
| mkdir -p "${{ env.PROJECT_NAME }}/packs" | |
| for dir in packs/*/; do | |
| pack=$(basename "$dir") | |
| mkdir -p "${{ env.PROJECT_NAME }}/packs/$pack" | |
| find "$dir" -maxdepth 1 -not -name '_source' -not -path "$dir" -exec cp -r {} "${{ env.PROJECT_NAME }}/packs/$pack/" \; | |
| done | |
| else | |
| cp -r "$item" "${{ env.PROJECT_NAME }}/" | |
| fi | |
| done | |
| zip -r "${{ env.PROJECT_NAME }}_${{ steps.vars.outputs.tag }}.zip" "${{ env.PROJECT_NAME }}" | |
| # ── 7. Zip size check ─────────────────────────────────────────────── | |
| - name: Check zip size | |
| if: steps.vars.outputs.type != 'skip' | |
| id: zip_check | |
| run: | | |
| ZIP_FILE="${{ env.PROJECT_NAME }}_${{ steps.vars.outputs.tag }}.zip" | |
| SIZE_BYTES=$(stat --format="%s" "$ZIP_FILE") | |
| SIZE_MB=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES / 1048576}") | |
| echo "size_bytes=$SIZE_BYTES" >> $GITHUB_OUTPUT | |
| echo "size_mb=$SIZE_MB" >> $GITHUB_OUTPUT | |
| echo "Total zip size: ${SIZE_MB} MB" | |
| LIMIT=10383360 # 9.9 MB | |
| if [ "$SIZE_BYTES" -ge "$LIMIT" ]; then | |
| echo "::warning::Zip file is ${SIZE_MB} MB, exceeds Discord's 9.9 MB upload limit" | |
| echo "over_limit=true" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "=== Top 30 files by size ===" | |
| unzip -l "$ZIP_FILE" | grep -v '/$' | tail -n +4 | head -n -2 | sort -rn -k1 2>/dev/null | head -30 | |
| echo "" | |
| echo "=== Size by directory (KB) ===" | |
| unzip -l "$ZIP_FILE" | grep -v '/$' | tail -n +4 | head -n -2 | awk '{dir=$4; sub(/\/[^\/]*$/, "", dir); sizes[dir]+=$1} END {for (d in sizes) printf "%8.1f KB %s\n", sizes[d]/1024, d}' | sort -rn -k1 2>/dev/null | head -20 | |
| else | |
| echo "over_limit=false" >> $GITHUB_OUTPUT | |
| echo "Zip is under 9.9 MB, ready for Discord upload" | |
| fi | |
| # ── 8. Release notes ──────────────────────────────────────────────── | |
| - name: Build pre-release notes | |
| if: steps.vars.outputs.type == 'prerelease' | |
| id: prerelease_notes | |
| run: | | |
| IDENTIFIER="${{ steps.vars.outputs.identifier }}" | |
| PREV_TAG=$(git tag --list "*-${IDENTIFIER}.*" --sort=-v:refname | grep -v "^${GITHUB_REF_NAME}$" | head -1) | |
| if [ -n "$PREV_TAG" ]; then | |
| CHANGELOG=$(git log --reverse --pretty=format:"- %s" "${PREV_TAG}..${GITHUB_REF_NAME}") | |
| else | |
| CHANGELOG=$(git log --reverse --pretty=format:"- %s" "${GITHUB_REF_NAME}") | |
| fi | |
| case "$IDENTIFIER" in | |
| alpha) | |
| WARNING="This is an **alpha** release. It is experimental and intended for early testing only. Expect breaking changes, missing features, and incomplete functionality. **Do not use in production.**" | |
| ;; | |
| beta) | |
| WARNING="This is a **beta** release. Core features are implemented but may still contain bugs. Breaking changes are still possible. Use for testing and feedback purposes only." | |
| ;; | |
| rc) | |
| WARNING="This is a **release candidate**. It is feature-complete and believed to be stable, but has not yet been fully validated. Please report any issues before the final release." | |
| ;; | |
| dev) | |
| WARNING="This is a **development** snapshot. It reflects the latest state of active development and may be highly unstable. **For developer testing only — do not use in production.**" | |
| ;; | |
| snapshot) | |
| WARNING="This is a **snapshot** build from a specific point in development. It may contain incomplete or untested changes. Use at your own risk." | |
| ;; | |
| canary) | |
| WARNING="This is a **canary** release built from the latest development branch. It includes the newest changes but has not been stabilized. Breakage is expected." | |
| ;; | |
| *) | |
| WARNING="This is a **${IDENTIFIER}** pre-release. It may be unstable or incomplete. **Do not use in production.**" | |
| ;; | |
| esac | |
| REPO="${{ github.repository }}" | |
| MANIFEST_URL="https://github.com/${REPO}/releases/download/pre-release-${IDENTIFIER}/${{ steps.manifest.outputs.file }}" | |
| { | |
| echo "notes<<NOTES_EOF" | |
| echo "$WARNING" | |
| echo "" | |
| echo "**Changes since ${PREV_TAG:-initial}** (${{ steps.zip_check.outputs.size_mb }} MB):" | |
| echo "$CHANGELOG" | |
| echo "" | |
| echo "**Manifest URL (${IDENTIFIER} channel):** \`${MANIFEST_URL}\`" | |
| echo "NOTES_EOF" | |
| } >> $GITHUB_OUTPUT | |
| - name: Build stable release notes | |
| if: steps.vars.outputs.type == 'stable' | |
| id: stable_notes | |
| run: | | |
| PREV_TAG=$(git tag --list --sort=-v:refname | grep -P '^[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^${GITHUB_REF_NAME}$" | head -1) | |
| if [ -n "$PREV_TAG" ]; then | |
| CHANGELOG=$(git log --reverse --pretty=format:"- %s" "${PREV_TAG}..${GITHUB_REF_NAME}") | |
| else | |
| CHANGELOG=$(git log --reverse --pretty=format:"- %s" "${GITHUB_REF_NAME}") | |
| fi | |
| REPO="${{ github.repository }}" | |
| MANIFEST_URL="https://github.com/${REPO}/releases/latest/download/${{ steps.manifest.outputs.file }}" | |
| { | |
| echo "notes<<NOTES_EOF" | |
| echo "${{ env.PROJECT_TITLE }} release ${{ steps.vars.outputs.tag }} (${{ steps.zip_check.outputs.size_mb }} MB)" | |
| echo "" | |
| echo "**Changes since ${PREV_TAG:-initial}:**" | |
| echo "$CHANGELOG" | |
| echo "" | |
| echo "**Manifest URL (stable channel):** \`${MANIFEST_URL}\`" | |
| echo "NOTES_EOF" | |
| } >> $GITHUB_OUTPUT | |
| # ── 9. GitHub release ─────────────────────────────────────────────── | |
| - name: Create GitHub release | |
| if: steps.vars.outputs.type != 'skip' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_NOTES: ${{ steps.prerelease_notes.outputs.notes }}${{ steps.stable_notes.outputs.notes }} | |
| run: | | |
| gh release create "${{ steps.vars.outputs.tag }}" \ | |
| "${{ env.PROJECT_NAME }}_${{ steps.vars.outputs.tag }}.zip" \ | |
| "/tmp/${{ steps.manifest.outputs.file }}" \ | |
| --title "${{ steps.vars.outputs.title }}" \ | |
| ${{ steps.vars.outputs.prerelease_flag }} \ | |
| --notes "$RELEASE_NOTES" | |
| - name: Update persistent pre-release manifest | |
| if: steps.vars.outputs.type == 'prerelease' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| MANIFEST_FILE="${{ steps.manifest.outputs.file }}" | |
| CHANNEL="pre-release-${{ steps.vars.outputs.identifier }}" | |
| INSTALL_URL="https://github.com/${{ github.repository }}/releases/download/${CHANNEL}/${MANIFEST_FILE}" | |
| if gh release view "$CHANNEL" &>/dev/null; then | |
| gh release upload "$CHANNEL" \ | |
| "/tmp/${MANIFEST_FILE}" \ | |
| --clobber | |
| else | |
| gh release create "$CHANNEL" \ | |
| "/tmp/${MANIFEST_FILE}" \ | |
| --title "${{ steps.vars.outputs.identifier }} Channel Manifest" \ | |
| --prerelease \ | |
| --notes "Persistent ${{ steps.vars.outputs.identifier }} manifest. Install URL: ${INSTALL_URL}" | |
| fi | |
| # ── 10. Discord notification ──────────────────────────────────────── | |
| - name: Prepare Discord scripts | |
| if: steps.vars.outputs.type != 'skip' | |
| run: | | |
| cd /tmp/push_to_discord | |
| npm install | |
| chmod +x *.js | |
| - name: Send Discord notification | |
| if: steps.vars.outputs.type != 'skip' | |
| env: | |
| WEBHOOK_URL: ${{ steps.vars.outputs.type == 'prerelease' && secrets.DISCORD_ALPHA_CHANNEL_URL || secrets.DISCORD_STABLE_CHANNEL_URL }} | |
| RELEASE_NOTES: ${{ steps.prerelease_notes.outputs.notes }}${{ steps.stable_notes.outputs.notes }} | |
| IS_PRIVATE: ${{ github.event.repository.private }} | |
| run: | | |
| # Build Discord message body | |
| BODY="$RELEASE_NOTES" | |
| if [ "$IS_PRIVATE" = "true" ]; then | |
| # Strip manifest URL and download link (not reachable for private repos) | |
| BODY=$(echo "$BODY" | grep -v '^\*\*Manifest URL') | |
| else | |
| RELEASE_URL="<https://github.com/${{ github.repository }}/releases/tag/${{ steps.vars.outputs.tag }}>" | |
| BODY=$(printf '%s\n\nDownload: %s' "$BODY" "$RELEASE_URL") | |
| fi | |
| if [ ${#BODY} -gt 1990 ]; then | |
| # Truncate the changelog section while preserving header and footer | |
| HEADER=$(echo "$BODY" | sed -n '1,/^\*\*Changes since/p') | |
| FOOTER=$(echo "$BODY" | sed -n '/^\*\*Manifest URL\|^Download:/,$p') | |
| FOOTER_LEN=${#FOOTER} | |
| HEADER_LEN=${#HEADER} | |
| # Reserve space for header + footer + ellipsis + newlines | |
| MAX_CHANGELOG=$((1990 - HEADER_LEN - FOOTER_LEN - 10)) | |
| if [ $MAX_CHANGELOG -gt 20 ]; then | |
| CHANGELOG=$(echo "$BODY" | sed -n '/^\*\*Changes since/,/^\*\*Manifest URL\|^Download:/{ /^\*\*Changes since/d; /^\*\*Manifest URL\|^Download:/d; p; }') | |
| CHANGELOG="${CHANGELOG:0:$MAX_CHANGELOG}..." | |
| BODY=$(printf '%s\n%s\n\n%s' "$HEADER" "$CHANGELOG" "$FOOTER") | |
| else | |
| BODY="${BODY:0:1987}..." | |
| fi | |
| fi | |
| # Send text message | |
| jq -n --arg content "$BODY" '{content: $content, flags: 4}' | \ | |
| curl -sf -H "Content-Type: application/json" -d @- "$WEBHOOK_URL" | |
| # Upload file(s) only for private repos | |
| if [ "$IS_PRIVATE" = "true" ]; then | |
| ZIP_FILE="${{ env.PROJECT_NAME }}_${{ steps.vars.outputs.tag }}.zip" | |
| if [ "${{ steps.zip_check.outputs.over_limit }}" = "true" ]; then | |
| # Split into 9.9 MB chunks and upload each | |
| split -b 10383360 -d --additional-suffix=.zip.part "$ZIP_FILE" "${ZIP_FILE%.zip}_chunk_" | |
| for chunk in ${ZIP_FILE%.zip}_chunk_*; do | |
| /tmp/push_to_discord/file_webhook.js "$WEBHOOK_URL" "$chunk" | |
| done | |
| else | |
| /tmp/push_to_discord/file_webhook.js "$WEBHOOK_URL" "$ZIP_FILE" | |
| fi | |
| fi |