yavsc/.forgejo/workflows/release.yml

231 lines
8.8 KiB
YAML
Raw Normal View History

# 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 rasterstate/forgejo-release-action 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.
#
# 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:
# Parse le tag, applique la règle de parité du patch
# (pair=stable / impair=preview / suffixe=unstable), fail-fast sur
# instable sauf opt-in, et vérifie que CHANGELOG.md contient une
# section `## [TAG] - <channel>` cohérente. Le body est extrait
# dans un artifact consommé par le job release.
validate-release:
runs-on: docker
steps:
- name: Checkout du code
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Valider le tag et la section CHANGELOG
env:
# En push tag : github.ref_name est le tag.
# En workflow_dispatch : on lit l'input 'tag' (obligatoire).
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
# 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" != "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.
BODY=$(awk -v tag="[$TAG]" '
/^## \[/ {
if (in_section) exit
if (index($0, tag) > 0) in_section=1
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".
if [[ "$BODY" != *" - $CHANNEL"* ]]; then
echo "::error::Section '## [$TAG]' must declare suffix '- $CHANNEL' to match tag parity."
echo "Current section body (first 5 lines):"
echo "$BODY" | head -5
exit 1
fi
echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL"
# Écrit le body dans un fichier pour transmission via artifact.
# Le body est multi-ligne, donc artifact > heredoc $GITHUB_ENV.
mkdir -p release-body
printf '%s\n' "$BODY" > release-body/body.md
- name: Uploader le body de la release comme artifact
uses: actions/upload-artifact@v7
with:
name: release-body
path: release-body/body.md
retention-days: 1
# Construit l'APK via le Dockerfile (stage build-env), puis publie
# la release Forgejo avec le body validé et l'APK en asset.
release:
needs: validate-release
runs-on: docker
steps:
- name: Checkout du code
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Checkout du tag (workflow_dispatch uniquement)
# En push tag, le runner checkout déjà au bon commit.
# En workflow_dispatch, on checkout explicitement le tag demandé
# pour que l'APK soit bien construit depuis ce commit.
if: github.event_name == 'workflow_dispatch'
env:
TAG: ${{ inputs.tag }}
run: |
if [[ -z "$TAG" ]]; then
echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input."
exit 1
fi
git checkout "$TAG"
- name: Build de l'image Docker (stage build-env uniquement)
run: 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 ./PostIt.Android.apk
docker rm extractor
- name: Récupérer le body validé
uses: actions/download-artifact@v7
with:
name: release-body
path: release-body
- name: Calculer le canal (stable / preview / unstable) depuis le tag
# On re-parse le tag ici plutôt que de transporter le channel
# via artifact. Le calcul est trivial (parité du patch + suffixe)
# et reste ainsi explicite.
id: set-channel
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 }}
run: |
if [[ -z "$TAG" ]]; then
echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input."
exit 1
fi
if [[ ! "$TAG" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-.*)?$ ]]; then
echo "::error::Tag '$TAG' does not match MAJOR.MINOR.PATCH[-SUFFIX] format."
exit 1
fi
PATCH="${BASH_REMATCH[3]}"
SUFFIX="${BASH_REMATCH[4]}"
if [[ -n "$SUFFIX" ]]; then
CHANNEL="unstable"
elif (( PATCH % 2 == 0 )); then
CHANNEL="stable"
else
CHANNEL="preview"
fi
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
echo "is_prerelease=$([[ $CHANNEL != stable ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- name: Publier la release Forgejo et uploader l'APK
uses: https://rasterhub.com/rasterstate/forgejo-release-action@v1
with:
# tag_name defaults to the pushed tag (GITHUB_REF_NAME).
body_path: release-body/body.md
# Stable -> Latest (false).
# Preview et Unstable -> prerelease (true).
prerelease: ${{ steps.set-channel.outputs.is_prerelease }}
files: |
PostIt.Android.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}