The first fix didn't help: the pipeline creates the tag via the Gitea API, and the push mirror (sync_on_commit only fires on real git pushes) doesn't propagate an API-created tag promptly. So the Codeberg release POST still raced the mirror and 500'd on a commit/tag Codeberg hadn't received (0.2.1 and 0.3.0 both shipped everywhere but Codeberg). Push the tag straight to Codeberg from the runner (guaranteed present), then attach the release to that existing tag with no target_commitish — which is what Forgejo 500s on. Race-free. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
448 lines
20 KiB
YAML
448 lines
20 KiB
YAML
name: Release — F-Droid repo + Gitea/Codeberg release
|
|
|
|
# A release is cut by merging a release branch into main with a bumped
|
|
# versionName (see docs/RELEASING.md). This workflow reads that versionName and,
|
|
# if no matching tag exists yet, runs tests, builds + signs the APK, publishes
|
|
# it to the F-Droid repo, creates the vX.Y.Z tag + Gitea release, and mirrors
|
|
# that release to Codeberg with the signed APK + a SHA-256 checksum as a
|
|
# direct-download channel — the tag is an output of the pipeline, not its
|
|
# trigger. Ordinary merges (no version bump) fall through `detect` and do
|
|
# nothing.
|
|
#
|
|
# A manual workflow_dispatch (from a branch) runs the re-sign-only recovery
|
|
# path: it re-signs the existing F-Droid index with the repo key and re-uploads,
|
|
# without building an APK or creating a release. Used for key rotation / repo
|
|
# recovery.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
# Cheap gate: resolve the version from the committed build.gradle and decide
|
|
# whether this push actually cuts a new release (no tag for it yet). Keeps the
|
|
# heavy job from running on every merge to main.
|
|
detect:
|
|
runs-on: docker
|
|
outputs:
|
|
is_release: ${{ steps.v.outputs.is_release }}
|
|
version: ${{ steps.v.outputs.version }}
|
|
version_code: ${{ steps.v.outputs.version_code }}
|
|
prerelease: ${{ steps.v.outputs.prerelease }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Resolve version and whether it is a new release
|
|
id: v
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
|
run: |
|
|
set -e
|
|
VERSION=$(grep -oP 'versionName\s*=\s*"\K[^"]+' app/build.gradle.kts)
|
|
if [ -z "$VERSION" ]; then echo "No versionName in app/build.gradle.kts" >&2; exit 1; fi
|
|
MAJOR=$(echo "$VERSION" | cut -d. -f1); MINOR=$(echo "$VERSION" | cut -d. -f2); PATCH=$(echo "$VERSION" | cut -d. -f3)
|
|
MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
|
|
VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "version_code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
|
|
# Pre-1.0 is not stable yet: mark the Gitea release as a pre-release
|
|
# while MAJOR is 0. Graduates to a stable release automatically at 1.0.0.
|
|
if [ "$MAJOR" = "0" ]; then PRERELEASE=true; else PRERELEASE=false; fi
|
|
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
|
|
echo "Resolved version $VERSION (code $VERSION_CODE, prerelease=$PRERELEASE)"
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
echo "Manual dispatch — re-sign path, not a release."
|
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
# A tag for this version already existing means the release shipped on
|
|
# an earlier push; do nothing. Absent => this merge cuts the release.
|
|
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: token $TOKEN" "$API/git/refs/tags/v$VERSION")
|
|
if [ "$STATUS" = "200" ]; then
|
|
echo "Tag v$VERSION already exists — nothing to release."
|
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "No tag for v$VERSION yet — cutting the release."
|
|
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# Releases: build + sign + publish, then mint the tag and Gitea release.
|
|
# Also runs on manual dispatch, where it skips the build and just re-signs and
|
|
# re-uploads the existing index (recovery path).
|
|
release:
|
|
needs: detect
|
|
if: needs.detect.outputs.is_release == 'true' || github.event_name == 'workflow_dispatch'
|
|
runs-on: docker
|
|
env:
|
|
ANDROID_HOME: /opt/android-sdk
|
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
VERSION: ${{ needs.detect.outputs.version }}
|
|
VERSION_CODE: ${{ needs.detect.outputs.version_code }}
|
|
IS_RELEASE: ${{ needs.detect.outputs.is_release }}
|
|
PRERELEASE: ${{ needs.detect.outputs.prerelease }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: 'zulu'
|
|
java-version: '17'
|
|
|
|
- name: Setup Android SDK
|
|
uses: android-actions/setup-android@v3
|
|
with:
|
|
packages: ''
|
|
|
|
- name: Setup Android SDK cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: /opt/android-sdk
|
|
key: ${{ runner.os }}-android-sdk-37-36.0.0
|
|
|
|
- name: Install Android SDK packages
|
|
run: |
|
|
yes | sdkmanager --licenses >/dev/null || true
|
|
sdkmanager \
|
|
"platform-tools" \
|
|
"platforms;android-37.0" \
|
|
"build-tools;36.0.0"
|
|
|
|
- name: Setup Gradle cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-gradle-
|
|
|
|
- name: Install jq
|
|
run: |
|
|
set -e
|
|
SUDO=""
|
|
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y jq
|
|
elif command -v apk >/dev/null 2>&1; then
|
|
$SUDO apk add --no-cache jq
|
|
fi
|
|
|
|
- name: Grant execute permission for gradlew
|
|
run: chmod +x ./gradlew
|
|
|
|
# The committed versionName is the source of truth. Pin versionCode to the
|
|
# value derived from it so the published APK's code is always
|
|
# MAJOR*10000 + MINOR*100 + PATCH even if the committed code was forgotten.
|
|
- name: Pin versionCode to versionName
|
|
if: env.IS_RELEASE == 'true'
|
|
run: |
|
|
set -e
|
|
sed -i "s/versionCode = .*/versionCode = $VERSION_CODE/" app/build.gradle.kts
|
|
grep -E 'versionName|versionCode' app/build.gradle.kts
|
|
|
|
# Test the exact commit being shipped (only on a real release).
|
|
- name: Unit tests
|
|
if: env.IS_RELEASE == 'true'
|
|
run: ./gradlew testDebugUnitTest
|
|
|
|
- name: Setup Android Keystore
|
|
if: env.IS_RELEASE == 'true'
|
|
env:
|
|
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
|
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
run: |
|
|
mkdir -p app
|
|
echo "$KEYSTORE_BASE64" | base64 --decode > app/upload-keystore.jks
|
|
cat > key.properties <<EOF
|
|
storePassword=$KEY_PASSWORD
|
|
keyPassword=$KEY_PASSWORD
|
|
keyAlias=$KEY_ALIAS
|
|
storeFile=upload-keystore.jks
|
|
EOF
|
|
|
|
- name: Build release APK
|
|
if: env.IS_RELEASE == 'true'
|
|
run: ./gradlew assembleRelease
|
|
|
|
- name: Setup F-Droid Server Tools
|
|
run: |
|
|
SUDO=""
|
|
if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi
|
|
$SUDO apt-get update
|
|
$SUDO apt-get install -y sshpass python3-pip
|
|
pip3 install --break-system-packages --upgrade fdroidserver
|
|
|
|
- name: Fetch existing F-Droid repo from Hetzner
|
|
env:
|
|
HOST: ${{ secrets.HETZNER_HOST }}
|
|
USER: ${{ secrets.HETZNER_USER }}
|
|
PASS: ${{ secrets.HETZNER_PASS }}
|
|
run: |
|
|
set -euo pipefail
|
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20"
|
|
mkdir -p fdroid
|
|
# Pull only the published repo/ (all apps' APKs), any per-app
|
|
# metadata, and the repo icon — enough to rebuild the index without
|
|
# dropping the other apps. The signing key is deliberately NOT pulled
|
|
# from the box; it comes from CI secrets in the next step so it never
|
|
# has to live in the web-served tree.
|
|
sshpass -p "$PASS" scp $SSH_OPTS -r "$USER@$HOST:dev/fdroid/repo" fdroid/ 2>/dev/null || true
|
|
sshpass -p "$PASS" scp $SSH_OPTS -r "$USER@$HOST:dev/fdroid/metadata" fdroid/ 2>/dev/null || true
|
|
sshpass -p "$PASS" scp $SSH_OPTS "$USER@$HOST:dev/fdroid/icon.png" fdroid/ 2>/dev/null || true
|
|
mkdir -p fdroid/repo fdroid/metadata
|
|
|
|
- name: Restore F-Droid signing key and config from secrets
|
|
env:
|
|
FDROID_KEYSTORE_BASE64: ${{ secrets.FDROID_KEYSTORE_BASE64 }}
|
|
FDROID_CONFIG_BASE64: ${{ secrets.FDROID_CONFIG_BASE64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Fail loudly if the repo key is not configured. NEVER auto-generate
|
|
# one: a fresh key changes the repo fingerprint and breaks every
|
|
# user's pinned repo.
|
|
if [ -z "${FDROID_KEYSTORE_BASE64:-}" ] || [ -z "${FDROID_CONFIG_BASE64:-}" ]; then
|
|
echo "ERROR: FDROID_KEYSTORE_BASE64 / FDROID_CONFIG_BASE64 secrets are not set." >&2
|
|
echo "Refusing to continue — will not auto-generate a new repo key." >&2
|
|
exit 1
|
|
fi
|
|
echo "$FDROID_KEYSTORE_BASE64" | base64 --decode > fdroid/keystore.p12
|
|
echo "$FDROID_CONFIG_BASE64" | base64 --decode > fdroid/config.yml
|
|
test -s fdroid/keystore.p12
|
|
test -s fdroid/config.yml
|
|
mkdir -p fdroid/repo/icons
|
|
|
|
- name: Copy new APK to repo
|
|
if: env.IS_RELEASE == 'true'
|
|
run: |
|
|
set -e
|
|
mkdir -p fdroid/repo
|
|
cp app/build/outputs/apk/release/app-release.apk "fdroid/repo/agendula_v${VERSION}.apk"
|
|
|
|
# Per-version "What's New": ensure this version's changelog exists in the
|
|
# fastlane tree (committed at release-cut time for the official repo; this
|
|
# regenerates it from CHANGELOG.md so the self-hosted repo never depends on
|
|
# the commit having happened). The transform below then carries it across.
|
|
- name: Ensure this version's changelog is in the fastlane tree
|
|
if: env.IS_RELEASE == 'true'
|
|
run: bash scripts/sync_changelog_to_fastlane.sh
|
|
|
|
- name: Build F-Droid metadata from fastlane (single source of truth)
|
|
run: |
|
|
mkdir -p fdroid/metadata
|
|
# App-level control file (Categories/License/links) for the self-hosted
|
|
# repo's `fdroid update`.
|
|
cp fdroid-metadata/de.jeanlucmakiola.agendula.yml fdroid/metadata/
|
|
# Localized text + graphics + per-version changelogs come from the SAME
|
|
# fastlane tree the official F-Droid repo harvests from source,
|
|
# transformed into the F-Droid repo "localized" layout. One source of
|
|
# truth, both channels.
|
|
bash scripts/fastlane_to_fdroid_localized.sh \
|
|
fastlane/metadata/android \
|
|
fdroid/metadata/de.jeanlucmakiola.agendula
|
|
|
|
- name: Generate F-Droid Index
|
|
run: |
|
|
cd fdroid
|
|
fdroid update -c
|
|
|
|
- name: Upload repo/ to Hetzner
|
|
env:
|
|
HOST: ${{ secrets.HETZNER_HOST }}
|
|
USER: ${{ secrets.HETZNER_USER }}
|
|
PASS: ${{ secrets.HETZNER_PASS }}
|
|
run: |
|
|
set -euo pipefail
|
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20"
|
|
sshpass -p "$PASS" sftp $SSH_OPTS "$USER@$HOST" <<'SFTP'
|
|
-mkdir dev
|
|
-mkdir dev/fdroid
|
|
SFTP
|
|
# Publish the signed repo/ plus metadata/ (descriptions, screenshots,
|
|
# per-version changelogs) so changelog history survives across
|
|
# releases. keystore.p12 and config.yml are NEVER uploaded.
|
|
sshpass -p "$PASS" scp $SSH_OPTS -r fdroid/repo fdroid/metadata "$USER@$HOST:dev/fdroid/"
|
|
|
|
# The APK is published and the index re-signed — now record the release.
|
|
# Creating it with target_commitish makes Gitea create the vX.Y.Z tag at
|
|
# this commit, so the tag only ever marks a fully-shipped release (and a
|
|
# failure before here leaves no tag, so re-running the workflow retries).
|
|
- name: Create tag + Gitea release
|
|
if: env.IS_RELEASE == 'true'
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
set -e
|
|
TAG="v$VERSION"
|
|
# Notes = this version's CHANGELOG section.
|
|
awk -v ver="$VERSION" '
|
|
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
|
|
/^## \[/ { flag = 0 }
|
|
flag' CHANGELOG.md > release-notes.md
|
|
sed -i -e '/./,$!d' release-notes.md
|
|
if [ ! -s release-notes.md ]; then
|
|
echo "_No changelog entry for ${VERSION} — see CHANGELOG.md._" > release-notes.md
|
|
fi
|
|
python3 - "$TAG" "$SHA" "$PRERELEASE" <<'PY' > payload.json
|
|
import json, sys
|
|
print(json.dumps({
|
|
"tag_name": sys.argv[1],
|
|
"target_commitish": sys.argv[2],
|
|
"name": sys.argv[1],
|
|
"body": open("release-notes.md").read(),
|
|
"draft": False,
|
|
# Pre-1.0 releases are flagged as pre-releases (see detect job).
|
|
"prerelease": sys.argv[3] == "true",
|
|
}))
|
|
PY
|
|
# Upsert (re-run safe): PATCH if a release for the tag already exists,
|
|
# else POST a new one (which also creates the tag at target_commitish).
|
|
curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" > existing.json
|
|
ID=$(jq -r '.id // empty' existing.json 2>/dev/null || true)
|
|
if [ -n "$ID" ]; then
|
|
CODE=$(curl -s -o response.json -w '%{http_code}' -X PATCH \
|
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
-d @payload.json "$API/releases/$ID")
|
|
OK=200
|
|
else
|
|
CODE=$(curl -s -o response.json -w '%{http_code}' -X POST \
|
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
-d @payload.json "$API/releases")
|
|
OK=201
|
|
fi
|
|
cat response.json
|
|
if [ "$CODE" != "$OK" ]; then
|
|
echo "Release upsert failed with HTTP $CODE (expected $OK)" >&2
|
|
exit 1
|
|
fi
|
|
echo "Created/updated release $TAG at $SHA"
|
|
|
|
# Archive the R8 mapping so user crash stacktraces stay deobfuscatable.
|
|
# Attached to the release (it's not an APK, so it fits the no-binaries
|
|
# rule). Best-effort: never fail a release over it.
|
|
- name: Attach R8 mapping to Gitea release
|
|
if: env.IS_RELEASE == 'true'
|
|
continue-on-error: true
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
|
run: |
|
|
set -e
|
|
MAP="app/build/outputs/mapping/release/mapping.txt"
|
|
if [ ! -f "$MAP" ]; then echo "No mapping.txt (R8 off?) — skipping."; exit 0; fi
|
|
TAG="v$VERSION"
|
|
ASSET="mapping-${VERSION}.txt.gz"
|
|
gzip -c "$MAP" > "/tmp/$ASSET"
|
|
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
|
if [ -z "$ID" ]; then echo "Could not resolve release id — skipping."; exit 0; fi
|
|
# Replace any prior asset of the same name (re-run safe).
|
|
OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
|
|
| jq -r --arg n "$ASSET" '.[] | select(.name==$n) | .id')
|
|
[ -n "$OLD" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$OLD" >/dev/null || true
|
|
curl -s -X POST -H "Authorization: token $TOKEN" \
|
|
-F "attachment=@/tmp/$ASSET" \
|
|
"$API/releases/$ID/assets?name=$ASSET" -o /dev/null -w "asset upload HTTP %{http_code}\n"
|
|
|
|
# Mirror the release to the Codeberg mirror as a direct-download channel
|
|
# for users who don't want F-Droid. Gitea already push-mirrors branches +
|
|
# tags to Codeberg, but releases aren't git objects so they don't sync —
|
|
# we create the release there over the API and attach the signed APK plus
|
|
# a SHA-256 checksum. The APK is identical to the F-Droid one (same app
|
|
# key), so this adds no trust surface. Best-effort: a Codeberg outage
|
|
# (it 504s under load) must never fail an already-published F-Droid
|
|
# release. Needs the CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset.
|
|
- name: Publish release to Codeberg
|
|
if: env.IS_RELEASE == 'true'
|
|
continue-on-error: true
|
|
env:
|
|
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
|
|
API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
|
|
SHA: ${{ github.sha }}
|
|
run: |
|
|
set -e
|
|
if [ -z "${TOKEN:-}" ]; then
|
|
echo "CODEBERG_RELEASE_TOKEN not set — skipping Codeberg publish."
|
|
exit 0
|
|
fi
|
|
TAG="v$VERSION"
|
|
APK="app/build/outputs/apk/release/app-release.apk"
|
|
if [ ! -f "$APK" ]; then echo "No release APK found — skipping." >&2; exit 1; fi
|
|
ASSET_APK="agendula_v${VERSION}.apk"
|
|
ASSET_SUM="${ASSET_APK}.sha256"
|
|
cp "$APK" "/tmp/$ASSET_APK"
|
|
( cd /tmp && sha256sum "$ASSET_APK" > "$ASSET_SUM" )
|
|
|
|
# Release notes: reuse the section extracted for the Gitea release,
|
|
# fall back to the CHANGELOG entry if that step's file is gone.
|
|
if [ ! -s release-notes.md ]; then
|
|
awk -v ver="$VERSION" '
|
|
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
|
|
/^## \[/ { flag = 0 }
|
|
flag' CHANGELOG.md > release-notes.md
|
|
sed -i -e '/./,$!d' release-notes.md
|
|
fi
|
|
[ -s release-notes.md ] || echo "_See CHANGELOG.md for ${VERSION}._" > release-notes.md
|
|
# The pipeline creates the tag via the Gitea API, which the push mirror
|
|
# (sync_on_commit only fires on real git pushes) doesn't propagate
|
|
# promptly — so a release POST that carries a target_commitish can
|
|
# outrun the mirror and 500 on a commit/tag Codeberg hasn't received.
|
|
# Push the tag straight to Codeberg so it's guaranteed present, then
|
|
# attach the release to that existing tag with NO target_commitish
|
|
# (which is what triggered the 500).
|
|
git tag -f "$TAG" "$SHA"
|
|
git push -f "https://jlmakiola:${TOKEN}@codeberg.org/jlmakiola/agendula.git" \
|
|
"refs/tags/$TAG"
|
|
python3 - "$TAG" "$PRERELEASE" <<'PY' > cb-payload.json
|
|
import json, sys
|
|
tag, pre = sys.argv[1:3]
|
|
print(json.dumps({
|
|
"tag_name": tag,
|
|
"name": tag,
|
|
"body": open("release-notes.md").read(),
|
|
"draft": False,
|
|
# Pre-1.0 releases are flagged as pre-releases (see detect job).
|
|
"prerelease": pre == "true",
|
|
}))
|
|
PY
|
|
# Upsert (re-run safe).
|
|
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
|
if [ -n "$ID" ]; then
|
|
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
-d @cb-payload.json "$API/releases/$ID"
|
|
else
|
|
curl -s -o cb-response.json -w "release POST HTTP %{http_code}\n" -X POST \
|
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
|
-d @cb-payload.json "$API/releases"
|
|
ID=$(jq -r '.id // empty' cb-response.json 2>/dev/null || true)
|
|
fi
|
|
if [ -z "$ID" ]; then echo "Could not resolve Codeberg release id." >&2; exit 1; fi
|
|
|
|
# Attach APK + checksum, replacing any prior asset of the same name.
|
|
for A in "$ASSET_APK" "$ASSET_SUM"; do
|
|
OLD=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/$ID/assets" \
|
|
| jq -r --arg n "$A" '.[] | select(.name==$n) | .id')
|
|
[ -n "$OLD" ] && curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$ID/assets/$OLD" >/dev/null || true
|
|
curl -s -X POST -H "Authorization: token $TOKEN" \
|
|
-F "attachment=@/tmp/$A" \
|
|
"$API/releases/$ID/assets?name=$A" -o /dev/null -w "asset $A HTTP %{http_code}\n"
|
|
done
|
|
echo "Published $TAG to Codeberg."
|