ci: stabilize anno apply/reject (event parsing + strict gating)
Some checks failed
CI / build-and-anchors (push) Successful in 52s
CI / build-and-anchors (pull_request) Successful in 41s
SMOKE / smoke (push) Failing after 12m55s

This commit is contained in:
2026-03-02 11:10:53 +01:00
parent 901d28b89b
commit 7c8e49c1a9
2 changed files with 62 additions and 48 deletions

View File

@@ -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."

View File

@@ -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");