name: 📦 Create GitHub Release
on:
workflow_run:
workflows: ["🔨 Build Mudlet", "🔨 Build Mudlet (windows)"]
types: [completed]
branches: [master, development, release-*, Mudlet-*]
permissions:
contents: write
actions: read
# Both build workflows trigger this; don't cancel the first - the second trigger completes the release
concurrency:
group: create-github-release-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false
jobs:
create-release:
runs-on: ubuntu-latest
if: >
github.repository_owner == 'Mudlet' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: Check if platform builds completed
id: check
uses: actions/github-script@v9
with:
script: |
const triggeringRun = context.payload.workflow_run;
if (triggeringRun.event === 'pull_request') {
core.info(`Skipping: triggered by a pull_request build`);
core.setOutput('ready', 'false');
return;
}
const sha = triggeringRun.head_sha;
const platforms = {
'linux_macos': 'build-mudlet.yml',
'windows': 'build-mudlet-win.yml'
};
const runIds = {};
for (const [key, filename] of Object.entries(platforms)) {
const {data} = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: filename,
head_sha: sha,
status: 'success',
event: triggeringRun.event,
per_page: 1
});
if (data.total_count === 0) {
core.info(`Workflow "${filename}" has not completed successfully yet for ${sha}`);
} else {
runIds[key] = data.workflow_runs[0].id;
}
}
const totalPlatforms = Object.keys(platforms).length;
const completedCount = Object.keys(runIds).length;
if (completedCount === 0) {
core.info('No platform builds have succeeded yet');
core.setOutput('ready', 'false');
return;
}
// Release builds must wait for all platforms; PTBs can proceed with partial
if (completedCount < totalPlatforms && triggeringRun.head_branch !== 'development') {
core.info(`Only ${completedCount}/${totalPlatforms} platforms completed - waiting for all platforms (release build)`);
core.setOutput('ready', 'false');
return;
}
core.setOutput('ready', 'true');
core.setOutput('linux_macos_run_id', runIds['linux_macos'] || '');
core.setOutput('windows_run_id', runIds['windows'] || '');
# Checkout early so .git exists before artifact downloads — actions/checkout
# wipes the workspace when no .git directory is present, even with clean: false
- uses: actions/checkout@v7
if: steps.check.outputs.ready == 'true'
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
# workflow_run always runs this file from the default branch, while the
# checkout above is the commit that was built - which may predate the release
# scripts below. Take them from the same ref as this workflow file.
- uses: actions/checkout@v7
if: steps.check.outputs.ready == 'true'
with:
ref: ${{ github.workflow_sha }}
path: release-scripts
fetch-depth: 1
- uses: leafo/gh-actions-lua@v13
if: steps.check.outputs.ready == 'true'
with:
luaVersion: "5.1.5"
- uses: leafo/gh-actions-luarocks@v6
if: steps.check.outputs.ready == 'true'
- name: Install changelog dependencies
if: steps.check.outputs.ready == 'true'
run: |
luarocks install argparse
luarocks install lunajson
# Download build metadata to determine if this is a release or PTB build (not present for snapshot/PR builds)
- name: Download metadata from Linux/macOS build
if: steps.check.outputs.ready == 'true' && steps.check.outputs.linux_macos_run_id != ''
id: download-metadata-lm
uses: actions/download-artifact@v8
continue-on-error: true
with:
pattern: release-metadata-*
run-id: ${{ steps.check.outputs.linux_macos_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: metadata/
- name: Download metadata from Windows build
if: steps.check.outputs.ready == 'true' && steps.download-metadata-lm.outcome != 'success' && steps.check.outputs.windows_run_id != ''
id: download-metadata-win
uses: actions/download-artifact@v8
continue-on-error: true
with:
pattern: release-metadata-*
run-id: ${{ steps.check.outputs.windows_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: metadata/
- name: Report skipped release
if: steps.check.outputs.ready == 'true' && steps.download-metadata-lm.outcome != 'success' && steps.download-metadata-win.outcome != 'success'
run: echo "::warning::Release metadata download failed - cannot determine release type"
- name: Determine release type
if: steps.check.outputs.ready == 'true' && (steps.download-metadata-lm.outcome == 'success' || steps.download-metadata-win.outcome == 'success')
id: release-type
run: |
META_FILE=$(find metadata/ -name 'release-metadata.json' -type f | head -1)
if [[ -z "${META_FILE}" ]]; then
echo "No release metadata found - this is likely a snapshot or PR build"
echo "type=skip" >> "$GITHUB_OUTPUT"
exit 0
fi
META=$(cat "${META_FILE}")
REF=$(echo "$META" | jq -r '.ref')
EVENT=$(echo "$META" | jq -r '.event')
VERSION=$(echo "$META" | jq -r '.version')
BUILD_SUFFIX=$(echo "$META" | jq -r '.build_suffix')
if [[ -z "${VERSION}" || "${VERSION}" == "null" ]]; then
echo "::error::VERSION field is empty or missing in release metadata"
exit 1
fi
if [[ -z "${REF}" || "${REF}" == "null" ]]; then
echo "::error::REF field is empty or missing in release metadata"
exit 1
fi
COMMIT=$(echo "$META" | jq -r '.commit')
if [[ "$REF" == refs/tags/Mudlet-* ]]; then
# release-scripts/ is this workflow's own ref, so the guard is present
# even when the tagged commit predates it
bash release-scripts/CI/check-release-tag.sh "${VERSION}" "${REF#refs/tags/}"
echo "type=release" >> "$GITHUB_OUTPUT"
echo "tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT"
echo "title=Mudlet ${VERSION}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
elif [[ "$EVENT" == "schedule" ]] || [[ "$BUILD_SUFFIX" == -ptb* ]]; then
if [[ -z "${COMMIT}" || "${COMMIT}" == "null" ]]; then
echo "::error::COMMIT field is empty or missing in release metadata for PTB"
exit 1
fi
# Nothing validates APP_VERSION on development, and a two-component one
# makes a PTB unofferable the same way
bash release-scripts/CI/check-release-tag.sh "${VERSION}"
PTB_TAG="Mudlet-${VERSION}${BUILD_SUFFIX}-${COMMIT}"
echo "type=ptb" >> "$GITHUB_OUTPUT"
echo "tag=${PTB_TAG}" >> "$GITHUB_OUTPUT"
echo "title=Public Test Build" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
else
echo "type=skip" >> "$GITHUB_OUTPUT"
fi
# Download all release assets from both workflow runs
- name: Download Linux/macOS release assets
if: (steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb') && steps.check.outputs.linux_macos_run_id != ''
id: download-linux-macos
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: release-asset-*
run-id: ${{ steps.check.outputs.linux_macos_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: assets/
- name: Download Windows release assets
if: (steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb') && steps.check.outputs.windows_run_id != ''
id: download-windows
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: release-asset-*
run-id: ${{ steps.check.outputs.windows_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: assets/
# A download that fails leaves the release short of a platform, so surface it
# rather than letting continue-on-error hide it
- name: Report asset download failures
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
env:
LINUX_MACOS_OUTCOME: ${{ steps.download-linux-macos.outcome }}
WINDOWS_OUTCOME: ${{ steps.download-windows.outcome }}
run: |
if [[ "${LINUX_MACOS_OUTCOME}" == "failure" ]]; then
echo "::warning::Downloading the Linux/macOS release assets failed - they will be missing from this release"
fi
if [[ "${WINDOWS_OUTCOME}" == "failure" ]]; then
echo "::warning::Downloading the Windows release assets failed - it will be missing from this release"
fi
- name: Prepare release assets
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
env:
RELEASE_TAG: ${{ steps.release-type.outputs.tag }}
RELEASE_TYPE: ${{ steps.release-type.outputs.type }}
run: bash release-scripts/CI/prepare-release-assets.sh assets/ "${RELEASE_TAG}" "${RELEASE_TYPE}"
# The release may already carry assets from the other build workflow's run of
# this job, so its SHA256SUMS.txt has to be merged rather than overwritten
- name: Fetch published SHA256SUMS.txt
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
env:
RELEASE_TAG: ${{ steps.release-type.outputs.tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p published/
if ! gh release view "${RELEASE_TAG}" 2> view-error.txt; then
if grep -qi 'release not found' view-error.txt; then
echo "Release ${RELEASE_TAG} does not exist yet - no checksums to merge"
exit 0
fi
cat view-error.txt
echo "::error::Could not read release ${RELEASE_TAG} - refusing to rebuild its checksums from a partial view"
exit 1
fi
if gh release download "${RELEASE_TAG}" --pattern SHA256SUMS.txt --dir published/; then
echo "Already published on ${RELEASE_TAG}:"
cat published/SHA256SUMS.txt
else
echo "::warning::${RELEASE_TAG} exists but its SHA256SUMS.txt could not be downloaded - any entry only it covers will be lost"
fi
# Assemble SHA256SUMS.txt from per-platform .sha256 sidecar files
- name: Assemble SHA256SUMS.txt
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
run: bash release-scripts/CI/assemble-release-checksums.sh assets/ published/SHA256SUMS.txt
- name: Generate changelog
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
env:
RELEASE_TYPE: ${{ steps.release-type.outputs.type }}
RELEASE_TAG: ${{ steps.release-type.outputs.tag }}
run: |
if [[ "${RELEASE_TYPE}" == "release" ]]; then
# For release: changelog from previous stable release tag to this tag
PREV_TAG=$(git tag --sort=-version:refname | grep '^Mudlet-' | grep -v -- '-ptb' | grep -v "^${RELEASE_TAG}$" | head -1)
echo "Generating changelog from ${PREV_TAG} to ${RELEASE_TAG}"
if [[ -n "${PREV_TAG}" ]]; then
if ! lua CI/generate-changelog.lua -f md -m release \
--start-commit "${PREV_TAG}" --end-commit "${RELEASE_TAG}" > changelog.md; then
echo "::error::Changelog generation failed for stable release"
exit 1
fi
else
echo "::error::No previous release tag found - cannot generate changelog for stable release"
exit 1
fi
else
# For PTB: short changelog since last PTB (prominent) + full changelog since last stable release (collapsed)
STABLE_TAG=$(git tag --sort=-version:refname | grep '^Mudlet-' | grep -v -- '-ptb' | head -1)
PTB_TAG=$(git tag --sort=-version:refname | grep '^Mudlet-' | grep -- '-ptb' | grep -v "^${RELEASE_TAG}$" | head -1)
# Verify the PTB tag is an ancestor of HEAD
if [[ -n "${PTB_TAG}" ]] && ! git merge-base --is-ancestor "${PTB_TAG}" HEAD; then
echo "::warning::Latest PTB tag ${PTB_TAG} is not an ancestor of HEAD, skipping PTB diff"
PTB_TAG=""
fi
# Generate the full changelog since last stable release
FULL_CHANGELOG=""
if [[ -n "${STABLE_TAG}" ]]; then
echo "Generating full changelog from ${STABLE_TAG} to HEAD"
if ! FULL_CHANGELOG=$(lua CI/generate-changelog.lua -f md -m release \
--start-commit "${STABLE_TAG}" --end-commit HEAD); then
echo "::warning::Full changelog generation failed (from ${STABLE_TAG} to HEAD)"
FULL_CHANGELOG=""
fi
fi
# Generate the incremental changelog since last PTB
PTB_CHANGELOG=""
if [[ -n "${PTB_TAG}" ]]; then
echo "Generating PTB changelog from ${PTB_TAG} to HEAD"
if ! PTB_CHANGELOG=$(lua CI/generate-changelog.lua -f md -m release \
--start-commit "${PTB_TAG}" --end-commit HEAD); then
echo "::warning::PTB changelog generation failed (from ${PTB_TAG} to HEAD)"
PTB_CHANGELOG=""
fi
fi
# Assemble the combined changelog
if [[ -n "${PTB_CHANGELOG}" && -n "${FULL_CHANGELOG}" ]]; then
{
echo "#### Changes since last PTB (${PTB_TAG})"
echo ""
echo "${PTB_CHANGELOG}"
echo ""
echo "Full changelog since last release (${STABLE_TAG})
"
echo ""
echo "${FULL_CHANGELOG}"
echo ""
echo "
See release notes for details.
" fi CHANNEL="release" if [[ "${RELEASE_TYPE}" == "ptb" ]]; then CHANNEL="ptb" fi export PUB_DATE=$(date -R) # Get asset download URLs from the GitHub Release RELEASE_JSON=$(gh release view "${RELEASE_TAG}" --json assets) for ARCH in arm64 x86_64; do DMG_FILE=$(find assets/ -name "*-${ARCH}.dmg" -type f | head -1) if [[ -z "${DMG_FILE}" ]]; then echo "No macOS DMG found for ${ARCH} - skipping appcast" continue fi export FILE_SIZE=$(stat --printf="%s" "${DMG_FILE}") DMG_NAME=$(basename "${DMG_FILE}") export DOWNLOAD_URL=$(echo "${RELEASE_JSON}" | jq -r --arg name "${DMG_NAME}" '.assets[] | select(.name == $name) | .url') if [[ -z "${DOWNLOAD_URL}" || "${DOWNLOAD_URL}" == "null" ]]; then echo "::warning::Could not find download URL for ${DMG_NAME}" continue fi # Use envsubst with quoted heredoc to safely handle special characters in changelog HTML envsubst '$RELEASE_VERSION $BUNDLE_VERSION $PUB_DATE $DOWNLOAD_URL $FILE_SIZE $CHANGELOG_HTML' \ > "appcast/${CHANNEL}-${ARCH}.xml" << 'APPCAST_EOF'