🏗️ Build and Test JellyTau / Run Tests (push) Successful in 13m55s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m22s
Traceability Validation / Check Requirement Traces (push) Successful in 16s
Build & Release / Run Tests (push) Successful in 14m0s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m9s
Build & Release / Build Linux (push) Successful in 16m39s
Build & Release / Build Windows (push) Successful in 20m42s
Build & Release / Build Android (push) Successful in 29m18s
Build & Release / Create Release (push) Successful in 13s
"Prepare Linux artifacts" ran `shopt -s nullglob`, but the runner executes
`run:` blocks with POSIX sh, where shopt does not exist. It exited 127 and
failed the step -- so build-linux never uploaded, create-release (which
needs all three build jobs) never ran, and v0.9.0 and v0.9.1 both compiled
successfully but published nothing. The last release with assets is v0.8.2.
Reproduced under busybox sh: the current block prints "shopt: not found",
passes the unmatched rpm glob through literally ("cp: can't stat
'.../bundle/rpm/*.rpm'"), and exits 127. Without nullglob an unmatched
pattern stays literal, so test each candidate with [ -e ] instead; the
same input then exits 0 with the AppImage and deb copied.
traceability-check.yml already carries this rule in two places (`case`
instead of `[[ == ]]`, a pipe instead of a here-string). Keeping the fix
POSIX rather than adding `shell: bash` follows that convention and drops
the dependency on bash being present in the builder image.
497 lines
20 KiB
YAML
497 lines
20 KiB
YAML
name: Build & Release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*'
|
||
workflow_dispatch:
|
||
inputs:
|
||
version:
|
||
description: 'Version to build (e.g., v1.0.0)'
|
||
required: false
|
||
|
||
env:
|
||
RUST_BACKTRACE: 1
|
||
CARGO_TERM_COLOR: always
|
||
# Incremental state is never reused between CI runs -- pure disk cost.
|
||
CARGO_INCREMENTAL: 0
|
||
|
||
jobs:
|
||
test:
|
||
name: Run Tests
|
||
runs-on: linux/amd64
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
# Registry only -- never src-tauri/target. That directory is ~16 GB and
|
||
# was cached under five separate keys, which filled the runner's 74 GB
|
||
# disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026).
|
||
# registry/src is omitted too: cargo re-extracts it for free from
|
||
# registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted).
|
||
path: |
|
||
~/.cargo/registry/index
|
||
~/.cargo/registry/cache
|
||
~/.cargo/git/db
|
||
# One shared key across every job. The old per-job keys existed to stop
|
||
# debug/release target artifacts clobbering each other; with target no
|
||
# longer cached, registry contents are target-independent, so all jobs
|
||
# want the same crates. First job to finish saves; the rest restore.
|
||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-registry-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install
|
||
|
||
- name: Run frontend tests
|
||
run: |
|
||
bunx svelte-kit sync
|
||
bun run test --run
|
||
continue-on-error: false
|
||
|
||
# Same gate as build-and-test.yml. A release must not ship from a tree
|
||
# that would fail the per-commit checks. rustfmt/clippy come from the
|
||
# builder image; nothing is installed here.
|
||
- name: Check Rust formatting
|
||
run: |
|
||
cd src-tauri
|
||
cargo fmt --all -- --check
|
||
continue-on-error: false
|
||
|
||
# Advisory until the ~51 pre-existing warnings are cleared; see the longer
|
||
# note in build-and-test.yml. Tighten both to `-- -D warnings` together.
|
||
- name: Run clippy (advisory)
|
||
run: |
|
||
cd src-tauri
|
||
cargo clippy --all-targets
|
||
|
||
- name: Run Rust tests
|
||
run: bun run test:rust
|
||
continue-on-error: false
|
||
|
||
- name: Check TypeScript
|
||
run: bun run check
|
||
continue-on-error: false
|
||
|
||
build-linux:
|
||
name: Build Linux
|
||
runs-on: linux/amd64
|
||
needs: test
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
# Registry only -- never src-tauri/target. That directory is ~16 GB and
|
||
# was cached under five separate keys, which filled the runner's 74 GB
|
||
# disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026).
|
||
# registry/src is omitted too: cargo re-extracts it for free from
|
||
# registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted).
|
||
path: |
|
||
~/.cargo/registry/index
|
||
~/.cargo/registry/cache
|
||
~/.cargo/git/db
|
||
# One shared key across every job. The old per-job keys existed to stop
|
||
# debug/release target artifacts clobbering each other; with target no
|
||
# longer cached, registry contents are target-independent, so all jobs
|
||
# want the same crates. First job to finish saves; the rest restore.
|
||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-registry-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install
|
||
|
||
# The Linux job previously had no version step at all, so a tagged release
|
||
# built Linux packages from whatever version happened to be committed.
|
||
- name: Set app version from tag
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
|
||
- name: Build for Linux
|
||
run: bun run tauri build
|
||
env:
|
||
TAURI_SKIP_UPDATER: true
|
||
|
||
- name: Prepare Linux artifacts
|
||
run: |
|
||
mkdir -p dist/linux
|
||
# Match by extension, not by product name. Bundle filenames follow
|
||
# `productName`, so renaming the app (jellytau -> JellyTau) made the
|
||
# old `jellytau_*.deb` glob match nothing — and because the copy was
|
||
# wrapped in `if [ -f ... ]`, the artifact simply vanished from the
|
||
# release with no error. Each bundle directory holds one file.
|
||
#
|
||
# `if [ -f "dir/"*.ext ]` was also wrong on its own terms: with more
|
||
# than one match `test` gets extra arguments and fails.
|
||
#
|
||
# No `shopt -s nullglob` here: the runner executes `run:` blocks with
|
||
# POSIX sh, where shopt does not exist -- it exited 127 and killed the
|
||
# step (which is why v0.9.0 and v0.9.1 built but never published).
|
||
# Without nullglob an unmatched pattern stays literal, so test each
|
||
# candidate instead. Same POSIX-only rule as traceability-check.yml.
|
||
for bundle in \
|
||
src-tauri/target/release/bundle/appimage/*.AppImage \
|
||
src-tauri/target/release/bundle/deb/*.deb \
|
||
src-tauri/target/release/bundle/rpm/*.rpm; do
|
||
[ -e "$bundle" ] || continue
|
||
cp -v "$bundle" dist/linux/
|
||
done
|
||
|
||
# A release with no Linux package is a failure, not a quiet success.
|
||
if [ -z "$(ls -A dist/linux/)" ]; then
|
||
echo "::error::No Linux bundles found under src-tauri/target/release/bundle/"
|
||
exit 1
|
||
fi
|
||
ls -lah dist/linux/
|
||
|
||
- name: Upload Linux build artifact
|
||
uses: actions/upload-artifact@v3
|
||
with:
|
||
name: jellytau-linux
|
||
path: dist/linux/
|
||
retention-days: 7
|
||
|
||
build-windows:
|
||
name: Build Windows
|
||
runs-on: linux/amd64
|
||
needs: test
|
||
# Cross-compiled from Linux via the official Tauri path (MSVC + cargo-xwin),
|
||
# baked into the builder image. No toolchain installs here — the image has
|
||
# cargo-xwin, clang/clang-cl, lld, llvm, nsis and the msvc target.
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
# Registry only -- never src-tauri/target. That directory is ~16 GB and
|
||
# was cached under five separate keys, which filled the runner's 74 GB
|
||
# disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026).
|
||
# registry/src is omitted too: cargo re-extracts it for free from
|
||
# registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted).
|
||
path: |
|
||
~/.cargo/registry/index
|
||
~/.cargo/registry/cache
|
||
~/.cargo/git/db
|
||
# One shared key across every job. The old per-job keys existed to stop
|
||
# debug/release target artifacts clobbering each other; with target no
|
||
# longer cached, registry contents are target-independent, so all jobs
|
||
# want the same crates. First job to finish saves; the rest restore.
|
||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-registry-
|
||
|
||
- name: Cache Windows CRT/SDK (cargo-xwin)
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: ~/.cache/cargo-xwin
|
||
# Contents track the xwin version baked into the builder image, not our
|
||
# lockfile -- keying this on Cargo.lock re-downloaded the whole SDK on
|
||
# every release bump. Bump the suffix by hand if the image's xwin moves.
|
||
key: ${{ runner.os }}-cargo-xwin-v1
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
# The tag is the single source of truth for a release version; the script
|
||
# stamps every file that carries it (package.json, tauri.conf.json,
|
||
# Cargo.toml, Cargo.lock). This step used to sed only tauri.conf.json, so
|
||
# the other three shipped whatever was committed.
|
||
- name: Set app version from tag
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
|
||
- name: Build Windows (NSIS installer + exe)
|
||
run: OUTPUT_DIR="$PWD/dist/windows" WIN_BUNDLES=nsis ./scripts/build-windows-cross.sh
|
||
|
||
- name: List Windows artifacts
|
||
run: ls -lah dist/windows/
|
||
|
||
- name: Upload Windows build artifact
|
||
uses: actions/upload-artifact@v3
|
||
with:
|
||
name: jellytau-windows
|
||
path: dist/windows/
|
||
retention-days: 7
|
||
|
||
build-android:
|
||
name: Build Android
|
||
runs-on: linux/amd64
|
||
needs: test
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
env:
|
||
ANDROID_HOME: /opt/android-sdk
|
||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||
ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Cache Rust dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
# Registry only -- never src-tauri/target. That directory is ~16 GB and
|
||
# was cached under five separate keys, which filled the runner's 74 GB
|
||
# disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026).
|
||
# registry/src is omitted too: cargo re-extracts it for free from
|
||
# registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted).
|
||
path: |
|
||
~/.cargo/registry/index
|
||
~/.cargo/registry/cache
|
||
~/.cargo/git/db
|
||
# One shared key across every job. The old per-job keys existed to stop
|
||
# debug/release target artifacts clobbering each other; with target no
|
||
# longer cached, registry contents are target-independent, so all jobs
|
||
# want the same crates. First job to finish saves; the rest restore.
|
||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-cargo-registry-
|
||
|
||
- name: Cache Node dependencies
|
||
uses: actions/cache@v3
|
||
with:
|
||
path: |
|
||
~/.bun/install/cache
|
||
node_modules
|
||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-bun-
|
||
|
||
- name: Install dependencies
|
||
run: bun install
|
||
|
||
# Stamp before `android init`: it derives its generated project (including
|
||
# the initial versionCode) from tauri.conf.json.
|
||
- name: Set app version from tag
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
|
||
- name: Initialize Android project
|
||
run: bun run tauri android init
|
||
|
||
# Re-run after init: tauri.properties only exists now, and its
|
||
# autogenerated versionCode (0.0.15 -> 15) is both tiny and NOT monotonic
|
||
# against the 1000 floor already shipped in the field. The script rewrites
|
||
# it as 1000 + major*10000 + minor*100 + patch. Runs unconditionally so
|
||
# untagged builds get a sane code too, derived from git describe.
|
||
- name: Pin a monotonic Android versionCode
|
||
run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}"
|
||
|
||
- name: Sync custom Android sources & gradle config
|
||
run: ./scripts/sync-android-sources.sh
|
||
|
||
- name: Write signing keystore
|
||
run: |
|
||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/jellytau-release.jks"
|
||
cat > src-tauri/gen/android/keystore.properties <<EOF
|
||
storeFile=$RUNNER_TEMP/jellytau-release.jks
|
||
storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
|
||
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
EOF
|
||
|
||
- name: Build signed Android APK
|
||
run: bun run tauri android build --apk true --target aarch64
|
||
|
||
- name: Collect & verify signed APK
|
||
run: |
|
||
mkdir -p dist/android
|
||
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name '*-release.apk' | head -1)
|
||
if [ -z "$APK" ]; then echo "❌ No release APK produced"; exit 1; fi
|
||
cp "$APK" dist/android/jellytau-release.apk
|
||
APKSIGNER=$(find "$ANDROID_SDK_ROOT/build-tools" -name apksigner | sort -V | tail -1)
|
||
echo "🔏 Verifying signature with $APKSIGNER"
|
||
"$APKSIGNER" verify --print-certs dist/android/jellytau-release.apk
|
||
ls -lah dist/android/
|
||
|
||
- name: Upload Android build artifact
|
||
uses: actions/upload-artifact@v3
|
||
with:
|
||
name: jellytau-android
|
||
path: dist/android/
|
||
retention-days: 7
|
||
|
||
create-release:
|
||
name: Create Release
|
||
runs-on: linux/amd64
|
||
needs: [build-linux, build-windows, build-android]
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
container:
|
||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||
steps:
|
||
- name: Checkout repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Get version from tag
|
||
id: tag_name
|
||
run: |
|
||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
echo "RELEASE_NAME=JellyTau ${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||
|
||
- name: Download Linux artifacts
|
||
uses: actions/download-artifact@v3
|
||
with:
|
||
name: jellytau-linux
|
||
path: artifacts/linux/
|
||
|
||
- name: Download Windows artifacts
|
||
uses: actions/download-artifact@v3
|
||
with:
|
||
name: jellytau-windows
|
||
path: artifacts/windows/
|
||
|
||
- name: Download Android artifacts
|
||
uses: actions/download-artifact@v3
|
||
with:
|
||
name: jellytau-android
|
||
path: artifacts/android/
|
||
|
||
- name: Prepare release notes
|
||
id: release_notes
|
||
run: |
|
||
VERSION="${{ steps.tag_name.outputs.VERSION }}"
|
||
echo "## JellyTau $VERSION Release" > release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Downloads" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Linux" >> release_notes.md
|
||
echo "- **AppImage** - Run directly on most Linux distributions" >> release_notes.md
|
||
echo "- **DEB** - Install via \`sudo dpkg -i JellyTau_*.deb\` (Ubuntu/Debian)" >> release_notes.md
|
||
echo "- **RPM** - Install via \`sudo rpm -i JellyTau-*.rpm\` (Fedora/openSUSE)" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Windows" >> release_notes.md
|
||
echo "- **Installer (.exe)** - Run \`JellyTau_*-setup.exe\` (NSIS). Unsigned — SmartScreen may warn on first run." >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Android" >> release_notes.md
|
||
echo "- **APK** - Install via \`adb install jellytau-release.apk\` or sideload via file manager" >> release_notes.md
|
||
echo "- **AAB** - Upload to Google Play Console or testing platforms" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### What's New" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "See [CHANGELOG.md](CHANGELOG.md) for detailed changes." >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Installation" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Linux (AppImage)" >> release_notes.md
|
||
echo "\`\`\`bash" >> release_notes.md
|
||
echo "chmod +x JellyTau_*.AppImage" >> release_notes.md
|
||
echo "./JellyTau_*.AppImage" >> release_notes.md
|
||
echo "\`\`\`" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Linux (DEB)" >> release_notes.md
|
||
echo "\`\`\`bash" >> release_notes.md
|
||
echo "sudo dpkg -i JellyTau_*.deb" >> release_notes.md
|
||
echo "jellytau" >> release_notes.md
|
||
echo "\`\`\`" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "#### Android" >> release_notes.md
|
||
echo "- Sideload: Download APK and install via file manager or ADB" >> release_notes.md
|
||
echo "- Play Store: Coming soon" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Known Issues" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "See [GitHub Issues](../../issues) for reported bugs." >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "### Requirements" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "**Linux:**" >> release_notes.md
|
||
echo "- 64-bit Linux system" >> release_notes.md
|
||
echo "- GLIBC 2.29+" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "**Android:**" >> release_notes.md
|
||
echo "- Android 8.0 or higher" >> release_notes.md
|
||
echo "- 50MB free storage" >> release_notes.md
|
||
echo "" >> release_notes.md
|
||
echo "---" >> release_notes.md
|
||
echo "Built with Tauri, SvelteKit, and Rust" >> release_notes.md
|
||
|
||
- name: Publish Gitea release & upload assets
|
||
env:
|
||
# GITEA_TOKEN (a PAT) is preferred; falls back to the auto-provided token.
|
||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -e
|
||
command -v jq >/dev/null || { echo "❌ jq is required on the runner"; exit 1; }
|
||
VERSION="${{ steps.tag_name.outputs.VERSION }}"
|
||
API="${GITHUB_SERVER_URL}/api/v1"
|
||
REPO="${GITHUB_REPOSITORY}"
|
||
TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}"
|
||
case "$VERSION" in *rc*|*beta*|*alpha*) PRE=true;; *) PRE=false;; esac
|
||
|
||
PAYLOAD=$(jq -n \
|
||
--arg tag "$VERSION" \
|
||
--arg name "JellyTau $VERSION" \
|
||
--rawfile body release_notes.md \
|
||
--argjson pre "$PRE" \
|
||
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:$pre}')
|
||
|
||
echo "📦 Creating release $VERSION on $REPO"
|
||
# -f drops on HTTP error; capture status so an existing release (409) is handled gracefully.
|
||
HTTP=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/repos/$REPO/releases" \
|
||
-H "Authorization: token $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$PAYLOAD")
|
||
if [ "$HTTP" = "201" ]; then
|
||
RELEASE_ID=$(jq -r '.id' resp.json)
|
||
elif [ "$HTTP" = "409" ]; then
|
||
echo "ℹ️ Release $VERSION already exists; fetching its id to upload assets"
|
||
RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$VERSION" \
|
||
-H "Authorization: token $TOKEN" | jq -r '.id')
|
||
else
|
||
echo "❌ Failed to create release (HTTP $HTTP):"; cat resp.json; exit 1
|
||
fi
|
||
echo "Release id=$RELEASE_ID"
|
||
|
||
for f in artifacts/android/* artifacts/linux/* artifacts/windows/*; do
|
||
[ -f "$f" ] || continue
|
||
echo "⬆️ Uploading $(basename "$f")"
|
||
curl -fsS -X POST \
|
||
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
|
||
-H "Authorization: token $TOKEN" \
|
||
-F "attachment=@$f" >/dev/null
|
||
done
|
||
echo "✅ Release $VERSION published with assets"
|