mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-10-18 23:33:05 +00:00
feat(ci): automate stable releases and Linux repositories
This commit is contained in:
parent
07797c14a8
commit
1c07729e88
7 changed files with 1094 additions and 0 deletions
481
.github/workflows/stable-release.yml
vendored
Normal file
481
.github/workflows/stable-release.yml
vendored
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
name: Stable Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Existing stable release tag, for example v2.2.0
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: stable-release-${{ github.event.release.tag_name || inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: ${{ steps.release.outputs.tag }}
|
||||
version: ${{ steps.release.outputs.version }}
|
||||
commit: ${{ steps.release.outputs.commit }}
|
||||
steps:
|
||||
- name: Resolve release tag
|
||||
id: tag
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag=${EVENT_TAG:-$INPUT_TAG}
|
||||
test -n "$tag"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.tag.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate stable release identity
|
||||
id: release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_json=$(gh release view "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--json isDraft,isPrerelease,tagName)
|
||||
test "$(jq -r '.isDraft' <<< "$release_json")" = false
|
||||
test "$(jq -r '.isPrerelease' <<< "$release_json")" = false
|
||||
test "$(jq -r '.tagName' <<< "$release_json")" = "$RELEASE_TAG"
|
||||
|
||||
version=$(node scripts/ci/verify-release-version.mjs "$RELEASE_TAG")
|
||||
commit=$(git rev-parse HEAD)
|
||||
{
|
||||
echo "tag=$RELEASE_TAG"
|
||||
echo "version=$version"
|
||||
echo "commit=$commit"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
desktop:
|
||||
needs: prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: windows-latest
|
||||
target_args: ""
|
||||
portable: true
|
||||
portable_name: windows-x64
|
||||
- platform: macos-latest
|
||||
target_args: --target x86_64-apple-darwin
|
||||
portable: false
|
||||
- platform: macos-latest
|
||||
target_args: --target aarch64-apple-darwin
|
||||
portable: false
|
||||
- platform: ubuntu-22.04
|
||||
target_args: --bundles appimage,deb,rpm
|
||||
portable: true
|
||||
portable_name: linux-x64
|
||||
- platform: ubuntu-22.04-arm
|
||||
target_args: --bundles deb,rpm
|
||||
portable: true
|
||||
portable_name: linux-arm64
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
TAURITAVERN_BUILD_BRANCH: main
|
||||
TAURITAVERN_BUILD_REVISION: ${{ needs.prepare.outputs.commit }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: startsWith(matrix.platform, 'ubuntu-22.04')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libwebkit2gtk-4.1-dev librsvg2-dev patchelf rpm \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-alsa gstreamer1.0-pulseaudio
|
||||
if apt-cache show libayatana-appindicator3-dev >/dev/null 2>&1; then
|
||||
sudo apt-get install -y libayatana-appindicator3-dev
|
||||
else
|
||||
sudo apt-get install -y libappindicator3-dev
|
||||
fi
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup macOS signing and notarization
|
||||
if: matrix.platform == 'macos-latest'
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||
run: ./scripts/ci/setup-macos-signing.sh
|
||||
|
||||
- name: Build desktop bundles
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_API_ISSUER: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_API_ISSUER || '' }}
|
||||
APPLE_API_KEY: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_API_KEY || '' }}
|
||||
with:
|
||||
projectPath: src-tauri/crates/tauritavern
|
||||
tauriScript: node ../../../scripts/tauri-app.mjs
|
||||
args: ${{ matrix.target_args }}
|
||||
uploadWorkflowArtifacts: true
|
||||
uploadUpdaterJson: false
|
||||
uploadUpdaterSignatures: false
|
||||
workflowArtifactNamePattern: 'TauriTavern-${{ needs.prepare.outputs.version }}-[platform]-[arch]-[bundle]'
|
||||
|
||||
- name: Build portable binary
|
||||
if: matrix.portable
|
||||
run: node scripts/build-portable.mjs --skip-web-build --output-dir portable
|
||||
|
||||
- name: Upload portable binary
|
||||
if: matrix.portable
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: TauriTavern-${{ needs.prepare.outputs.version }}-${{ matrix.portable_name }}-portable
|
||||
path: portable/*
|
||||
if-no-files-found: error
|
||||
|
||||
mobile:
|
||||
needs: prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- mobile: android
|
||||
platform: ubuntu-latest
|
||||
args: --verbose --apk --split-per-abi --target aarch64 armv7
|
||||
- mobile: ios
|
||||
platform: macos-latest
|
||||
args: --verbose
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
TAURITAVERN_BUILD_BRANCH: main
|
||||
TAURITAVERN_BUILD_REVISION: ${{ needs.prepare.outputs.commit }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- name: Install Android host dependencies
|
||||
if: matrix.mobile == 'android'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev \
|
||||
libayatana-appindicator3-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.mobile == 'android' && 'aarch64-linux-android,armv7-linux-androideabi' || 'aarch64-apple-ios' }}
|
||||
|
||||
- name: Setup JDK
|
||||
if: matrix.mobile == 'android'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: temurin
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src-tauri/crates/tauritavern/gen/android/**/*.gradle*
|
||||
src-tauri/crates/tauritavern/gen/android/**/gradle-wrapper.properties
|
||||
|
||||
- name: Setup Android SDK
|
||||
if: matrix.mobile == 'android'
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Setup Android NDK
|
||||
if: matrix.mobile == 'android'
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r29
|
||||
link-to-sdk: true
|
||||
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "src-tauri -> target"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Android signing
|
||||
if: matrix.mobile == 'android'
|
||||
shell: bash
|
||||
run: |
|
||||
cd src-tauri/crates/tauritavern/gen/android
|
||||
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties
|
||||
echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties
|
||||
base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > "$RUNNER_TEMP/keystore.jks"
|
||||
echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties
|
||||
|
||||
- name: Setup App Store Connect API key
|
||||
if: matrix.mobile == 'ios'
|
||||
shell: bash
|
||||
env:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||
run: |
|
||||
: "${APPLE_API_ISSUER:?Missing APPLE_API_ISSUER secret}"
|
||||
: "${APPLE_API_KEY:?Missing APPLE_API_KEY secret}"
|
||||
: "${APPLE_API_KEY_P8:?Missing APPLE_API_KEY_P8 secret}"
|
||||
key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8"
|
||||
umask 077
|
||||
printf '%s' "$APPLE_API_KEY_P8" > "$key_path"
|
||||
echo "APPLE_API_KEY_PATH=$key_path" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build mobile bundle
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NDK_HOME: ${{ matrix.mobile == 'android' && steps.setup-ndk.outputs.ndk-path || '' }}
|
||||
APPLE_API_ISSUER: ${{ matrix.mobile == 'ios' && secrets.APPLE_API_ISSUER || '' }}
|
||||
APPLE_API_KEY: ${{ matrix.mobile == 'ios' && secrets.APPLE_API_KEY || '' }}
|
||||
with:
|
||||
projectPath: src-tauri/crates/tauritavern
|
||||
tauriScript: node ../../../scripts/tauri-app.mjs
|
||||
mobile: ${{ matrix.mobile }}
|
||||
args: ${{ matrix.args }}
|
||||
uploadWorkflowArtifacts: true
|
||||
uploadPlainBinary: true
|
||||
uploadUpdaterJson: false
|
||||
uploadUpdaterSignatures: false
|
||||
workflowArtifactNamePattern: 'TauriTavern-${{ needs.prepare.outputs.version }}-[platform]-[arch]-[bundle]'
|
||||
|
||||
publish-release:
|
||||
needs: [prepare, desktop, mobile]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
env:
|
||||
ASSET_PREFIX: TauriTavern-${{ needs.prepare.outputs.version }}-
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: 'TauriTavern-${{ needs.prepare.outputs.version }}-*'
|
||||
path: dist
|
||||
|
||||
- name: Validate and collect release assets
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir release-assets
|
||||
for artifact in dist/"${ASSET_PREFIX}"*; do
|
||||
base=$(basename "$artifact")
|
||||
[[ "$base" == *-app ]] && continue
|
||||
mapfile -t files < <(find "$artifact" -type f)
|
||||
test "${#files[@]}" -eq 1 || {
|
||||
echo "Expected one file in $artifact, found ${#files[@]}" >&2
|
||||
exit 1
|
||||
}
|
||||
case "${files[0]}" in
|
||||
*.AppImage) ext=.AppImage ;;
|
||||
*.dmg) ext=.dmg ;;
|
||||
*.msi) ext=.msi ;;
|
||||
*.exe) ext=.exe ;;
|
||||
*.deb) ext=.deb ;;
|
||||
*.rpm) ext=.rpm ;;
|
||||
*.apk) ext=.apk ;;
|
||||
*.aab) ext=.aab ;;
|
||||
*.ipa) ext=.ipa ;;
|
||||
*)
|
||||
[[ "$base" == *-portable ]] || {
|
||||
echo "Unknown stable artifact type: ${files[0]}" >&2
|
||||
exit 1
|
||||
}
|
||||
ext=.bin
|
||||
;;
|
||||
esac
|
||||
cp "${files[0]}" "release-assets/${base}${ext}"
|
||||
done
|
||||
mapfile -t assets < <(find release-assets -type f -name "${ASSET_PREFIX}*" | sort)
|
||||
printf '%s\n' "${assets[@]}"
|
||||
test "${#assets[@]}" -eq 15 || {
|
||||
echo "Expected 15 stable assets, found ${#assets[@]}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Upload assets without changing release notes
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
find release-assets -type f -name "${ASSET_PREFIX}*" -print0 \
|
||||
| xargs -0 gh release upload "$RELEASE_TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--clobber
|
||||
|
||||
publish-package-repositories:
|
||||
needs: [prepare, publish-release]
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_EC2_METADATA_DISABLED: "true"
|
||||
R2_ENDPOINT: https://feb4df7b9167cb2a5c75b327584225b5.r2.cloudflarestorage.com
|
||||
PACKAGES_R2_BUCKET: tauritavern-packages
|
||||
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
|
||||
EXPECTED_RPM_RELEASE: "1"
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
|
||||
RELEASE_COMMIT: ${{ needs.prepare.outputs.commit }}
|
||||
LINUX_REPOSITORY_SIGNING_FPR: D609D0B174E0073BB3980A1BEDC6CEF924B6C529
|
||||
RELEASE_ASSETS_DIR: dist
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: 'TauriTavern-${{ needs.prepare.outputs.version }}-*'
|
||||
path: dist
|
||||
|
||||
- name: Install repository tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
apt-utils awscli createrepo-c gnupg2 jq rpm
|
||||
|
||||
- name: Import repository signing subkey
|
||||
id: signing
|
||||
shell: bash
|
||||
env:
|
||||
GPG_PRIVATE_KEY_BASE64: ${{ secrets.LINUX_REPOSITORY_GPG_PRIVATE_KEY_BASE64 }}
|
||||
GPG_PASSPHRASE: ${{ secrets.LINUX_REPOSITORY_GPG_PASSPHRASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: "${GPG_PRIVATE_KEY_BASE64:?Missing LINUX_REPOSITORY_GPG_PRIVATE_KEY_BASE64 secret}"
|
||||
key_file="$RUNNER_TEMP/tauritavern-repository-private-key.asc"
|
||||
passphrase_file="$RUNNER_TEMP/tauritavern-repository-passphrase"
|
||||
umask 077
|
||||
printf '%s' "$GPG_PRIVATE_KEY_BASE64" | base64 --decode > "$key_file"
|
||||
printf '%s' "$GPG_PASSPHRASE" > "$passphrase_file"
|
||||
gpg --batch --import "$key_file"
|
||||
gpg --batch --with-colons --with-subkey-fingerprint \
|
||||
--list-secret-keys "$LINUX_REPOSITORY_SIGNING_FPR" \
|
||||
| awk -F: -v expected="$LINUX_REPOSITORY_SIGNING_FPR" \
|
||||
'$1 == "fpr" && $10 == expected { found = 1 } END { exit !found }'
|
||||
echo "passphrase_file=$passphrase_file" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish signed APT and RPM repositories
|
||||
env:
|
||||
LINUX_REPOSITORY_GPG_PASSPHRASE_FILE: ${{ steps.signing.outputs.passphrase_file }}
|
||||
run: ./scripts/ci/publish-linux-repositories.sh
|
||||
|
||||
- name: Verify public repository pointers
|
||||
run: |
|
||||
curl -fsSL https://packages.tauritavern.com/apt/dists/stable/InRelease \
|
||||
| gpg --batch --verify
|
||||
curl -fsSL \
|
||||
https://packages.tauritavern.com/rpm/fedora/stable/x86_64/repodata/repomd.xml \
|
||||
>/dev/null
|
||||
curl -fsSL \
|
||||
https://packages.tauritavern.com/rpm/opensuse/16.0/x86_64/repodata/repomd.xml \
|
||||
>/dev/null
|
||||
|
||||
publish-nix-cache:
|
||||
needs: [prepare, publish-release]
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- system: x86_64-linux
|
||||
runner: ubuntu-22.04
|
||||
- system: aarch64-linux
|
||||
runner: ubuntu-22.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_EC2_METADATA_DISABLED: "true"
|
||||
R2_ENDPOINT: https://feb4df7b9167cb2a5c75b327584225b5.r2.cloudflarestorage.com
|
||||
NIX_R2_BUCKET: tauritavern-nix-cache
|
||||
NIX_CACHE_URL: https://nix-cache.tauritavern.com
|
||||
NIX_CACHE_PUBLIC_KEY: nix-cache.tauritavern.com-1:mOl/sCsfndubNIhnLODjA7GPqk1qw5iknbayZLRn92U=
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Build release flake
|
||||
run: >-
|
||||
nix
|
||||
--accept-flake-config
|
||||
build
|
||||
.#tauritavern
|
||||
--print-build-logs
|
||||
|
||||
- name: Sign and publish runtime and dependency paths
|
||||
env:
|
||||
NIX_CACHE_PRIVATE_KEY_BASE64: ${{ secrets.NIX_CACHE_PRIVATE_KEY_BASE64 }}
|
||||
run: |
|
||||
cargo_deps=$(nix eval --raw \
|
||||
".#packages.${{ matrix.system }}.tauritavern.cargoDeps.outPath")
|
||||
pnpm_deps=$(nix eval --raw \
|
||||
".#packages.${{ matrix.system }}.tauritavern.pnpmDeps.outPath")
|
||||
./scripts/ci/publish-nix-cache.sh "$cargo_deps" "$pnpm_deps"
|
||||
|
||||
- name: Verify public Nix cache
|
||||
run: curl -fsSL https://nix-cache.tauritavern.com/nix-cache-info
|
||||
330
scripts/ci/publish-linux-repositories.sh
Executable file
330
scripts/ci/publish-linux-repositories.sh
Executable file
|
|
@ -0,0 +1,330 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${RELEASE_ASSETS_DIR:?Missing RELEASE_ASSETS_DIR}"
|
||||
: "${RELEASE_VERSION:?Missing RELEASE_VERSION}"
|
||||
: "${RELEASE_TAG:?Missing RELEASE_TAG}"
|
||||
: "${RELEASE_COMMIT:?Missing RELEASE_COMMIT}"
|
||||
: "${R2_ENDPOINT:?Missing R2_ENDPOINT}"
|
||||
: "${PACKAGES_R2_BUCKET:?Missing PACKAGES_R2_BUCKET}"
|
||||
: "${LINUX_REPOSITORY_SIGNING_FPR:?Missing LINUX_REPOSITORY_SIGNING_FPR}"
|
||||
: "${LINUX_REPOSITORY_GPG_PASSPHRASE_FILE:?Missing LINUX_REPOSITORY_GPG_PASSPHRASE_FILE}"
|
||||
|
||||
REPOSITORY_CHANNEL=${REPOSITORY_CHANNEL:-stable}
|
||||
EXPECTED_DEB_VERSION=${EXPECTED_DEB_VERSION:-$RELEASE_VERSION}
|
||||
EXPECTED_RPM_RELEASE=${EXPECTED_RPM_RELEASE:-}
|
||||
|
||||
case "$REPOSITORY_CHANNEL" in
|
||||
stable)
|
||||
apt_pool_path=pool/main/t/tauri-tavern
|
||||
manifest_name=repository-manifest.json
|
||||
rpm_repository_paths=(
|
||||
rpm/fedora/stable/x86_64
|
||||
rpm/fedora/stable/aarch64
|
||||
rpm/opensuse/16.0/x86_64
|
||||
rpm/opensuse/16.0/aarch64
|
||||
)
|
||||
;;
|
||||
canary)
|
||||
apt_pool_path=pool/canary/main/t/tauri-tavern
|
||||
manifest_name=repository-manifest-canary.json
|
||||
rpm_repository_paths=(
|
||||
rpm/fedora/canary/x86_64
|
||||
rpm/fedora/canary/aarch64
|
||||
rpm/opensuse/16.0/canary/x86_64
|
||||
rpm/opensuse/16.0/canary/aarch64
|
||||
)
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported repository channel: $REPOSITORY_CHANNEL" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
for required_command in \
|
||||
apt-ftparchive aws createrepo_c dpkg-deb gpg gzip jq rpm rpmsign; do
|
||||
command -v "$required_command" >/dev/null 2>&1 || {
|
||||
echo "Missing required command: $required_command" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
repository_dir=$(mktemp -d "${RUNNER_TEMP:-/tmp}/tauritavern-repository.XXXXXX")
|
||||
trap 'rm -rf "$repository_dir"' EXIT
|
||||
|
||||
aws_r2() {
|
||||
aws --endpoint-url "$R2_ENDPOINT" s3 "$@"
|
||||
}
|
||||
|
||||
sync_existing_packages() {
|
||||
mkdir -p "$repository_dir/apt/$apt_pool_path"
|
||||
aws_r2 sync \
|
||||
"s3://$PACKAGES_R2_BUCKET/apt/$apt_pool_path" \
|
||||
"$repository_dir/apt/$apt_pool_path"
|
||||
|
||||
for repository_path in "${rpm_repository_paths[@]}"; do
|
||||
mkdir -p "$repository_dir/$repository_path/packages"
|
||||
aws_r2 sync \
|
||||
"s3://$PACKAGES_R2_BUCKET/$repository_path/packages" \
|
||||
"$repository_dir/$repository_path/packages"
|
||||
done
|
||||
}
|
||||
|
||||
stage_deb_packages() {
|
||||
mapfile -d '' deb_assets < <(find "$RELEASE_ASSETS_DIR" -type f -name '*.deb' -print0)
|
||||
test "${#deb_assets[@]}" -eq 2 || {
|
||||
echo "Expected two DEB assets, found ${#deb_assets[@]}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for asset in "${deb_assets[@]}"; do
|
||||
package=$(dpkg-deb -f "$asset" Package)
|
||||
version=$(dpkg-deb -f "$asset" Version)
|
||||
architecture=$(dpkg-deb -f "$asset" Architecture)
|
||||
test "$package" = "tauri-tavern" || {
|
||||
echo "Unexpected DEB package name in $asset: $package" >&2
|
||||
exit 1
|
||||
}
|
||||
test "$version" = "$EXPECTED_DEB_VERSION" || {
|
||||
echo "Unexpected DEB version in $asset: $version" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$architecture" in
|
||||
amd64 | arm64) ;;
|
||||
*)
|
||||
echo "Unexpected DEB architecture in $asset: $architecture" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
install -m 0644 "$asset" \
|
||||
"$repository_dir/apt/$apt_pool_path/tauri-tavern_${version}_${architecture}.deb"
|
||||
done
|
||||
}
|
||||
|
||||
stage_rpm_packages() {
|
||||
mapfile -d '' rpm_assets < <(find "$RELEASE_ASSETS_DIR" -type f -name '*.rpm' -print0)
|
||||
test "${#rpm_assets[@]}" -eq 2 || {
|
||||
echo "Expected two RPM assets, found ${#rpm_assets[@]}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rpm_signature_db="$repository_dir/rpm-signature-db"
|
||||
signing_public_key="$repository_dir/repository-signing-public-key.asc"
|
||||
mkdir -p "$rpm_signature_db"
|
||||
gpg --batch --armor --export "$LINUX_REPOSITORY_SIGNING_FPR" >"$signing_public_key"
|
||||
rpm --dbpath "$rpm_signature_db" --initdb
|
||||
rpm --dbpath "$rpm_signature_db" --import "$signing_public_key"
|
||||
|
||||
for asset in "${rpm_assets[@]}"; do
|
||||
package=$(rpm --dbpath "$rpm_signature_db" -qp --queryformat '%{NAME}' "$asset")
|
||||
version=$(rpm --dbpath "$rpm_signature_db" -qp --queryformat '%{VERSION}' "$asset")
|
||||
release=$(rpm --dbpath "$rpm_signature_db" -qp --queryformat '%{RELEASE}' "$asset")
|
||||
architecture=$(rpm --dbpath "$rpm_signature_db" -qp --queryformat '%{ARCH}' "$asset")
|
||||
test "$package" = "tauri-tavern" || {
|
||||
echo "Unexpected RPM package name in $asset: $package" >&2
|
||||
exit 1
|
||||
}
|
||||
test "$version" = "$RELEASE_VERSION" || {
|
||||
echo "Unexpected RPM version in $asset: $version" >&2
|
||||
exit 1
|
||||
}
|
||||
if [ -n "$EXPECTED_RPM_RELEASE" ] && [ "$release" != "$EXPECTED_RPM_RELEASE" ]; then
|
||||
echo "Unexpected RPM release in $asset: $release" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$architecture" in
|
||||
x86_64 | aarch64) ;;
|
||||
*)
|
||||
echo "Unexpected RPM architecture in $asset: $architecture" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
signed_rpm="$repository_dir/tauri-tavern-${version}-${release}.${architecture}.rpm"
|
||||
install -m 0644 "$asset" "$signed_rpm"
|
||||
rpmsign \
|
||||
--dbpath "$rpm_signature_db" \
|
||||
--define "_gpg_name ${LINUX_REPOSITORY_SIGNING_FPR}!" \
|
||||
--define "_gpg_sign_cmd_extra_args --batch --pinentry-mode loopback --passphrase-file $LINUX_REPOSITORY_GPG_PASSPHRASE_FILE" \
|
||||
--addsign \
|
||||
"$signed_rpm"
|
||||
signature_check=$(rpm --dbpath "$rpm_signature_db" --checksig "$signed_rpm")
|
||||
echo "$signature_check"
|
||||
grep -q 'digests signatures OK$' <<<"$signature_check"
|
||||
|
||||
for distribution_path in "${rpm_repository_paths[@]}"; do
|
||||
[[ "$distribution_path" == */"$architecture" ]] || continue
|
||||
install -m 0644 "$signed_rpm" \
|
||||
"$repository_dir/$distribution_path/packages/$(basename "$signed_rpm")"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
generate_apt_metadata() {
|
||||
apt_root="$repository_dir/apt"
|
||||
for architecture in amd64 arm64; do
|
||||
binary_dir="$apt_root/dists/$REPOSITORY_CHANNEL/main/binary-$architecture"
|
||||
mkdir -p "$binary_dir"
|
||||
(
|
||||
cd "$apt_root"
|
||||
apt-ftparchive -a "$architecture" packages "$apt_pool_path" \
|
||||
>"dists/$REPOSITORY_CHANNEL/main/binary-$architecture/Packages"
|
||||
)
|
||||
gzip -n -9 -c "$binary_dir/Packages" >"$binary_dir/Packages.gz"
|
||||
done
|
||||
|
||||
(
|
||||
cd "$apt_root"
|
||||
apt-ftparchive \
|
||||
-o APT::FTPArchive::Release::Origin=TauriTavern \
|
||||
-o APT::FTPArchive::Release::Label=TauriTavern \
|
||||
-o APT::FTPArchive::Release::Suite="$REPOSITORY_CHANNEL" \
|
||||
-o APT::FTPArchive::Release::Codename="$REPOSITORY_CHANNEL" \
|
||||
-o APT::FTPArchive::Release::Architectures='amd64 arm64' \
|
||||
-o APT::FTPArchive::Release::Components=main \
|
||||
-o APT::FTPArchive::Release::Description="TauriTavern $REPOSITORY_CHANNEL repository" \
|
||||
release "dists/$REPOSITORY_CHANNEL" >"dists/$REPOSITORY_CHANNEL/Release"
|
||||
)
|
||||
|
||||
gpg \
|
||||
--batch \
|
||||
--yes \
|
||||
--pinentry-mode loopback \
|
||||
--passphrase-file "$LINUX_REPOSITORY_GPG_PASSPHRASE_FILE" \
|
||||
--local-user "${LINUX_REPOSITORY_SIGNING_FPR}!" \
|
||||
--armor \
|
||||
--detach-sign \
|
||||
--output "$apt_root/dists/$REPOSITORY_CHANNEL/Release.gpg" \
|
||||
"$apt_root/dists/$REPOSITORY_CHANNEL/Release"
|
||||
gpg \
|
||||
--batch \
|
||||
--yes \
|
||||
--pinentry-mode loopback \
|
||||
--passphrase-file "$LINUX_REPOSITORY_GPG_PASSPHRASE_FILE" \
|
||||
--local-user "${LINUX_REPOSITORY_SIGNING_FPR}!" \
|
||||
--clearsign \
|
||||
--output "$apt_root/dists/$REPOSITORY_CHANNEL/InRelease" \
|
||||
"$apt_root/dists/$REPOSITORY_CHANNEL/Release"
|
||||
}
|
||||
|
||||
generate_rpm_metadata() {
|
||||
for repository_path in "${rpm_repository_paths[@]}"; do
|
||||
repository_root="$repository_dir/$repository_path"
|
||||
createrepo_c --update "$repository_root"
|
||||
gpg \
|
||||
--batch \
|
||||
--yes \
|
||||
--pinentry-mode loopback \
|
||||
--passphrase-file "$LINUX_REPOSITORY_GPG_PASSPHRASE_FILE" \
|
||||
--local-user "${LINUX_REPOSITORY_SIGNING_FPR}!" \
|
||||
--armor \
|
||||
--detach-sign \
|
||||
--output "$repository_root/repodata/repomd.xml.asc" \
|
||||
"$repository_root/repodata/repomd.xml"
|
||||
done
|
||||
}
|
||||
|
||||
write_manifest() {
|
||||
jq -n \
|
||||
--arg version "$RELEASE_VERSION" \
|
||||
--arg tag "$RELEASE_TAG" \
|
||||
--arg commit "$RELEASE_COMMIT" \
|
||||
--arg channel "$REPOSITORY_CHANNEL" \
|
||||
--arg deb_version "$EXPECTED_DEB_VERSION" \
|
||||
--arg rpm_release "$EXPECTED_RPM_RELEASE" \
|
||||
--arg published_at "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
|
||||
'{
|
||||
schema_version: 1,
|
||||
channel: $channel,
|
||||
version: $version,
|
||||
deb_version: $deb_version,
|
||||
rpm_release: (if $rpm_release == "" then null else $rpm_release end),
|
||||
tag: $tag,
|
||||
commit: $commit,
|
||||
published_at: $published_at
|
||||
}' >"$repository_dir/$manifest_name"
|
||||
}
|
||||
|
||||
verify_repository_metadata() {
|
||||
gpg --batch --verify \
|
||||
"$repository_dir/apt/dists/$REPOSITORY_CHANNEL/Release.gpg" \
|
||||
"$repository_dir/apt/dists/$REPOSITORY_CHANNEL/Release"
|
||||
gpg --batch --verify "$repository_dir/apt/dists/$REPOSITORY_CHANNEL/InRelease"
|
||||
|
||||
for repository_path in "${rpm_repository_paths[@]}"; do
|
||||
repository_root="$repository_dir/$repository_path"
|
||||
gpg --batch --verify \
|
||||
"$repository_root/repodata/repomd.xml.asc" \
|
||||
"$repository_root/repodata/repomd.xml"
|
||||
done
|
||||
}
|
||||
|
||||
upload_apt_repository() {
|
||||
aws_r2 sync \
|
||||
"$repository_dir/apt/pool" \
|
||||
"s3://$PACKAGES_R2_BUCKET/apt/pool" \
|
||||
--cache-control 'public, max-age=31536000, immutable'
|
||||
aws_r2 sync \
|
||||
"$repository_dir/apt/dists" \
|
||||
"s3://$PACKAGES_R2_BUCKET/apt/dists" \
|
||||
--exclude '*/Release' \
|
||||
--exclude '*/Release.gpg' \
|
||||
--exclude '*/InRelease' \
|
||||
--cache-control 'no-cache'
|
||||
for release_file in Release Release.gpg InRelease; do
|
||||
aws_r2 cp \
|
||||
"$repository_dir/apt/dists/$REPOSITORY_CHANNEL/$release_file" \
|
||||
"s3://$PACKAGES_R2_BUCKET/apt/dists/$REPOSITORY_CHANNEL/$release_file" \
|
||||
--cache-control 'no-cache'
|
||||
done
|
||||
}
|
||||
|
||||
upload_rpm_repository() {
|
||||
for repository_path in "${rpm_repository_paths[@]}"; do
|
||||
repository_root="$repository_dir/$repository_path"
|
||||
destination="s3://$PACKAGES_R2_BUCKET/$repository_path"
|
||||
aws_r2 sync \
|
||||
"$repository_root/packages" \
|
||||
"$destination/packages" \
|
||||
--cache-control 'public, max-age=31536000, immutable'
|
||||
aws_r2 sync \
|
||||
"$repository_root/repodata" \
|
||||
"$destination/repodata" \
|
||||
--exclude 'repomd.xml' \
|
||||
--exclude 'repomd.xml.asc' \
|
||||
--cache-control 'public, max-age=31536000, immutable'
|
||||
aws_r2 cp \
|
||||
"$repository_root/repodata/repomd.xml.asc" \
|
||||
"$destination/repodata/repomd.xml.asc" \
|
||||
--cache-control 'no-cache'
|
||||
aws_r2 cp \
|
||||
"$repository_root/repodata/repomd.xml" \
|
||||
"$destination/repodata/repomd.xml" \
|
||||
--cache-control 'no-cache'
|
||||
done
|
||||
}
|
||||
|
||||
sync_existing_packages
|
||||
stage_deb_packages
|
||||
stage_rpm_packages
|
||||
generate_apt_metadata
|
||||
generate_rpm_metadata
|
||||
write_manifest
|
||||
verify_repository_metadata
|
||||
|
||||
if [ "${TAURITAVERN_REPOSITORY_PREPARE_ONLY:-0}" = "1" ]; then
|
||||
echo "Prepared and verified TauriTavern $RELEASE_VERSION repository metadata"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Immutable payloads and ordinary metadata are uploaded before the signed
|
||||
# release pointers, so a failed upload leaves clients on the previous snapshot.
|
||||
upload_apt_repository
|
||||
upload_rpm_repository
|
||||
aws_r2 cp \
|
||||
"$repository_dir/$manifest_name" \
|
||||
"s3://$PACKAGES_R2_BUCKET/$manifest_name" \
|
||||
--cache-control 'no-cache'
|
||||
|
||||
echo "Published TauriTavern $RELEASE_VERSION ($REPOSITORY_CHANNEL) to $PACKAGES_R2_BUCKET"
|
||||
90
scripts/ci/publish-nix-cache.sh
Executable file
90
scripts/ci/publish-nix-cache.sh
Executable file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${NIX_CACHE_PRIVATE_KEY_BASE64:?Missing NIX_CACHE_PRIVATE_KEY_BASE64}"
|
||||
: "${NIX_CACHE_PUBLIC_KEY:?Missing NIX_CACHE_PUBLIC_KEY}"
|
||||
: "${R2_ENDPOINT:?Missing R2_ENDPOINT}"
|
||||
: "${NIX_R2_BUCKET:?Missing NIX_R2_BUCKET}"
|
||||
|
||||
NIX_STORE_PATH=${NIX_STORE_PATH:-./result}
|
||||
NIX_CACHE_URL=${NIX_CACHE_URL:-}
|
||||
NIX_CACHE_URL=${NIX_CACHE_URL%/}
|
||||
|
||||
for required_command in aws base64 curl nix; do
|
||||
command -v "$required_command" >/dev/null 2>&1 || {
|
||||
echo "Missing required command: $required_command" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
work_dir=$(mktemp -d "${RUNNER_TEMP:-/tmp}/tauritavern-nix-cache.XXXXXX")
|
||||
trap 'rm -rf "$work_dir"' EXIT
|
||||
key_file="$work_dir/private-key"
|
||||
cache_dir="$work_dir/cache"
|
||||
mkdir -p "$cache_dir"
|
||||
|
||||
umask 077
|
||||
printf '%s' "$NIX_CACHE_PRIVATE_KEY_BASE64" | base64 --decode >"$key_file"
|
||||
actual_public_key=$(nix key convert-secret-to-public <"$key_file")
|
||||
test "$actual_public_key" = "$NIX_CACHE_PUBLIC_KEY" || {
|
||||
echo "Nix cache private key does not match NIX_CACHE_PUBLIC_KEY" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
store_roots=("$NIX_STORE_PATH")
|
||||
for extra_path in "$@"; do
|
||||
nix path-info "$extra_path" >/dev/null
|
||||
store_name=$(basename "$extra_path")
|
||||
store_hash=${store_name%%-*}
|
||||
[[ "$store_hash" =~ ^[0-9a-df-np-sv-z]{32}$ ]] || {
|
||||
echo "Invalid Nix store path: $extra_path" >&2
|
||||
exit 1
|
||||
}
|
||||
: "${NIX_CACHE_URL:?Missing NIX_CACHE_URL for additional store paths}"
|
||||
response_code=$(curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--retry 3 \
|
||||
--output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
"$NIX_CACHE_URL/$store_hash.narinfo")
|
||||
case "$response_code" in
|
||||
200)
|
||||
echo "Already cached: $extra_path"
|
||||
;;
|
||||
404)
|
||||
store_roots+=("$extra_path")
|
||||
;;
|
||||
*)
|
||||
echo "Unable to query $extra_path in the public cache: HTTP $response_code" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
nix path-info --recursive "${store_roots[@]}" |
|
||||
sort -u |
|
||||
nix store sign --stdin --key-file "$key_file"
|
||||
nix path-info --sigs "${store_roots[@]}"
|
||||
nix copy --to "file://$cache_dir" "${store_roots[@]}"
|
||||
|
||||
aws_r2() {
|
||||
aws --endpoint-url "$R2_ENDPOINT" s3 "$@"
|
||||
}
|
||||
|
||||
aws_r2 sync \
|
||||
"$cache_dir/nar" \
|
||||
"s3://$NIX_R2_BUCKET/nar" \
|
||||
--cache-control 'public, max-age=31536000, immutable'
|
||||
aws_r2 sync \
|
||||
"$cache_dir" \
|
||||
"s3://$NIX_R2_BUCKET" \
|
||||
--exclude '*' \
|
||||
--include '*.narinfo' \
|
||||
--cache-control 'public, max-age=31536000, immutable'
|
||||
aws_r2 cp \
|
||||
"$cache_dir/nix-cache-info" \
|
||||
"s3://$NIX_R2_BUCKET/nix-cache-info" \
|
||||
--cache-control 'no-cache'
|
||||
54
scripts/ci/rewrite-deb-version.sh
Executable file
54
scripts/ci/rewrite-deb-version.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <package.deb> <version>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
package_path=$1
|
||||
target_version=$2
|
||||
|
||||
command -v dpkg-deb >/dev/null 2>&1 || {
|
||||
echo "Missing required command: dpkg-deb" >&2
|
||||
exit 1
|
||||
}
|
||||
test -f "$package_path" || {
|
||||
echo "DEB package not found: $package_path" >&2
|
||||
exit 1
|
||||
}
|
||||
dpkg --validate-version "$target_version"
|
||||
|
||||
work_dir=$(mktemp -d "${RUNNER_TEMP:-/tmp}/tauritavern-deb.XXXXXX")
|
||||
trap 'rm -rf "$work_dir"' EXIT
|
||||
|
||||
package_root="$work_dir/package"
|
||||
rebuilt_package="$work_dir/rebuilt.deb"
|
||||
dpkg-deb --raw-extract "$package_path" "$package_root"
|
||||
|
||||
control_file="$package_root/DEBIAN/control"
|
||||
test -f "$control_file" || {
|
||||
echo "Missing DEBIAN/control in $package_path" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
awk -v version="$target_version" '
|
||||
BEGIN { replaced = 0 }
|
||||
/^Version: / {
|
||||
print "Version: " version
|
||||
replaced = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!replaced) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "$control_file" >"$control_file.new"
|
||||
mv "$control_file.new" "$control_file"
|
||||
|
||||
dpkg-deb --build --root-owner-group "$package_root" "$rebuilt_package"
|
||||
test "$(dpkg-deb -f "$rebuilt_package" Version)" = "$target_version"
|
||||
install -m 0644 "$rebuilt_package" "$package_path"
|
||||
62
scripts/ci/verify-release-version.mjs
Executable file
62
scripts/ci/verify-release-version.mjs
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const repositoryRoot = resolve(import.meta.dirname, '../..');
|
||||
const requestedTag = process.argv[2] ?? '';
|
||||
const version = requestedTag.startsWith('v') ? requestedTag.slice(1) : requestedTag;
|
||||
|
||||
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)) {
|
||||
throw new Error(`Stable release tag must be vMAJOR.MINOR.PATCH, received: ${requestedTag || '(empty)'}`);
|
||||
}
|
||||
|
||||
async function read(path) {
|
||||
return readFile(resolve(repositoryRoot, path), 'utf8');
|
||||
}
|
||||
|
||||
function expectMatch(path, source, pattern) {
|
||||
const match = source.match(pattern);
|
||||
if (!match) {
|
||||
throw new Error(`Unable to locate the release version in ${path}`);
|
||||
}
|
||||
if (match[1] !== version) {
|
||||
throw new Error(`${path} declares ${match[1]}, but the release tag declares ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
const packageJsonPath = 'package.json';
|
||||
const tauriConfigPath = 'src-tauri/crates/tauritavern/tauri.conf.json';
|
||||
const cargoManifestPath = 'src-tauri/crates/tauritavern/Cargo.toml';
|
||||
const cargoLockPath = 'src-tauri/Cargo.lock';
|
||||
const nixPackagePath = 'nix/package.nix';
|
||||
|
||||
const [
|
||||
packageJsonSource,
|
||||
tauriConfigSource,
|
||||
cargoManifestSource,
|
||||
cargoLockSource,
|
||||
nixPackageSource,
|
||||
] = await Promise.all([
|
||||
read(packageJsonPath),
|
||||
read(tauriConfigPath),
|
||||
read(cargoManifestPath),
|
||||
read(cargoLockPath),
|
||||
read(nixPackagePath),
|
||||
]);
|
||||
|
||||
const packageJson = JSON.parse(packageJsonSource);
|
||||
const tauriConfig = JSON.parse(tauriConfigSource);
|
||||
|
||||
if (packageJson.version !== version) {
|
||||
throw new Error(`${packageJsonPath} declares ${packageJson.version}, but the release tag declares ${version}`);
|
||||
}
|
||||
if (tauriConfig.version !== version) {
|
||||
throw new Error(`${tauriConfigPath} declares ${tauriConfig.version}, but the release tag declares ${version}`);
|
||||
}
|
||||
|
||||
expectMatch(cargoManifestPath, cargoManifestSource, /^\[package\]\s+name = "tauritavern"\s+version = "([^"]+)"/m);
|
||||
expectMatch(cargoLockPath, cargoLockSource, /\[\[package\]\]\s+name = "tauritavern"\s+version = "([^"]+)"/m);
|
||||
expectMatch(nixPackagePath, nixPackageSource, /^\s*version = "([^"]+)";$/m);
|
||||
|
||||
process.stdout.write(`${version}\n`);
|
||||
35
tests/release-version.test.mjs
Normal file
35
tests/release-version.test.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { resolve } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const verifier = resolve('scripts/ci/verify-release-version.mjs');
|
||||
const currentVersion = JSON.parse(readFileSync('package.json', 'utf8')).version;
|
||||
|
||||
test('stable release verifier accepts the synchronized project version', () => {
|
||||
const result = spawnSync(process.execPath, [verifier, `v${currentVersion}`], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout.trim(), currentVersion);
|
||||
});
|
||||
|
||||
test('stable release verifier rejects a mismatched tag', () => {
|
||||
const result = spawnSync(process.execPath, [verifier, 'v99.99.99'], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /package\.json declares/);
|
||||
});
|
||||
|
||||
test('stable release verifier rejects prerelease tags', () => {
|
||||
const result = spawnSync(process.execPath, [verifier, `v${currentVersion}-rc.1`], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /vMAJOR\.MINOR\.PATCH/);
|
||||
});
|
||||
42
tests/stable-release-workflow.test.mjs
Normal file
42
tests/stable-release-workflow.test.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import YAML from 'yaml';
|
||||
|
||||
const workflowPath = '.github/workflows/stable-release.yml';
|
||||
const workflowSource = readFileSync(workflowPath, 'utf8');
|
||||
const workflow = YAML.parse(workflowSource);
|
||||
|
||||
test('stable release workflow starts from a published release or an explicit tag', () => {
|
||||
assert.deepEqual(workflow.on.release.types, ['published']);
|
||||
assert.equal(workflow.on.workflow_dispatch.inputs.tag.required, true);
|
||||
});
|
||||
|
||||
test('stable release workflow preserves manually written release notes', () => {
|
||||
assert.doesNotMatch(workflowSource, /codex|release edit|notes-file/i);
|
||||
assert.match(workflowSource, /Upload assets without changing release notes/);
|
||||
});
|
||||
|
||||
test('stable release workflow publishes release assets before optional repositories', () => {
|
||||
assert.deepEqual(workflow.jobs['publish-release'].needs, ['prepare', 'desktop', 'mobile']);
|
||||
|
||||
for (const jobName of ['publish-package-repositories', 'publish-nix-cache']) {
|
||||
const job = workflow.jobs[jobName];
|
||||
assert.deepEqual(job.needs, ['prepare', 'publish-release']);
|
||||
assert.equal(job['continue-on-error'], true);
|
||||
}
|
||||
});
|
||||
|
||||
test('stable release workflow keeps repository credentials in GitHub secrets', () => {
|
||||
assert.match(workflowSource, /secrets\.R2_ACCESS_KEY_ID/);
|
||||
assert.match(workflowSource, /secrets\.R2_SECRET_ACCESS_KEY/);
|
||||
assert.match(workflowSource, /secrets\.LINUX_REPOSITORY_GPG_PRIVATE_KEY_BASE64/);
|
||||
assert.match(workflowSource, /secrets\.NIX_CACHE_PRIVATE_KEY_BASE64/);
|
||||
assert.doesNotMatch(workflowSource, /BEGIN (?:PGP|OPENSSH|PRIVATE) PRIVATE KEY/);
|
||||
});
|
||||
|
||||
test('stable Nix publication includes reusable project dependencies', () => {
|
||||
assert.match(workflowSource, /tauritavern\.cargoDeps\.outPath/);
|
||||
assert.match(workflowSource, /tauritavern\.pnpmDeps\.outPath/);
|
||||
assert.match(workflowSource, /NIX_CACHE_URL: https:\/\/nix-cache\.tauritavern\.com/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue