diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index 88a4e4f..0000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,330 +0,0 @@ -# Build and publish a postit-debian release on the Forgejo instance. -# -# Triggered by a push of a git tag. Validates the tag/changelog pair, -# builds the .deb for amd64 and arm64 (sequential cross-RID .NET -# publishes on a single amd64 runner container — matrix is not used -# here because the runner image pazof/yavsc-build-env has no Node, -# so actions/upload-artifact and actions/download-artifact (which -# require Node) cannot be used to pass the .deb files between jobs. -# All in one job, like yavsc's .forgejo/workflows/release.yml.), -# then publishes a Forgejo release via the REST API and uploads both -# .deb files as assets. -# -# Authentication: the runner auto-provides a token scoped to the -# repository. We read it once into the local env var FORGEJO_TOKEN -# and never reference the runtime-level name again. -# -# Why bash + jq + curl, no third-party actions: the runner's docker -# label points at pazof/yavsc-build-env, a Debian image with jq but -# without Node.js or python3. Any action like actions/checkout, -# rasterstate/forgejo-release-action, actions/upload-artifact, -# actions/download-artifact, etc. fails with "executable file not -# found in $PATH". Same constraint as yavsc's -# .forgejo/workflows/release.yml. -# -# Re-tag policy (cf. AGENTS.md "Re-tag = le mal") : on push de tag -# ou dispatch, on *réutilise* la release existante (via PATCH) au -# lieu d'en créer une nouvelle. Un tag Git pointe vers un commit -# fixe ; si le binaire change (rebuild après modif du packaging), -# on met à jour la release existante plutôt que d'en multiplier -# pour un même tag. Le permalien /releases/tag/ reste stable. -# -# Inter-step state: we persist values between steps via a plain -# env file under /tmp, sourced at the top of each step that needs -# it. This keeps the workflow self-contained and avoids any -# runtime variable names we did not choose. -name: Forgejo Release postit-deb - -on: - push: - tags: - - '*' - workflow_dispatch: - inputs: - tag: - description: 'Tag pazof/yavsc à packager (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 amd64 + build - # arm64 + publication via l'API REST Forgejo (pas d'actions - # tierces Node). - release: - runs-on: docker - container: - image: docker.io/pazof/yavsc-build-env:debian12-dotnet10-android36-v2 - env: - STATE_FILE: /tmp/release-state.env - steps: - - name: Installer les pré-requis de build (debhelper + icônes) - # L'image runner fournit déjà dotnet-sdk-10.0, git, jq, - # curl. On ajoute les outils spécifiques au packaging - # Debian (debhelper, imagemagick pour les icônes .png - # via `convert`, librsvg2-bin pour le SVG). - run: | - apt-get update - apt-get install -y --no-install-recommends \ - build-essential debhelper imagemagick librsvg2-bin \ - ca-certificates - rm -rf /var/lib/apt/lists/* - : > "$STATE_FILE" - - - name: Clone du repo au tag demandé - env: - TAG: ${{ forgejo.event_name == 'push' && forgejo.ref_name || inputs.tag }} - run: | - if [[ -z "$TAG" ]]; then - echo "::error::No tag provided. In workflow_dispatch, set the 'tag' input." - exit 1 - fi - - cd /src - if [[ ! -d _src/.git ]]; then - # Clone unshallow pour préserver l'historique — utile - # si un futur test en a besoin. Le coût est marginal - # pour ce repo (< 50 commits). - git clone https://forgejo.pschneider.fr/notazof/postit-debian.git _src - fi - - cd _src - git fetch --tags --force --prune origin - git checkout "$TAG" - - echo "Checked out at $(git rev-parse HEAD) on tag $TAG" - echo "TAG=$TAG" >> "$STATE_FILE" - - - name: Valider le tag et la section CHANGELOG - run: | - source "$STATE_FILE" - cd /src/_src - 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. - 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 garde le titre - # (ligne `## [TAG] - channel`) pour la vérification du - # canal, puis on l'exclut du body envoyé à la release. - 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 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 - - RELEASE_BODY=$(echo "$BODY" | tail -n +2) - IS_PRERELEASE=$([ "$CHANNEL" = "stable" ] && echo false || echo true) - - echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" - - # Persist values for the next steps via our local state file. - { - echo "RELEASE_BODY<> "$STATE_FILE" - - - name: Build .deb amd64 - env: - POSTIT_RUNTIME: linux-x64 - run: | - source "$STATE_FILE" - cd /src/_src - echo "→ Building amd64 for POSTIT_GIT_TAG=$TAG" - make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-x64 - - - name: Build .deb arm64 - env: - POSTIT_RUNTIME: linux-arm64 - run: | - source "$STATE_FILE" - cd /src/_src - echo "→ Building arm64 for POSTIT_GIT_TAG=$TAG" - # Cross-RID .NET depuis un hôte amd64 : standard, pas - # besoin de runner arm64 natif. - make deb POSTIT_GIT_TAG="$TAG" POSTIT_RUNTIME=linux-arm64 - - - name: Localiser les .deb produits - run: | - source "$STATE_FILE" - cd /src - DEB_AMD64=$(find . -maxdepth 3 -name "postit_*${TAG}-1_amd64.deb" \ - -not -path "./_src/debian/*" -printf '%p\n' | head -1) - DEB_ARM64=$(find . -maxdepth 3 -name "postit_*${TAG}-1_arm64.deb" \ - -not -path "./_src/debian/*" -printf '%p\n' | head -1) - if [[ -z "$DEB_AMD64" || -z "$DEB_ARM64" ]]; then - echo "::error::Missing .deb files. amd64='$DEB_AMD64' arm64='$DEB_ARM64'" - ls -la /src/ 2>/dev/null || true - exit 1 - fi - echo "DEB_AMD64=/src/$DEB_AMD64" >> "$STATE_FILE" - echo "DEB_ARM64=/src/$DEB_ARM64" >> "$STATE_FILE" - echo "✓ Found both .deb files" - - - name: Publier la release Forgejo via l'API REST - env: - FORGEJO_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - FORGEJO_API_URL: ${{ forgejo.api_url }} - FORGEJO_REPOSITORY: ${{ forgejo.repository }} - run: | - source "$STATE_FILE" - if [[ -z "$TAG" ]]; then - echo "::error::No tag resolved for the API call." - exit 1 - fi - - # Le runner Forgejo expose l'API sur forgejo.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="${FORGEJO_API_URL%/}" - API_BASE="${API_BASE%/api/v1}" - - # 1. Vérifier si la release existe déjà pour ce tag. - # Politique : on réutilise (PATCH) plutôt que d'en - # créer une nouvelle — cf. note "Re-tag policy" en - # tête de fichier. - echo "::group::Check existing release for tag $TAG" - HTTP=$(curl -sS -o /tmp/existing.json -w '%{http_code}' \ - -H "Authorization: token $FORGEJO_TOKEN" \ - -H "Accept: application/json" \ - "$API_BASE/api/v1/repos/$FORGEJO_REPOSITORY/releases/tags/$TAG") - echo "GET releases/tags/$TAG -> HTTP $HTTP" - EXISTING_ID="" - if [[ "$HTTP" == "200" ]]; then - EXISTING_ID=$(jq -r '.id // empty' /tmp/existing.json) - echo "Existing release id: ${EXISTING_ID:-none}" - fi - echo "::endgroup::" - - # 2. Créer ou mettre à jour la release. - if [[ -n "$EXISTING_ID" ]]; then - echo "::group::Update release id=$EXISTING_ID" - jq -n \ - --arg body "$RELEASE_BODY" \ - --argjson prerelease "$IS_PRERELEASE" \ - '{body: $body, prerelease: $prerelease}' \ - > /tmp/patch.json - HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ - -X PATCH \ - -H "Authorization: token $FORGEJO_TOKEN" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - --data-binary @/tmp/patch.json \ - "$API_BASE/api/v1/repos/$FORGEJO_REPOSITORY/releases/$EXISTING_ID") - echo "PATCH release -> HTTP $HTTP" - echo "::endgroup::" - else - echo "::group::Create release" - jq -n \ - --arg tag "$TAG" \ - --arg name "$TAG" \ - --arg body "$RELEASE_BODY" \ - --argjson prerelease "$IS_PRERELEASE" \ - '{tag_name: $tag, name: $name, body: $body, prerelease: $prerelease}' \ - > /tmp/post.json - HTTP=$(curl -sS -o /tmp/release.json -w '%{http_code}' \ - -X POST \ - -H "Authorization: token $FORGEJO_TOKEN" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - --data-binary @/tmp/post.json \ - "$API_BASE/api/v1/repos/$FORGEJO_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=$(jq -r '.id' /tmp/release.json) - echo "Release id=$RELEASE_ID" - - # 3. Upload les .deb en assets. Le nom du fichier passe - # en query string (?name=...), pas en argument - # positionnel entre --data-binary et l'URL. - for entry in "amd64:$DEB_AMD64" "arm64:$DEB_ARM64"; do - arch="${entry%%:*}" - deb="${entry#*:}" - echo "::group::Upload asset for arch=$arch: $deb" - HTTP=$(curl -sS -o /tmp/asset.json -w '%{http_code}' \ - -X POST \ - -H "Authorization: token $FORGEJO_TOKEN" \ - -H "Content-Type: application/octet-stream" \ - -H "Accept: application/json" \ - --data-binary "@$deb" \ - "$API_BASE/api/v1/repos/$FORGEJO_REPOSITORY/releases/$RELEASE_ID/assets?name=$(basename "$deb")") - echo "POST asset ($arch) -> HTTP $HTTP" - echo "::endgroup::" - - if [[ "$HTTP" != "201" ]]; then - echo "::error::Asset upload failed for $arch (HTTP $HTTP):" - cat /tmp/asset.json - exit 1 - fi - done - - echo "Release publiée : $API_BASE/$FORGEJO_REPOSITORY/releases/tag/$TAG" diff --git a/.github/workflows/build-and-release-deb.yml b/.github/workflows/build-and-release-deb.yml new file mode 100644 index 0000000..c485ef6 --- /dev/null +++ b/.github/workflows/build-and-release-deb.yml @@ -0,0 +1,283 @@ +name: Build and Release postit-deb + +on: + push: + branches: + - main + tags: + - '*' + workflow_dispatch: + inputs: + tag: + description: 'Tag de pazof/yavsc à packager (ex. 1.0.6, 1.0.7-rc1). Requis pour un build ad-hoc.' + 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 + force_republish: + description: 'Re-publier une release dont le tag existe déjà. Par défaut refusé (re-tag = le mal).' + required: false + type: boolean + default: false + +# softprops/action-gh-release a besoin de contents: write +# pour publier une release + uploader un asset. +permissions: + contents: write + +jobs: + # Build matrix : un .deb par architecture. Le tag Git poussé sur + # ce dépôt devient POSTIT_GIT_TAG pour `make deb`, qui clone + # l'amont pazof/yavsc à ce tag et produit le .deb correspondant. + # Sur amd64, la cross-compilation linux-arm64 marche nativement + # (dotnet publish --runtime linux-arm64 depuis un hôte amd64). + # On évite donc les runners arm64 natifs (qui existent mais sont + # récents et plus chers en minutes). + deb-build: + name: Build .deb (${{ matrix.runtime }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - runtime: linux-x64 + arch: amd64 + artifact_name: postit-amd64 + - runtime: linux-arm64 + arch: arm64 + artifact_name: postit-arm64 + steps: + - name: Checkout postit-debian + uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Installer les pré-requis de build (debhelper + icônes) + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential debhelper imagemagick librsvg2-bin \ + git ca-certificates + + - name: Installer .NET SDK 10 + uses: microsoft/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Déterminer POSTIT_GIT_TAG + id: tag + run: | + # Sur un push de branche (pas un tag), github.ref_name est + # 'main' — `make deb POSTIT_GIT_TAG=main` clone pazof/yavsc + # sur la branche main et produit un .deb à jour. Sur un push + # de tag, c'est le numéro de tag (ex. '1.0.6'). Sur + # workflow_dispatch, on lit l'input `tag`. + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + TAG="${{ inputs.tag }}" + else + TAG="${{ github.ref_name }}" + fi + if [[ -z "$TAG" ]]; then + echo "::error::POSTIT_GIT_TAG is empty. Pour workflow_dispatch, l'input 'tag' est obligatoire." + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "→ POSTIT_GIT_TAG=$TAG" + + - name: Build du .deb via make deb + env: + POSTIT_GIT_TAG: ${{ steps.tag.outputs.tag }} + POSTIT_RUNTIME: ${{ matrix.runtime }} + run: | + echo "→ Building for POSTIT_GIT_TAG=$POSTIT_GIT_TAG POSTIT_RUNTIME=$POSTIT_RUNTIME" + make deb POSTIT_GIT_TAG="$POSTIT_GIT_TAG" POSTIT_RUNTIME="$POSTIT_RUNTIME" + + - name: Localiser le .deb produit + id: locate + run: | + # Le Makefile mv les .deb vers $POSTIT_OUT_DIR (par défaut + # le répertoire parent du repo). Sur GitHub Actions, c'est + # le workspace parent : /home/runner/work/.. Le .deb est + # nommé d'après le tag brut (avec ou sans 'v', tel quel + # poussé sur le remote), on cherche donc avec ref_name. + DEB=$(find /home/runner -maxdepth 4 -name "postit_*${{ github.ref_name }}-1_${{ matrix.arch }}.deb" \ + -not -path "*/debian/*" \ + -printf '%p\n' | head -1) + if [[ -z "$DEB" ]]; then + echo "::error::No .deb matching postit_*${{ github.ref_name }}-1_${{ matrix.arch }}.deb found." + echo "Files in parent dir:" + ls -la /home/runner/work/ 2>/dev/null || true + exit 1 + fi + echo "deb_path=$DEB" >> "$GITHUB_OUTPUT" + echo "✓ Found $DEB" + + - name: Téléverser le .deb en tant qu'Artéfact GitHub + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact_name }} + path: ${{ steps.locate.outputs.deb_path }} + retention-days: 7 + + # Validation : parse le tag, applique la parité patch (pair=stable / + # impair=preview / suffixe=instable), vérifie que CHANGELOG.md + # contient une section cohérente, et — point non négociable — + # refuse de re-publier un tag qui existe déjà (re-tag = le mal). + validate-release: + # Tourne sur push de tag (release officielle) ou sur workflow_dispatch + # avec un tag explicite (release ad-hoc). Sur push de branche, on + # ne publie pas — les jobs de build suffisent (artefacts seulement). + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Checkout postit-debian + uses: actions/checkout@v7 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Déterminer le tag à publier + id: pick_tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + TAG="${{ inputs.tag }}" + else + TAG="${{ github.ref_name }}" + fi + if [[ -z "$TAG" ]]; then + echo "::error::Tag is empty. Sur workflow_dispatch, l'input 'tag' est obligatoire." + exit 1 + fi + # Strip leading 'v' (git tag convention). + if [[ "$TAG" =~ ^v(.*)$ ]]; then + TAG="${BASH_REMATCH[1]}" + echo "Stripped leading 'v' — using TAG=$TAG for validation." + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Valider le tag, le CHANGELOG et l'unicité du tag + env: + FORCE_UNSTABLE: ${{ inputs.force_unstable || github.event.inputs.force_unstable || 'false' }} + FORCE_REPUBLISH: ${{ inputs.force_republish || github.event.inputs.force_republish || 'false' }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG="${{ steps.pick_tag.outputs.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. + 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]. awk en mode paragraphe. + 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é. + HEADER=$(grep -m1 "^## \[$TAG\]" CHANGELOG.md) + if [[ "$HEADER" != *" - $CHANNEL"* ]]; then + echo "::error::Section '## [$TAG]' must declare suffix '- $CHANNEL' to match tag parity." + echo "Current section header: $HEADER" + exit 1 + fi + + echo "Section CHANGELOG validée pour [$TAG] - $CHANNEL" + + # Anti-re-tag : refuse de publier si une release existe déjà + # pour ce tag. softprops/action-gh-release créerait sinon une + # nouvelle release par-dessus (re-tag = le mal). Opt-in via + # workflow_dispatch + force_republish=true uniquement. + if gh release view "$TAG" >/dev/null 2>&1; then + if [[ "$FORCE_REPUBLISH" != "true" ]]; then + echo "::error::Release for tag '$TAG' already exists. Refusing to re-tag." + echo "Set force_republish=true via workflow_dispatch to override." + exit 1 + else + echo "::warning::Release '$TAG' already exists — force_republish=true, proceeding." + fi + else + echo "✓ No existing release for tag '$TAG'." + fi + + # Exposition aux étapes suivantes via $GITHUB_ENV. + { + echo "RELEASE_BODY<> "$GITHUB_ENV" + + publish-release: + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + needs: [deb-build, validate-release] + runs-on: ubuntu-latest + steps: + - name: Récupérer les .deb depuis les artefacts + uses: actions/download-artifact@v7 + with: + path: ./ + merge-multiple: true + + - name: Lister les .deb téléchargés + run: ls -la ./ + + - name: Publier la release GitHub et uploader les .deb + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.RELEASE_TAG }} + files: | + ./postit-amd64/*.deb + ./postit-arm64/*.deb + body: ${{ env.RELEASE_BODY }} + prerelease: ${{ env.IS_PRERELEASE }}