mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions - Adds `CI/check-release-tag.sh`: APP_VERSION must be three-component, and a release tag must be exactly `Mudlet-<APP_VERSION>`. - Wires it into the tag-build validation (`CI/validate_deployment.sh`, `CI/validate-deployment-for-windows.sh`) so a bad tag fails minutes after the push, before an asset exists, and into `create-github-release.yml` as the last gate before anything is published. The PTB path gets the version-shape half, which nothing checks on `development` today. - Covers it with `test/ci/release-tag-version-test.sh`, registered as `ReleaseTagVersionTest`. #### Motivation for adding to Mudlet Tagging `Mudlet-5.0` instead of `Mudlet-5.0.0` would strand the entire 4.22.0 user base with no error anywhere, and it is the one version mistake CI does not currently catch. The updater takes the version it offers from the tag, not the binary: `Release::Release()` strips the `Mudlet-` prefix (`src/updater/Release.cpp:49`) and `SemVer::getRegExp()` needs three components (`src/updater/SemVer.cpp:111`), so `"5.0"` is invalid, `Release::operator<` (`src/updater/Release.cpp:96`) reports the release as not newer, and `Feed::getUpdates()` returns nothing. The update check goes on logging `0 update(s) available` - the same line as a week with no release. The asymmetry is what makes it dangerous. A stale APP_VERSION with a correct tag fails loudly, because `CI/prepare-release-assets.sh:62` rejects assets by tag prefix. A short tag with a correct APP_VERSION passes everything, because `Mudlet-5.0.0-linux-x64.AppImage.tar` genuinely does start with `Mudlet-5.0`. **Why the build scripts and not only the workflow:** `create-github-release.yml` is `workflow_run`-triggered, so it cannot fail before the assets are built - by the time it runs, the full matrix has already finished. The validate scripts run at the start of every tag build on all three platforms and already parse APP_VERSION, so that is where the fast failure belongs. The workflow keeps a copy because it always runs from the default branch, so it still guards a tag placed on a commit that predates this change. APP_VERSION is deliberately left at 4.22.0 - bumping it is a release decision, not a QA fix. This guard is what catches a mismatch when the bump happens. #### Other info (issues closed, discussion etc) From the 5.0 release QA sweep, finding C1, "A two-component release tag silently disables auto-update for every existing user". Pre-existing mechanism, no single commit introduced it. Three claims from an earlier draft did not survive checking and were corrected: `src/sparkleupdater.mm` installs no `versionComparatorForUpdater:`, so Sparkle's default component-wise comparator would still offer `5.0` over `4.22.0` (macOS breaks on the opposite mismatch instead); the update check does log, it is just indistinguishable from having nothing to offer; and SemVer does accept a prerelease component, so rejecting `Mudlet-5.0.0-rc1` follows from APP_VERSION being unable to carry a suffix, not from the updater. No video - a CI guard is not visually observable. The shell output below is the evidence instead. **Test case:** `ctest -R ReleaseTagVersionTest`, and the guard run directly: ``` $ CI/check-release-tag.sh 5.0.0 Mudlet-5.0.0 Release tag 'Mudlet-5.0.0' matches APP_VERSION '5.0.0'. exit=0 $ CI/check-release-tag.sh 5.0.0 Mudlet-5.0 error: release tag 'Mudlet-5.0' does not match APP_VERSION '5.0.0'. The tag has to be exactly 'Mudlet-5.0.0'. Publishing under a mismatched tag breaks auto-update, without saying so. [...] exit=1 ``` Replayed over every release tag since 4.18.5, each against the APP_VERSION at that tag - all accepted, so the guard blocks nothing Mudlet has actually shipped. Executing the real `Determine release type` step under GitHub's shell flags fails on `Mudlet-5.0` + `5.0.0` and on a PTB with APP_VERSION `5.0`, and passes on `Mudlet-5.0.0` + `5.0.0` and on a normal PTB. The updater trace was confirmed by compiling `Release.cpp` + `SemVer.cpp` and comparing: tag `Mudlet-5.0.0` gives `(4.22.0 < release) = true`, tag `Mudlet-5.0` gives `false`. Assisted-by: Claude:claude-opus-5
581 lines
26 KiB
YAML
581 lines
26 KiB
YAML
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 "<details>"
|
|
echo "<summary>Full changelog since last release (${STABLE_TAG})</summary>"
|
|
echo ""
|
|
echo "${FULL_CHANGELOG}"
|
|
echo ""
|
|
echo "</details>"
|
|
} > changelog.md
|
|
elif [[ -n "${FULL_CHANGELOG}" ]]; then
|
|
# No previous PTB or first PTB after a release - just show full changelog
|
|
echo "${FULL_CHANGELOG}" > changelog.md
|
|
else
|
|
echo "::warning::Changelog generation failed, using placeholder"
|
|
echo "See commit history for changes." > changelog.md
|
|
fi
|
|
fi
|
|
|
|
echo "Changelog preview:"
|
|
head -20 changelog.md
|
|
|
|
# Clean up old PTB releases, keeping only the latest 1 (+ the new one = 2 total)
|
|
- name: Clean up old PTB releases
|
|
if: steps.release-type.outputs.type == 'ptb'
|
|
env:
|
|
RELEASE_TAG: ${{ steps.release-type.outputs.tag }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
# Filter by -ptb in tag name to avoid deleting non-PTB prereleases (e.g. release candidates).
|
|
# Exclude the current tag so we never delete what we are about to publish.
|
|
gh release list --limit 100 --json tagName,isPrerelease,createdAt \
|
|
--jq '[.[] | select(.isPrerelease and (.tagName | contains("-ptb")))] | sort_by(.createdAt) | reverse | .[1:] | .[].tagName' | \
|
|
while IFS= read -r old_tag; do
|
|
if [[ "${old_tag}" == "${RELEASE_TAG}" ]]; then
|
|
continue
|
|
fi
|
|
echo "Cleaning up old PTB release: ${old_tag}"
|
|
gh release delete "${old_tag}" --yes --cleanup-tag 2>/dev/null || true
|
|
done
|
|
|
|
# Create the release if it doesn't exist yet, or upload any missing assets.
|
|
# Both build workflows trigger this job so it runs twice per commit - this
|
|
# avoids the old delete+recreate cycle that spammed notifications.
|
|
- name: Create or update GitHub Release
|
|
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 }}
|
|
RELEASE_TITLE: ${{ steps.release-type.outputs.title }}
|
|
TARGET_SHA: ${{ github.event.workflow_run.head_sha }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
# Everything in assets/ except the per-platform sidecars, which only feed
|
|
# SHA256SUMS.txt. Not a list of binary suffixes: a new asset type must not
|
|
# be able to reach the release without the checksum gate below seeing it.
|
|
mapfile -t BINARIES < <(find assets/ -type f ! -name '*.sha256' ! -name 'SHA256SUMS.txt')
|
|
|
|
if [[ ${#BINARIES[@]} -eq 0 ]]; then
|
|
echo "::error::No release files found to upload"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Files to upload:"
|
|
printf '%s\n' assets/SHA256SUMS.txt "${BINARIES[@]}"
|
|
|
|
# Guard against publishing a binary SHA256SUMS.txt does not cover: the
|
|
# release ends up holding what is already on it plus what we upload now
|
|
: > final-assets.txt
|
|
if gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' >> final-assets.txt 2> view-error.txt; then
|
|
RELEASE_EXISTS=yes
|
|
elif grep -qi 'release not found' view-error.txt; then
|
|
RELEASE_EXISTS=no
|
|
else
|
|
cat view-error.txt
|
|
echo "::error::Could not list the assets already published on ${RELEASE_TAG} - refusing to publish without checking checksum coverage"
|
|
exit 1
|
|
fi
|
|
basename -a -- "${BINARIES[@]}" >> final-assets.txt
|
|
|
|
echo "Assets the release will hold afterwards:"
|
|
sort -u final-assets.txt
|
|
bash release-scripts/CI/verify-release-checksums.sh assets/SHA256SUMS.txt final-assets.txt assets/
|
|
|
|
# SHA256SUMS.txt first: it is a superset of the old and new binaries, so if
|
|
# the upload dies partway the release is never left holding an uncovered one
|
|
if [[ "${RELEASE_EXISTS}" == "yes" ]]; then
|
|
echo "Release ${RELEASE_TAG} already exists - uploading any missing assets"
|
|
gh release upload "${RELEASE_TAG}" assets/SHA256SUMS.txt --clobber
|
|
gh release upload "${RELEASE_TAG}" "${BINARIES[@]}" --clobber
|
|
else
|
|
ARGS=(
|
|
"${RELEASE_TAG}"
|
|
--title "${RELEASE_TITLE}"
|
|
--notes-file changelog.md
|
|
)
|
|
|
|
if [[ "${RELEASE_TYPE}" == "ptb" ]]; then
|
|
ARGS+=(--prerelease --target "${TARGET_SHA}")
|
|
fi
|
|
|
|
gh release create "${ARGS[@]}" assets/SHA256SUMS.txt \
|
|
|| gh release upload "${RELEASE_TAG}" assets/SHA256SUMS.txt --clobber
|
|
gh release upload "${RELEASE_TAG}" "${BINARIES[@]}" --clobber
|
|
fi
|
|
|
|
# Confirm against the live release, not just the files we meant to upload
|
|
- name: Verify published release checksums
|
|
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: |
|
|
# A freshly uploaded asset is not always immediately readable, so retry
|
|
# rather than declaring a good release broken
|
|
for attempt in 1 2 3 4 5; do
|
|
rm -rf published-final/
|
|
mkdir -p published-final/
|
|
if gh release download "${RELEASE_TAG}" --pattern SHA256SUMS.txt --dir published-final/ \
|
|
&& gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' > published-final/assets.txt; then
|
|
break
|
|
fi
|
|
if [[ "${attempt}" -eq 5 ]]; then
|
|
echo "::error::Could not read back the published ${RELEASE_TAG} assets to verify them"
|
|
exit 1
|
|
fi
|
|
echo "Attempt ${attempt} could not read the published release yet - retrying"
|
|
sleep 10
|
|
done
|
|
|
|
echo "Published assets:"
|
|
cat published-final/assets.txt
|
|
bash release-scripts/CI/verify-release-checksums.sh published-final/SHA256SUMS.txt published-final/assets.txt
|
|
|
|
# Generate and upload Sparkle appcast XML for macOS updates
|
|
- name: Add SSH agent for appcast upload
|
|
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
|
|
uses: webfactory/ssh-agent@v0.10.0
|
|
with:
|
|
ssh-private-key: ${{ secrets.UPLOAD_PRIVATEKEY }}
|
|
|
|
- name: Generate Sparkle appcast XML
|
|
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 }}
|
|
RELEASE_VERSION: ${{ steps.release-type.outputs.version }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
mkdir -p appcast
|
|
|
|
# sparkle:version must match CFBundleVersion in the built app
|
|
export BUNDLE_VERSION="${RELEASE_TAG#Mudlet-}"
|
|
export RELEASE_VERSION
|
|
|
|
# Convert changelog to HTML for Sparkle's description field
|
|
export CHANGELOG_HTML=""
|
|
if [[ -f changelog.md ]]; then
|
|
CHANGELOG_HTML=$(pandoc changelog.md -f markdown -t html --no-highlight 2>/dev/null || true)
|
|
fi
|
|
if [[ -z "${CHANGELOG_HTML}" ]]; then
|
|
CHANGELOG_HTML="<p>See <a href='https://github.com/Mudlet/Mudlet/releases/tag/${RELEASE_TAG}'>release notes</a> for details.</p>"
|
|
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'
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
<channel>
|
|
<title>Mudlet Updates</title>
|
|
<description>Updates for Mudlet</description>
|
|
<language>en</language>
|
|
<item>
|
|
<title>Mudlet $RELEASE_VERSION</title>
|
|
<pubDate>$PUB_DATE</pubDate>
|
|
<sparkle:version>$BUNDLE_VERSION</sparkle:version>
|
|
<sparkle:shortVersionString>$RELEASE_VERSION</sparkle:shortVersionString>
|
|
<sparkle:minimumSystemVersion>11.0</sparkle:minimumSystemVersion>
|
|
<description><![CDATA[$CHANGELOG_HTML]]></description>
|
|
<enclosure
|
|
url="$DOWNLOAD_URL"
|
|
length="$FILE_SIZE"
|
|
type="application/octet-stream"/>
|
|
</item>
|
|
</channel>
|
|
</rss>
|
|
APPCAST_EOF
|
|
|
|
echo "Generated appcast/${CHANNEL}-${ARCH}.xml (version=${BUNDLE_VERSION}, size=${FILE_SIZE})"
|
|
done
|
|
|
|
echo "Generated appcast files:"
|
|
ls -la appcast/ 2>/dev/null || echo "No appcast files generated"
|
|
|
|
- name: Upload appcast to mudlet.org
|
|
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
|
|
env:
|
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
|
run: |
|
|
shopt -s nullglob
|
|
APPCAST_FILES=(appcast/*.xml)
|
|
shopt -u nullglob
|
|
|
|
if [[ ${#APPCAST_FILES[@]} -eq 0 ]]; then
|
|
echo "::warning::No appcast files to upload"
|
|
exit 0
|
|
fi
|
|
|
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
mudmachine@make.mudlet.org "mkdir -p \"${DEPLOY_PATH}/appcast\""
|
|
|
|
for f in "${APPCAST_FILES[@]}"; do
|
|
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
"$f" "mudmachine@make.mudlet.org:${DEPLOY_PATH}/appcast/"
|
|
echo "Uploaded $(basename "$f")"
|
|
done
|
|
|
|
- name: Verify appcast upload
|
|
if: steps.release-type.outputs.type == 'release' || steps.release-type.outputs.type == 'ptb'
|
|
run: |
|
|
shopt -s nullglob
|
|
APPCAST_FILES=(appcast/*.xml)
|
|
shopt -u nullglob
|
|
|
|
for f in "${APPCAST_FILES[@]}"; do
|
|
basename=$(basename "$f")
|
|
url="https://www.mudlet.org/wp-content/files/appcast/${basename}"
|
|
if curl --output /dev/null --silent --head --fail "$url"; then
|
|
echo "Verified: ${url}"
|
|
else
|
|
echo "::warning::Appcast not yet accessible at ${url}"
|
|
fi
|
|
done
|