The previous awk extracted the section body but excluded the title line (## [TAG] - channel), so the '* - $CHANNEL*' pattern never matched. Fix: include the title line in the extracted body, verify the channel suffix on the title, then strip the title before passing the body to the release API.
273 lines
No EOL
12 KiB
YAML
273 lines
No EOL
12 KiB
YAML
# Build and publish a release on the Forgejo source-of-truth instance
|
|
# with the PostIt Android APK as an attached asset.
|
|
#
|
|
# Triggered by a push of a git tag. Validates the tag/changelog pair,
|
|
# builds the APK using the existing Dockerfile (--target build-env), then
|
|
# publishes a Forgejo release via the Forgejo REST API and uploads the
|
|
# APK as an asset.
|
|
#
|
|
# Authentication uses ${{ secrets.GITHUB_TOKEN }} (auto-provided by the
|
|
# Forgejo runner, scoped to contents: write for the current repo). A
|
|
# dedicated PAT (${{ secrets.RELEASE_TOKEN }}) was the preferred option
|
|
# for least-privilege, but creating repo-level secrets is currently
|
|
# broken on this Forgejo instance (InsertEncryptedSecret fails with a
|
|
# UTF-8 byte-sequence error, probably a text-vs-bytea column type on
|
|
# the secret table). Bumping to Forgejo v16 should fix it; until then,
|
|
# the runner-provided token keeps the workflow operational.
|
|
#
|
|
# Why bash + curl, no third-party actions: the runner's docker label
|
|
# points at pazof/yavsc-build-env, a Debian image without Node.js. Any
|
|
# action like actions/checkout, rasterstate/forgejo-release-action, etc.
|
|
# fails with "executable file not found in $PATH". Same constraint as
|
|
# .forgejo/workflows/buildAndTest.yml.
|
|
#
|
|
# This workflow complements .github/workflows/docker-publish-android.yml
|
|
# which targets the GitHub mirror; the validate-release logic mirrors
|
|
# the GitHub-side job so the two channels stay consistent.
|
|
name: Forgejo Release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- '*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Tag à publier (requis en dispatch, ex. 1.0.6 ou 1.0.7-rc1).'
|
|
required: true
|
|
type: string
|
|
force_unstable:
|
|
description: 'Publier une release avec suffixe (ex. 1.0.0-rc1) malgré le fail-fast par défaut.'
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
# Job unique : validation tag/CHANGELOG + build APK + publication
|
|
# via l'API REST Forgejo (pas d'actions tierces Node).
|
|
release:
|
|
runs-on: docker
|
|
steps:
|
|
- name: Clone du repo au tag demandé
|
|
env:
|
|
# En push tag : github.ref_name est le tag.
|
|
# En workflow_dispatch : on lit l'input 'tag'.
|
|
TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
|
|
FORCE_UNSTABLE: ${{ inputs.force_unstable || 'false' }}
|
|
run: |
|
|
if [[ -z "$TAG" ]]; then
|
|
echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input."
|
|
exit 1
|
|
fi
|
|
|
|
# WORKDIR de l'image (cf. dotnet-android-build-image/Dockerfile).
|
|
cd /src
|
|
|
|
# Clone unshallow pour que GitVersion.MsBuild ait l'historique
|
|
# et les tags (sinon MSB3073 sur la cible Android cf. PR #21).
|
|
if [[ ! -d _src/.git ]]; then
|
|
git clone https://forgejo.pschneider.fr/notazof/yavsc.git _src
|
|
fi
|
|
|
|
cd _src
|
|
git fetch --tags --force --prune origin
|
|
git checkout "$TAG"
|
|
|
|
echo "Checked out at $(git rev-parse HEAD) on $(git describe --tags --always 2>/dev/null || echo unknown)"
|
|
|
|
- name: Valider le tag et la section CHANGELOG
|
|
run: |
|
|
cd /src/_src
|
|
TAG="$(git describe --tags --exact-match HEAD 2>/dev/null || git rev-parse --short HEAD)"
|
|
echo "Validating tag $TAG"
|
|
|
|
# Parse semver : MAJOR.MINOR.PATCH[-SUFFIX]
|
|
if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then
|
|
echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format."
|
|
exit 1
|
|
fi
|
|
|
|
MAJOR="${BASH_REMATCH[1]}"
|
|
MINOR="${BASH_REMATCH[2]}"
|
|
PATCH="${BASH_REMATCH[3]}"
|
|
SUFFIX="${BASH_REMATCH[4]}"
|
|
|
|
# Classification du canal par parité du patch.
|
|
# Patch pair + pas de suffixe -> stable.
|
|
# Patch impair + pas de suffixe -> preview.
|
|
# Suffixe présent -> instable.
|
|
if [[ -n "$SUFFIX" ]]; then
|
|
CHANNEL="unstable"
|
|
elif (( PATCH % 2 == 0 )); then
|
|
CHANNEL="stable"
|
|
else
|
|
CHANNEL="preview"
|
|
fi
|
|
|
|
echo "Tag $TAG classifié comme channel=$CHANNEL"
|
|
|
|
# Fail-fast sur instable sauf opt-in explicite.
|
|
if [[ "$CHANNEL" == "unstable" && "${FORCE_UNSTABLE:-false}" != "true" ]]; then
|
|
echo "::error::Tag '$TAG' is unstable (suffix '$SUFFIX'). Refusing to publish."
|
|
echo "Set force_unstable=true via workflow_dispatch to override."
|
|
exit 1
|
|
fi
|
|
|
|
# Lecture du CHANGELOG.md (doit exister à la racine du repo).
|
|
if [[ ! -f CHANGELOG.md ]]; then
|
|
echo "::error::CHANGELOG.md not found at repo root."
|
|
exit 1
|
|
fi
|
|
|
|
# Extraction de la section [TAG]. On cherche la première ligne
|
|
# commençant par '## [' qui contient '[TAG]' (entre '## [' et
|
|
# la prochaine ligne '## [' ou fin de fichier). awk en mode
|
|
# paragraphe suffit et reste POSIX. On garde aussi le titre
|
|
# (ligne `## [TAG] - channel`) pour la vérification du canal.
|
|
BODY=$(awk -v tag="[$TAG]" '
|
|
/^## \[/ {
|
|
if (in_section) exit
|
|
if (index($0, tag) > 0) {
|
|
in_section=1
|
|
print
|
|
next
|
|
}
|
|
}
|
|
in_section { print }
|
|
' CHANGELOG.md)
|
|
|
|
if [[ -z "$BODY" ]]; then
|
|
echo "::error::No section matching '## [$TAG]' found in CHANGELOG.md."
|
|
echo "Add a '## [$TAG] - $CHANNEL' section before tagging."
|
|
exit 1
|
|
fi
|
|
|
|
# Vérification cohérence du canal déclaré dans le suffixe.
|
|
# Format attendu : "## [TAG] - stable" / "- preview" / "- unstable".
|
|
# On lit la première ligne du body qui contient le titre.
|
|
TITLE=$(echo "$BODY" | head -1)
|
|
if [[ "$TITLE" != *" - $CHANNEL"* ]]; then
|
|
echo "::error::Section title '$TITLE' must declare suffix '- $CHANNEL' to match tag parity."
|
|
exit 1
|
|
fi
|
|
|
|
# Body pour la release : retire la première ligne (titre).
|
|
BODY=$(echo "$BODY" | tail -n +2)
|
|
|
|
echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL"
|
|
|
|
# Expose channel + body pour les étapes suivantes via $GITHUB_ENV.
|
|
echo "RELEASE_CHANNEL=$CHANNEL" >> "$GITHUB_ENV"
|
|
echo "RELEASE_BODY<<EOF" >> "$GITHUB_ENV"
|
|
echo "$BODY" >> "$GITHUB_ENV"
|
|
echo "EOF" >> "$GITHUB_ENV"
|
|
echo "IS_PRERELEASE=$([ "$CHANNEL" = "stable" ] && echo false || echo true)" >> "$GITHUB_ENV"
|
|
|
|
- name: Build de l'image Docker (stage build-env uniquement)
|
|
run: cd /src/_src && docker build --build-arg ANDROID_TARGET_RID=android-arm64 --target build-env -t postit-android .
|
|
|
|
- name: Extraire l'APK signé du conteneur
|
|
run: |
|
|
docker create --name extractor postit-android
|
|
docker cp extractor:/src/src/PostIt/PostIt.Android/bin/Release/net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk /src/_src/PostIt.Android.apk
|
|
docker rm extractor
|
|
|
|
- name: Publier la release Forgejo via l'API REST
|
|
# Pas d'action tierce (pas de Node dans l'image runner).
|
|
# On parle à l'API Forgejo directement via curl.
|
|
# Docs : https://forgejo.pschneider.fr/api/swagger#/repository/release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
GITHUB_API_URL: ${{ github.api_url }}
|
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
TAG: ${{ github.event_name == 'push' && github.ref_name || inputs.tag }}
|
|
RELEASE_BODY: ${{ env.RELEASE_BODY }}
|
|
IS_PRERELEASE: ${{ env.IS_PRERELEASE }}
|
|
run: |
|
|
if [[ -z "$TAG" ]]; then
|
|
echo "::error::No tag resolved for the API call."
|
|
exit 1
|
|
fi
|
|
|
|
# Le runner Forgejo expose l'API sur github.api_url (par
|
|
# défaut http://…/api/v1). On retire le suffixe /api/v1 s'il
|
|
# est présent pour dériver la base du serveur, puis on
|
|
# reconstruit l'URL de l'API proprement.
|
|
API_BASE="${GITHUB_API_URL%/}"
|
|
API_BASE="${API_BASE%/api/v1}"
|
|
|
|
# 1. Vérifier si la release existe déjà pour ce tag.
|
|
echo "::group::Check existing release for tag $TAG"
|
|
HTTP=$(curl -sS -o /tmp/existing.json -w '%{http_code}' \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
-H "Accept: application/json" \
|
|
"$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/tags/$TAG")
|
|
echo "GET releases/tags/$TAG -> HTTP $HTTP"
|
|
EXISTING_ID=""
|
|
if [[ "$HTTP" == "200" ]]; then
|
|
EXISTING_ID=$(python3 -c "import json,sys; print(json.load(open('/tmp/existing.json')).get('id',''))" 2>/dev/null || true)
|
|
echo "Existing release id: ${EXISTING_ID:-none}"
|
|
fi
|
|
echo "::endgroup::"
|
|
|
|
# 2. Créer ou mettre à jour la release.
|
|
# On utilise python3 pour générer le body JSON proprement
|
|
# (jq n'est pas garanti dans l'image runner).
|
|
if [[ -n "$EXISTING_ID" ]]; then
|
|
echo "::group::Update release id=$EXISTING_ID"
|
|
BODY=$(IS_PRERELEASE="$IS_PRERELEASE" RELEASE_BODY="$RELEASE_BODY" python3 -c 'import json,os; print(json.dumps({"body":os.environ["RELEASE_BODY"],"prerelease":os.environ["IS_PRERELEASE"].lower()=="true"}))')
|
|
HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \
|
|
-X PATCH \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Accept: application/json" \
|
|
--data-binary "$BODY" \
|
|
"$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$EXISTING_ID")
|
|
echo "PATCH release -> HTTP $HTTP"
|
|
echo "::endgroup::"
|
|
else
|
|
echo "::group::Create release"
|
|
BODY=$(IS_PRERELEASE="$IS_PRERELEASE" RELEASE_BODY="$RELEASE_BODY" TAG="$TAG" python3 -c 'import json,os; print(json.dumps({"tag_name":os.environ["TAG"],"name":os.environ["TAG"],"body":os.environ["RELEASE_BODY"],"prerelease":os.environ["IS_PRERELEASE"].lower()=="true"}))')
|
|
HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \
|
|
-X POST \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Accept: application/json" \
|
|
--data-binary "$BODY" \
|
|
"$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases")
|
|
echo "POST release -> HTTP $HTTP"
|
|
echo "::endgroup::"
|
|
fi
|
|
|
|
if [[ "$HTTP" != "200" && "$HTTP" != "201" ]]; then
|
|
echo "::error::Release creation/update failed (HTTP $HTTP):"
|
|
cat /tmp/release.json
|
|
exit 1
|
|
fi
|
|
|
|
RELEASE_ID=$(python3 -c "import json; print(json.load(open('/tmp/release.json'))['id'])")
|
|
echo "Release id=$RELEASE_ID"
|
|
|
|
# 3. Upload l'APK en asset.
|
|
echo "::group::Upload APK asset"
|
|
HTTP=$(curl -sS -o /tmp/asset.json -w '%{http_code}' \
|
|
-X POST \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
-H "Accept: application/json" \
|
|
--data-binary "@/src/_src/PostIt.Android.apk" \
|
|
"?name=PostIt.Android.apk" \
|
|
"$API_BASE/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets")
|
|
echo "POST asset -> HTTP $HTTP"
|
|
echo "::endgroup::"
|
|
|
|
if [[ "$HTTP" != "201" ]]; then
|
|
echo "::error::Asset upload failed (HTTP $HTTP):"
|
|
cat /tmp/asset.json
|
|
exit 1
|
|
fi
|
|
|
|
echo "Release publiée: $API_BASE/$GITHUB_REPOSITORY/releases/tag/$TAG" |