mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
b83c0f5e50
When an existing app-v tag points to a different commit than source_ref, re-point it to the checked out source commit and continue publishing. This preserves Source code archive correctness without blocking artifact release.
459 lines
18 KiB
YAML
459 lines
18 KiB
YAML
name: Reusable Channel Publish
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
channel:
|
|
description: "Delivery channel name (release/next)"
|
|
required: true
|
|
type: string
|
|
source_ref:
|
|
description: "Git ref to build from"
|
|
required: true
|
|
type: string
|
|
target_branch:
|
|
description: "Branch that receives nix refresh PRs"
|
|
required: true
|
|
type: string
|
|
prerelease:
|
|
description: "Mark GitHub release as prerelease"
|
|
required: true
|
|
type: boolean
|
|
draft_release:
|
|
description: "Create release as draft"
|
|
required: true
|
|
type: boolean
|
|
verify_nix:
|
|
description: "Run verify-nix job"
|
|
required: false
|
|
default: true
|
|
type: boolean
|
|
|
|
jobs:
|
|
create-release:
|
|
permissions:
|
|
contents: write
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_id: ${{ steps.create-release.outputs.result }}
|
|
package_version: ${{ steps.get-version.outputs.version }}
|
|
release_tag: ${{ steps.tag.outputs.value }}
|
|
source_commit_sha: ${{ steps.source-sha.outputs.value }}
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
ref: ${{ inputs.source_ref }}
|
|
- name: setup node
|
|
uses: actions/setup-node@v5
|
|
with:
|
|
node-version: lts/*
|
|
cache: "npm"
|
|
- name: get version
|
|
id: get-version
|
|
run: |
|
|
set -euo pipefail
|
|
V="$(node -p 'require("./package.json").version')"
|
|
# Never publish with a missing/non-semver-ish version — empty becomes tag "app-v" on GitHub and breaks manifests.
|
|
if [ -z "$V" ]; then
|
|
echo "::error::package.json version is empty"
|
|
exit 1
|
|
fi
|
|
if ! printf '%s' "$V" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
|
echo "::error::package.json version must start with semver X.Y.Z... (got '$V')"
|
|
exit 1
|
|
fi
|
|
delim="PKGVER_${GITHUB_RUN_ID}_${GITHUB_RUN_ATTEMPT}_$(openssl rand -hex 8)"
|
|
{
|
|
echo "version<<$delim"
|
|
printf '%s\n' "$V"
|
|
echo "$delim"
|
|
} >> "$GITHUB_OUTPUT"
|
|
- name: compute release tag
|
|
id: tag
|
|
env:
|
|
VERSION: ${{ steps.get-version.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${VERSION:-}" ]; then
|
|
echo "::error::release tag would be 'app-v' (empty version output) — aborting"
|
|
exit 1
|
|
fi
|
|
TAG="app-v${VERSION}"
|
|
if [ "$TAG" = "app-v" ]; then
|
|
echo "::error::refuse to create invalid tag 'app-v'"
|
|
exit 1
|
|
fi
|
|
echo "value=$TAG" >> "$GITHUB_OUTPUT"
|
|
- name: capture source commit sha
|
|
id: source-sha
|
|
run: |
|
|
set -euo pipefail
|
|
echo "value=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
- name: align existing tag to source ref
|
|
env:
|
|
TAG: ${{ steps.tag.outputs.value }}
|
|
EXPECTED_SHA: ${{ steps.source-sha.outputs.value }}
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --force --tags origin
|
|
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
|
TAG_SHA="$(git rev-list -n 1 "refs/tags/$TAG")"
|
|
if [ "$TAG_SHA" != "$EXPECTED_SHA" ]; then
|
|
echo "::warning::Tag '$TAG' points to $TAG_SHA, expected $EXPECTED_SHA from source_ref '${{ inputs.source_ref }}'"
|
|
echo "::warning::Re-pointing tag '$TAG' to expected source commit to keep Source code archives aligned with built artifacts."
|
|
git tag -f "$TAG" "$EXPECTED_SHA"
|
|
git push --force origin "refs/tags/$TAG"
|
|
fi
|
|
fi
|
|
- name: extract changelog
|
|
id: changelog
|
|
run: |
|
|
VERSION="${{ steps.get-version.outputs.version }}"
|
|
BODY=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md)
|
|
if [ -z "$BODY" ]; then
|
|
BASE_VERSION="$(node -e 'const v=process.argv[1]; const m=v.match(/^(\d+\.\d+\.\d+)/); if(m){process.stdout.write(m[1]);}' "$VERSION")"
|
|
if [ -n "$BASE_VERSION" ] && [ "$BASE_VERSION" != "$VERSION" ]; then
|
|
BODY=$(awk "/^## \[$BASE_VERSION\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md)
|
|
fi
|
|
fi
|
|
EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
|
echo "body<<$EOF_MARKER" >> "$GITHUB_OUTPUT"
|
|
echo "$BODY" >> "$GITHUB_OUTPUT"
|
|
echo "$EOF_MARKER" >> "$GITHUB_OUTPUT"
|
|
- name: create or update release
|
|
id: create-release
|
|
uses: actions/github-script@v7
|
|
env:
|
|
PACKAGE_VERSION: ${{ steps.get-version.outputs.version }}
|
|
RELEASE_TAG: ${{ steps.tag.outputs.value }}
|
|
RELEASE_COMMIT_SHA: ${{ steps.source-sha.outputs.value }}
|
|
CHANGELOG_BODY: ${{ steps.changelog.outputs.body }}
|
|
IS_PRERELEASE: ${{ inputs.prerelease }}
|
|
IS_DRAFT: ${{ inputs.draft_release }}
|
|
with:
|
|
script: |
|
|
const tag = process.env.RELEASE_TAG;
|
|
const body = process.env.CHANGELOG_BODY || "See the assets to download this version and install.";
|
|
const prerelease = process.env.IS_PRERELEASE === "true";
|
|
const draft = process.env.IS_DRAFT === "true";
|
|
const version = process.env.PACKAGE_VERSION;
|
|
const targetCommitish = process.env.RELEASE_COMMIT_SHA;
|
|
const name = `Psysonic v${version}`;
|
|
try {
|
|
const { data } = await github.rest.repos.getReleaseByTag({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
tag,
|
|
});
|
|
await github.rest.repos.updateRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
release_id: data.id,
|
|
body,
|
|
name,
|
|
draft,
|
|
prerelease,
|
|
});
|
|
return data.id;
|
|
} catch (e) {
|
|
if (e.status !== 404) throw e;
|
|
}
|
|
const { data } = await github.rest.repos.createRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
tag_name: tag,
|
|
target_commitish: targetCommitish,
|
|
name,
|
|
body,
|
|
draft,
|
|
prerelease,
|
|
});
|
|
return data.id;
|
|
|
|
build-macos-windows:
|
|
needs: create-release
|
|
permissions:
|
|
contents: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
settings:
|
|
- platform: "macos-latest"
|
|
args: "--target aarch64-apple-darwin"
|
|
- platform: "macos-latest"
|
|
args: "--target x86_64-apple-darwin"
|
|
- platform: "windows-latest"
|
|
args: "--bundles nsis"
|
|
runs-on: ${{ matrix.settings.platform }}
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
ref: ${{ inputs.source_ref }}
|
|
- name: setup node
|
|
uses: actions/setup-node@v5
|
|
with:
|
|
node-version: lts/*
|
|
cache: "npm"
|
|
- name: install Rust stable
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
- name: cache cargo
|
|
uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: src-tauri
|
|
- name: install npm dependencies
|
|
run: npm install
|
|
- name: write Apple API key (macOS only)
|
|
if: runner.os == 'macOS'
|
|
run: |
|
|
mkdir -p ~/private_keys
|
|
echo "${{ secrets.APPLE_API_KEY_B64 }}" | base64 --decode > ~/private_keys/AuthKey.p8
|
|
echo "APPLE_API_KEY_PATH=$HOME/private_keys/AuthKey.p8" >> "$GITHUB_ENV"
|
|
- uses: tauri-apps/tauri-action@v0
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }}
|
|
VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }}
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
with:
|
|
releaseId: ${{ needs.create-release.outputs.release_id }}
|
|
args: ${{ matrix.settings.args }}
|
|
- name: re-sign updater bundle + upload .sig (macOS only)
|
|
if: runner.os == 'macOS'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
set -e
|
|
RELEASE_TAG="${{ needs.create-release.outputs.release_tag }}"
|
|
TARGET_ARG='${{ matrix.settings.args }}'
|
|
if echo "$TARGET_ARG" | grep -q 'aarch64'; then
|
|
TARGET="aarch64-apple-darwin"
|
|
ARCH="aarch64"
|
|
else
|
|
TARGET="x86_64-apple-darwin"
|
|
ARCH="x64"
|
|
fi
|
|
TARBALL="src-tauri/target/${TARGET}/release/bundle/macos/Psysonic.app.tar.gz"
|
|
if [ ! -f "$TARBALL" ]; then
|
|
echo "::error::Expected tarball missing: $TARBALL"
|
|
ls -la "$(dirname "$TARBALL")" || true
|
|
exit 1
|
|
fi
|
|
npx @tauri-apps/cli signer sign "$TARBALL"
|
|
cp "${TARBALL}.sig" "Psysonic_${ARCH}.app.tar.gz.sig"
|
|
gh release upload "$RELEASE_TAG" "Psysonic_${ARCH}.app.tar.gz.sig" --clobber
|
|
|
|
generate-manifest:
|
|
needs: [create-release, build-macos-windows]
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
ref: ${{ inputs.source_ref }}
|
|
- name: generate latest.json
|
|
env:
|
|
VERSION: ${{ needs.create-release.outputs.package_version }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: node scripts/generate-update-manifest.js
|
|
- name: upload latest.json to release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
RELEASE_TAG="${{ needs.create-release.outputs.release_tag }}"
|
|
gh release upload "$RELEASE_TAG" latest.json --clobber
|
|
|
|
build-linux:
|
|
needs: create-release
|
|
permissions:
|
|
contents: write
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
ref: ${{ inputs.source_ref }}
|
|
- name: install dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y \
|
|
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf \
|
|
libasound2-dev squashfs-tools cmake
|
|
- name: setup node
|
|
uses: actions/setup-node@v5
|
|
with:
|
|
node-version: lts/*
|
|
cache: "npm"
|
|
- name: install Rust stable
|
|
uses: dtolnay/rust-toolchain@stable
|
|
- name: cache cargo
|
|
uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: src-tauri
|
|
- name: install npm dependencies
|
|
run: npm install
|
|
- name: build
|
|
env:
|
|
VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }}
|
|
VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }}
|
|
APPIMAGE_EXTRACT_AND_RUN: 1
|
|
run: npm run tauri:build -- --bundles deb,rpm,appimage
|
|
- name: upload Linux artifacts
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
RELEASE_TAG="${{ needs.create-release.outputs.release_tag }}"
|
|
find src-tauri/target/release/bundle \
|
|
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) \
|
|
| xargs gh release upload "$RELEASE_TAG" --clobber
|
|
|
|
verify-nix:
|
|
if: ${{ inputs.verify_nix }}
|
|
needs: create-release
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ inputs.target_branch }}
|
|
- name: install Nix
|
|
uses: DeterminateSystems/nix-installer-action@v15
|
|
- name: configure Cachix (managed signing)
|
|
uses: cachix/cachix-action@v15
|
|
with:
|
|
name: psysonic
|
|
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
|
- name: compute npmDepsHash from package-lock.json
|
|
id: npm-hash
|
|
run: |
|
|
set -euo pipefail
|
|
HASH="$(nix run nixpkgs/nixos-unstable#prefetch-npm-deps -- package-lock.json)"
|
|
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
|
|
echo "Computed npmDepsHash: $HASH"
|
|
- name: write npmDepsHash into nix/upstream-sources.json
|
|
run: |
|
|
set -euo pipefail
|
|
HASH='${{ steps.npm-hash.outputs.hash }}'
|
|
jq --arg h "$HASH" '.npmDepsHash = $h' nix/upstream-sources.json > nix/upstream-sources.json.new
|
|
mv nix/upstream-sources.json.new nix/upstream-sources.json
|
|
- name: refresh flake.lock (nixpkgs pin)
|
|
run: nix flake update --accept-flake-config
|
|
- name: verify nix build + push to Cachix
|
|
run: |
|
|
set -euo pipefail
|
|
nix build .#psysonic --accept-flake-config --no-link --print-build-logs
|
|
nix path-info --recursive .#psysonic | cachix push psysonic
|
|
- name: open + auto-merge PR with refreshed lock and hash (if changed)
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git add flake.lock nix/upstream-sources.json
|
|
if git diff --cached --quiet; then
|
|
echo "flake.lock / nix/upstream-sources.json unchanged — nothing to commit."
|
|
exit 0
|
|
fi
|
|
VERSION="${{ needs.create-release.outputs.package_version }}"
|
|
BRANCH="chore/nix-lock-refresh-${{ inputs.target_branch }}-v${VERSION}"
|
|
git checkout -b "$BRANCH"
|
|
git commit -m "chore(nix): refresh lock + npmDepsHash for v${VERSION}"
|
|
git push origin "$BRANCH"
|
|
gh pr create \
|
|
--base "${{ inputs.target_branch }}" \
|
|
--head "$BRANCH" \
|
|
--title "chore(nix): refresh lock + npmDepsHash for v${VERSION}" \
|
|
--body "Auto-generated for the \`${{ inputs.channel }}\` channel after v${VERSION}: refreshes \`flake.lock\` and \`nix/upstream-sources.json\`."
|
|
|
|
bump-main-to-next-dev:
|
|
if: ${{ inputs.channel == 'release' }}
|
|
needs: create-release
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
ref: main
|
|
- name: setup node
|
|
uses: actions/setup-node@v5
|
|
with:
|
|
node-version: lts/*
|
|
- name: compute next dev version
|
|
id: next-dev
|
|
env:
|
|
RELEASE_VERSION: ${{ needs.create-release.outputs.package_version }}
|
|
run: |
|
|
set -euo pipefail
|
|
NEXT_DEV="$(node -e 'const v=process.env.RELEASE_VERSION; const m=v.match(/^(\d+)\.(\d+)\.(\d+)/); if(!m){throw new Error(`Invalid release version: ${v}`)}; const major=Number(m[1]); const minor=Number(m[2]) + 1; process.stdout.write(`${major}.${minor}.0-dev`)')"
|
|
echo "value=$NEXT_DEV" >> "$GITHUB_OUTPUT"
|
|
- name: bump package version in main
|
|
run: |
|
|
set -euo pipefail
|
|
TARGET_VERSION="${{ steps.next-dev.outputs.value }}"
|
|
CURRENT_VERSION="$(node -p 'require("./package.json").version')"
|
|
SHOULD_BUMP="$(node -e '
|
|
const parse = (v) => {
|
|
const m = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
if (!m) throw new Error(`Invalid semver: ${v}`);
|
|
const pre = m[4] ?? "";
|
|
const preRank = pre === "" ? 3 : pre.startsWith("rc.") ? 2 : pre === "dev" ? 1 : 0;
|
|
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), preRank };
|
|
};
|
|
const a = parse(process.argv[1]); // current
|
|
const b = parse(process.argv[2]); // target
|
|
const keys = ["major", "minor", "patch", "preRank"];
|
|
for (const k of keys) {
|
|
if (b[k] > a[k]) { process.stdout.write("true"); process.exit(0); }
|
|
if (b[k] < a[k]) { process.stdout.write("false"); process.exit(0); }
|
|
}
|
|
process.stdout.write("false");
|
|
' "$CURRENT_VERSION" "$TARGET_VERSION")"
|
|
if [[ "$SHOULD_BUMP" != "true" ]]; then
|
|
echo "main already at ${CURRENT_VERSION} (target ${TARGET_VERSION} is not newer)"
|
|
exit 0
|
|
fi
|
|
npm version --no-git-tag-version "$TARGET_VERSION"
|
|
- name: sync Tauri version from package.json
|
|
run: node scripts/sync-tauri-version-from-package.js
|
|
- name: open PR with dev bump (if changed)
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git add package.json package-lock.json src-tauri/Cargo.toml src-tauri/tauri.conf.json
|
|
if git diff --cached --quiet; then
|
|
echo "No dev version bump required."
|
|
exit 0
|
|
fi
|
|
VERSION="${{ steps.next-dev.outputs.value }}"
|
|
SAFE_VERSION="${VERSION//./-}"
|
|
SAFE_VERSION="${SAFE_VERSION//\//-}"
|
|
BRANCH="chore/version-bump-main-${SAFE_VERSION}"
|
|
git checkout -b "$BRANCH"
|
|
git commit -m "chore(release): bump main to ${VERSION}"
|
|
git push origin "$BRANCH"
|
|
gh pr create \
|
|
--base main \
|
|
--head "$BRANCH" \
|
|
--title "chore(release): bump main to ${VERSION}" \
|
|
--body "Auto-generated after stable release: updates \`package.json\`, \`package-lock.json\`, \`src-tauri/Cargo.toml\`, and \`src-tauri/tauri.conf.json\` to the next development version."
|