Skip to content

Serialize shared setup draft transactions #480

Serialize shared setup draft transactions

Serialize shared setup draft transactions #480

Workflow file for this run

name: Rolling development build
on:
push:
branches: [main]
pull_request:
paths:
- scripts/build-windows.ps1
- scripts/build-posix.sh
- scripts/release-notes.js
- .github/workflows/beta.yml
workflow_dispatch:
permissions:
contents: write
pull-requests: read
concurrency:
group: rolling-development-build-${{ github.ref }}
cancel-in-progress: true
jobs:
windows:
runs-on: windows-2025
permissions:
contents: write
pull-requests: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci --omit=dev
- name: Name this development build
shell: bash
run: |
current=$(node -p "require('./package.json').version")
IFS=. read -r major minor patch <<<"$current"
echo "DEV_VERSION=${major}.${minor}.$((patch + 1))-dev+${GITHUB_SHA:0:7}" >> "$GITHUB_ENV"
- name: Build installer and portable ZIP
shell: pwsh
env:
# The installer title bar names the dev build, not package.json (#780).
NOWPLAYING_VERSION: ${{ env.DEV_VERSION }}
run: ./scripts/build-windows.ps1
- name: Give assets stable development names
shell: pwsh
run: |
Get-ChildItem dist/windows -File | Where-Object Extension -in '.exe','.zip' | ForEach-Object {
$name = $_.Name -replace '^nowplaying-v[0-9]+\.[0-9]+\.[0-9]+-', 'nowplaying-dev-'
if ($name -ne $_.Name) { Rename-Item $_.FullName $name }
}
- name: Attest development build provenance
if: ${{ github.event.repository.visibility == 'public' }}
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
with:
subject-path: |
dist/windows/*.exe
dist/windows/*.zip
- name: Upload Windows artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: windows
path: |
dist/windows/*.exe
dist/windows/*.zip
retention-days: 1
macos:
runs-on: macos-latest
permissions:
contents: write
pull-requests: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci --omit=dev
- name: Build macOS ZIP
shell: bash
run: ./scripts/build-posix.sh macos
- name: Attest development build provenance
if: ${{ github.event.repository.visibility == 'public' }}
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
with:
subject-path: dist/macos/*.zip
- name: Upload macOS artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: macos
path: dist/macos/*.zip
retention-days: 1
linux:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci --omit=dev
- name: Build Linux tarball
shell: bash
run: ./scripts/build-posix.sh linux
- name: Attest development build provenance
if: ${{ github.event.repository.visibility == 'public' }}
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
with:
subject-path: dist/linux/*.tar.gz
- name: Upload Linux artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: linux
path: dist/linux/*.tar.gz
retention-days: 1
# One moving pre-release: the "dev" tag and its release are updated in place
# on every push to main, so the releases page shows a single development
# build instead of one per merge. It is never marked Latest.
release:
needs: [windows, macos, linux]
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Download platform artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: '*'
path: dist
merge-multiple: true
- name: Combined checksums
shell: bash
run: |
cd dist
find . -maxdepth 1 -type f \( -name '*.exe' -o -name '*.zip' -o -name '*.tar.gz' \) -printf '%f\n' | sort | while IFS= read -r f; do
sha256sum "$f"
done > SHA256SUMS
cat SHA256SUMS
- name: Name this development build
shell: bash
run: |
current=$(node -p "require('./package.json').version")
IFS=. read -r major minor patch <<<"$current"
echo "DEV_VERSION=${major}.${minor}.$((patch + 1))-dev+${GITHUB_SHA:0:7}" >> "$GITHUB_ENV"
- name: Build cumulative development release notes
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPO_VISIBILITY: ${{ github.event.repository.visibility }}
run: |
set -euo pipefail
stable_tag=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName,isPrerelease,isDraft --jq 'select(.isPrerelease == false and .isDraft == false) | .tagName')
if [[ -z "$stable_tag" ]]; then
stable_tag=$(git rev-list --max-parents=0 HEAD | tail -1)
baseline_label="initial commit"
else
baseline_label="$stable_tag"
fi
git log --reverse --no-merges --pretty=format:'%H%x09%s' "$stable_tag..$GITHUB_SHA" > all-changes.tsv
# One line per commit for scripts/release-notes.js: sha, PR, size, subject.
: > changes.tsv
while IFS=$'\t' read -r sha subject || [[ -n "${sha:-}" ]]; do
[[ -z "${sha:-}" ]] && continue
# shellcheck disable=SC2016 # awk field references must remain literal.
size=$(git show --numstat --format= "$sha" | awk '{a+=$1; d+=$2; f++} END {print a+d+f+0}')
pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$sha/pulls" --jq '.[0].number // empty' 2>/dev/null | grep -m1 -E '^[0-9]+$' || true)
printf '%s\t%s\t%s\t%s\n' "$sha" "$pr" "$size" "$subject" >> changes.tsv
done < all-changes.tsv
node scripts/release-notes.js changes.tsv "$GITHUB_REPOSITORY" > ranked-changes.md
{
# shellcheck disable=SC2016 # Markdown backticks are intentional.
printf '**Dev build #%s** (`%s`), built from main at [`%s`](https://github.com/%s/commit/%s).\n\n' "$GITHUB_RUN_NUMBER" "$DEV_VERSION" "${GITHUB_SHA:0:7}" "$GITHUB_REPOSITORY" "$GITHUB_SHA"
printf 'This is a cumulative test build containing updates and fixes since **%s**. It may be unstable. For the latest supported release, use the version GitHub marks Latest. Assets are unsigned; verify SHA256SUMS before installing. To test it, follow the [dev build checklist](https://github.com/%s/blob/main/docs/testing-dev-build.md).\n\n' "$baseline_label" "$GITHUB_REPOSITORY"
if [[ "$REPO_VISIBILITY" == public ]]; then
# shellcheck disable=SC2016 # Markdown backticks are intentional.
printf 'Check where a download was built with `gh attestation verify <file> --repo %s`.\n\n' "$GITHUB_REPOSITORY"
fi
printf "## What's Changed\n\n"
cat ranked-changes.md
printf '\n**Full Changelog**: https://github.com/%s/compare/%s...%s\n' "$GITHUB_REPOSITORY" "$stable_tag" "$GITHUB_SHA"
} > development-release-notes.md
- name: Update the development pre-release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
title="nowplaying dev build #${GITHUB_RUN_NUMBER} (${DEV_VERSION})"
remote="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
mapfile -t files < <(find dist -maxdepth 1 -type f \( -name '*.exe' -o -name '*.zip' -o -name '*.tar.gz' -o -name SHA256SUMS \) | sort)
# Only a 404 means there is no dev release yet. Any other lookup error
# (rate limit, outage) used to fall through to "create", which moved the
# tag and then failed with "a release with the same tag name already
# exists", leaving the old assets under a moved tag. Retry, then stop.
lookup=failed
for attempt in 1 2 3; do
if gh api "repos/$GITHUB_REPOSITORY/releases/tags/dev" > dev-release.json 2> dev-release-error.txt; then lookup=found; break; fi
if grep -q "HTTP 404" dev-release-error.txt; then lookup=missing; break; fi
echo "Dev release lookup attempt $attempt failed: $(head -c 300 dev-release-error.txt)"
sleep $((attempt * 20))
done
[[ "$lookup" != failed ]] || { echo "Couldn't look up the dev release; nothing was changed"; exit 1; }
if [[ "$lookup" == missing ]]; then
git tag -f dev "$GITHUB_SHA"
git push --force "$remote" refs/tags/dev
gh release create dev --repo "$GITHUB_REPOSITORY" --prerelease --latest=false --verify-tag --title "$title" --notes-file development-release-notes.md "${files[@]}"
exit 0
fi
release_id=$(jq -r .id dev-release.json)
# Replace every asset explicitly instead of relying on --clobber, which
# can fail with a 404 on an asset it just looked up. A 404 on delete
# means the asset is already gone, which is the state we want.
for attempt in 1 2 3; do
gh api --paginate "repos/$GITHUB_REPOSITORY/releases/$release_id/assets" --jq '.[].id' > asset-ids.txt
while IFS= read -r id; do
[[ -n "$id" ]] && { gh api -X DELETE "repos/$GITHUB_REPOSITORY/releases/assets/$id" >/dev/null 2>&1 || true; }
done < asset-ids.txt
if gh release upload dev --repo "$GITHUB_REPOSITORY" "${files[@]}"; then uploaded=1; break; fi
echo "Upload attempt $attempt failed; retrying"
sleep $((attempt * 5))
done
[[ "${uploaded:-0}" == 1 ]] || { echo "Could not upload development assets"; exit 1; }
# Move the tag only once the new assets are on the release, so the tag,
# source archives and binaries always describe the same commit.
git tag -f dev "$GITHUB_SHA"
git push --force "$remote" refs/tags/dev
# Update the release by the ID looked up above.
gh api -X PATCH "repos/$GITHUB_REPOSITORY/releases/$release_id" -f name="$title" -F prerelease=true -f make_latest=false -F body=@development-release-notes.md --jq '.name'
# Old scheme: one immutable vX.Y.Z-dev.N pre-release per merge. Remove those
# (release and tag) only after the moving dev release is confirmed to carry
# this commit's assets. Stable releases never match the pattern.
- name: Remove old per-merge development pre-releases
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
target=$(gh release view dev --repo "$GITHUB_REPOSITORY" --json isPrerelease,assets,tagName --jq 'select(.isPrerelease) | [.assets[].name | select(endswith(".zip"))] | length')
[[ "${target:-0}" -ge 1 ]] || { echo "dev release not ready; skipping cleanup"; exit 0; }
[[ "$(git ls-remote --tags "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" refs/tags/dev | cut -f1)" == "$GITHUB_SHA" ]] || { echo "dev tag not at this commit; skipping cleanup"; exit 0; }
gh release list --repo "$GITHUB_REPOSITORY" --limit 500 --json tagName,isPrerelease --jq '.[] | select(.isPrerelease) | .tagName' > prereleases.txt
while IFS= read -r tag; do
if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+$ ]]; then
echo "Deleting $tag"
gh release delete "$tag" --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes
fi
done < prereleases.txt