Skip to content

Bump version to 13.0.0-alpha.25 #22

Bump version to 13.0.0-alpha.25

Bump version to 13.0.0-alpha.25 #22

# 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