yavsc/.forgejo/workflows/release.yml
Paul Schneider 80cb8c46fc
ci(forgejo): use runner-provided GITHUB_TOKEN for release workflow
Repo-level secrets creation is broken on this Forgejo instance
(InsertEncryptedSecret fails with UTF-8 byte-sequence error, likely
a text-vs-bytea column type on the secret table). The fix is in
upstream Forgejo v16; until then, ${{ secrets.GITHUB_TOKEN }} (auto-
provided by the runner, scoped to contents: write for the current
repo) keeps the release workflow operational without any UI setup.

When the instance is upgraded and the secret table is migrated,
revert this commit to switch back to ${{ secrets.RELEASE_TOKEN }}
for least-privilege.
2026-08-17 01:23:44 +01:00

231 lines
No EOL
8.8 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 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 }}