diff --git a/.gitea/workflows/anno-apply-pr.yml b/.gitea/workflows/anno-apply-pr.yml index dac539a..a1a7269 100644 --- a/.gitea/workflows/anno-apply-pr.yml +++ b/.gitea/workflows/anno-apply-pr.yml @@ -22,6 +22,13 @@ concurrency: jobs: apply-approved: + # ✅ Job ne se lance QUE si le label ajouté est state/approved, ou si workflow_dispatch + if: >- + ${{ + (github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'state/approved') + || github.event_name == 'workflow_dispatch' + }} + runs-on: mac-ci container: image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm @@ -35,6 +42,7 @@ jobs: npm --version - name: Derive context (event.json / workflow_dispatch) + id: ctx env: INPUT_ISSUE: ${{ inputs.issue }} FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }} @@ -81,6 +89,8 @@ jobs: throw new Error("No issue number in event.json or workflow_dispatch input"); } + // Ici, vu job-level if, on sait déjà que c'est state/approved en issues:labeled. + // Mais on garde pour logs / workflow_dispatch. const labelName = ev?.label?.name || ev?.label || @@ -102,42 +112,35 @@ jobs: `DEFAULT_BRANCH=${sh(defaultBranch)}`, `ISSUE_NUMBER=${sh(issueNumber)}`, `LABEL_NAME=${sh(labelName)}`, - `API_BASE=${sh(apiBase)}` + `API_BASE=${sh(apiBase)}`, + `SKIP=0` ].join("\n") + "\n"); NODE echo "✅ context:" - sed -n '1,120p' /tmp/anno.env - - - name: Gate on label state/approved - run: | - set -euo pipefail - source /tmp/anno.env - - if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then - echo "ℹ️ label=$LABEL_NAME => skip" - echo "SKIP=1" >> /tmp/anno.env - exit 0 - fi - echo "✅ proceed (issue=$ISSUE_NUMBER)" + sed -n '1,160p' /tmp/anno.env - name: Fetch issue + gate on Type (skip Proposer) + id: typegate env: FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }} run: | set -euo pipefail source /tmp/anno.env - [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } - ISSUE_JSON="$(curl -fsS \ + # ✅ récupérer issue JSON dans un fichier (zéro JSON dans argv) + curl -fsS \ -H "Authorization: token $FORGE_TOKEN" \ -H "Accept: application/json" \ - "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")" + "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \ + -o /tmp/issue.json - node --input-type=module - <<'NODE' "$ISSUE_JSON" >> /tmp/anno.env - const issue = JSON.parse(process.argv[1] || "{}"); + node --input-type=module <<'NODE' >> /tmp/anno.env + import fs from "node:fs"; + + const issue = JSON.parse(fs.readFileSync("/tmp/issue.json", "utf8")); const title = String(issue.title || ""); const body = String(issue.body || "").replace(/\r\n/g, "\n"); @@ -185,15 +188,13 @@ jobs: source /tmp/anno.env || true [[ "${SKIP:-0}" == "1" ]] || exit 0 - [[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0 + test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } - # message différent si Proposer REASON="${SKIP_REASON:-}" TYPE="${ISSUE_TYPE:-}" - TITLE="${ISSUE_TITLE:-}" if [[ "$REASON" == proposer_type:* ]]; then - MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors** (correction/fact-check + cat/*).\n- Le bot n'applique **jamais** Proposer et n'ajoute **jamais** state/approved automatiquement.\n\n✅ Action : traitement éditorial manuel." + MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors** (correction/fact-check + cat/*).\n- Le bot n'applique **jamais** Proposer.\n\n✅ Action : traitement éditorial manuel." elif [[ "$REASON" == unsupported_type:* ]]; then MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment.\n✅ Action : traitement manuel si nécessaire." else @@ -209,9 +210,10 @@ jobs: --data-binary "$PAYLOAD" - name: Checkout default branch + if: ${{ always() }} run: | set -euo pipefail - source /tmp/anno.env + source /tmp/anno.env || true [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } rm -rf .git @@ -222,16 +224,18 @@ jobs: git log -1 --oneline - name: Install deps + if: ${{ always() }} run: | set -euo pipefail - source /tmp/anno.env + source /tmp/anno.env || true [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } npm ci --no-audit --no-fund - name: Check apply script exists + if: ${{ always() }} run: | set -euo pipefail - source /tmp/anno.env + source /tmp/anno.env || true [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } test -f scripts/apply-annotation-ticket.mjs || { echo "❌ missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH" @@ -240,9 +244,10 @@ jobs: } - name: Build dist (needed for --verify) + if: ${{ always() }} run: | set -euo pipefail - source /tmp/anno.env + source /tmp/anno.env || true [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } npm run build @@ -255,6 +260,7 @@ jobs: echo "✅ dist/para-index.json present" - name: Apply ticket on bot branch (strict+verify, commit) + if: ${{ always() }} continue-on-error: true env: FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }} @@ -262,7 +268,7 @@ jobs: BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }} run: | set -euo pipefail - source /tmp/anno.env + source /tmp/anno.env || true [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } @@ -291,13 +297,12 @@ jobs: echo "== apply log (tail) ==" tail -n 180 "$LOG" || true - END_SHA="$(git rev-parse HEAD)" - if [[ "$RC" -ne 0 ]]; then echo "NOOP=0" >> /tmp/anno.env exit 0 fi + END_SHA="$(git rev-parse HEAD)" if [[ "$START_SHA" == "$END_SHA" ]]; then echo "NOOP=1" >> /tmp/anno.env else @@ -320,6 +325,8 @@ jobs: exit 0 fi + test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } + if [[ -f /tmp/apply.log ]]; then BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')" else @@ -344,9 +351,12 @@ jobs: source /tmp/anno.env || true [[ "${SKIP:-0}" != "1" ]] || exit 0 + # ✅ si apply a réussi ET NOOP=1 => commenter [[ "${APPLY_RC:-0}" == "0" ]] || exit 0 [[ "${NOOP:-0}" == "1" ]] || exit 0 + test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } + MSG="ℹ️ Ticket #${ISSUE_NUMBER} : rien à appliquer (déjà présent / dédupliqué)." PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")" @@ -368,6 +378,9 @@ jobs: [[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; } [[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; } + test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } + test -n "${BRANCH:-}" || { echo "❌ BRANCH missing"; exit 1; } + AUTH_URL="$(node --input-type=module -e ' const [clone, tok] = process.argv.slice(1); const u = new URL(clone); @@ -391,6 +404,10 @@ jobs: [[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip PR"; exit 0; } [[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip PR"; exit 0; } + test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } + test -n "${BRANCH:-}" || { echo "❌ BRANCH missing"; exit 1; } + test -n "${END_SHA:-}" || { echo "❌ END_SHA missing"; exit 1; } + PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}" PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK." diff --git a/.gitea/workflows/anno-reject.yml b/.gitea/workflows/anno-reject.yml index c1efb4e..3d0a5cd 100644 --- a/.gitea/workflows/anno-reject.yml +++ b/.gitea/workflows/anno-reject.yml @@ -22,6 +22,13 @@ concurrency: jobs: reject: + # ✅ Job ne se lance QUE si le label ajouté est state/rejected, ou si workflow_dispatch + if: >- + ${{ + (github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'state/rejected') + || github.event_name == 'workflow_dispatch' + }} + runs-on: mac-ci container: image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm @@ -96,44 +103,34 @@ jobs: `REPO=${sh(repo)}`, `ISSUE_NUMBER=${sh(issueNumber)}`, `LABEL_NAME=${sh(labelName)}`, - `API_BASE=${sh(apiBase)}` + `API_BASE=${sh(apiBase)}`, + `SKIP=0` ].join("\n") + "\n"); NODE echo "✅ context:" sed -n '1,120p' /tmp/reject.env - - name: Gate on label state/rejected only - run: | - set -euo pipefail - source /tmp/reject.env - - if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" ]]; then - echo "ℹ️ label=$LABEL_NAME => skip" - echo "SKIP=1" >> /tmp/reject.env - exit 0 - fi - echo "✅ proceed (issue=$ISSUE_NUMBER)" - - name: Comment + close (only if not conflicting with state/approved) env: FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }} run: | set -euo pipefail source /tmp/reject.env - [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; } test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; } test -n "${API_BASE:-}" || { echo "❌ Missing API_BASE"; exit 1; } - ISSUE_JSON="$(curl -fsS \ + curl -fsS \ -H "Authorization: token $FORGE_TOKEN" \ -H "Accept: application/json" \ - "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")" + "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \ + -o /tmp/issue.json # conflict guard: approved + rejected => do nothing, comment warning - node --input-type=module - <<'NODE' "$ISSUE_JSON" > /tmp/reject.flags - const issue = JSON.parse(process.argv[1] || "{}"); + node --input-type=module <<'NODE' > /tmp/reject.flags + import fs from "node:fs"; + const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8")); const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : []; const hasApproved = labels.includes("state/approved"); const hasRejected = labels.includes("state/rejected");