ci: make anno apply/reject gates API-hard (approved/rejected label present)
All checks were successful
SMOKE / smoke (push) Successful in 7s
CI / build-and-anchors (push) Successful in 40s
CI / build-and-anchors (pull_request) Successful in 39s

This commit is contained in:
2026-03-02 20:12:29 +01:00
parent 716c887045
commit dec5f8eba7
2 changed files with 118 additions and 97 deletions

View File

@@ -17,13 +17,11 @@ defaults:
shell: bash
concurrency:
group: anno-reject-${{ github.event.issue.number || inputs.issue || 'manual' }}
group: anno-reject-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
cancel-in-progress: true
jobs:
reject:
# ✅ Job ne démarre QUE si state/rejected (ou workflow_dispatch)
if: ${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'state/rejected' }}
runs-on: mac-ci
container:
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
@@ -33,12 +31,11 @@ jobs:
run: |
set -euo pipefail
node --version
curl --version | head -n 1
- name: Derive context (event.json / workflow_dispatch)
env:
INPUT_ISSUE: ${{ inputs.issue }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
run: |
set -euo pipefail
export EVENT_JSON="/var/run/act/workflow/event.json"
@@ -78,14 +75,20 @@ jobs:
throw new Error("No issue number in event.json or workflow_dispatch input");
}
const labelName =
ev?.label?.name ||
ev?.label ||
"workflow_dispatch";
// label name: best-effort (non-bloquant)
let labelName = "workflow_dispatch";
const lab = ev?.label;
if (typeof lab === "string") labelName = lab;
else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
: (cloneUrl ? new URL(cloneUrl).origin : "");
let apiBase = "";
if (process.env.FORGE_API && String(process.env.FORGE_API).trim()) {
apiBase = String(process.env.FORGE_API).trim().replace(/\/+$/,"");
} else if (cloneUrl) {
apiBase = new URL(cloneUrl).origin;
} else {
apiBase = "";
}
function sh(s){ return JSON.stringify(String(s)); }
@@ -101,26 +104,39 @@ jobs:
echo "✅ context:"
sed -n '1,120p' /tmp/reject.env
- name: Comment + close (only if not conflicting with state/approved)
- name: Early gate (fast-skip, tolerant)
run: |
set -euo pipefail
source /tmp/reject.env
echo " event label = $LABEL_NAME"
if [[ "$LABEL_NAME" != "state/rejected" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
echo " label=$LABEL_NAME => skip early"
echo "SKIP=1" >> /tmp/reject.env
echo "SKIP_REASON=\"label_not_rejected_event\"" >> /tmp/reject.env
exit 0
fi
- name: Comment + close (only if label state/rejected is PRESENT now, and no conflict)
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; }
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
curl -fsS \
-H "Authorization: token $FORGE_TOKEN" \
-H "Accept: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
-o /tmp/reject.issue.json
# conflict guard: approved + rejected => do nothing, comment warning
node --input-type=module - /tmp/reject.issue.json > /tmp/reject.flags <<'NODE'
node --input-type=module - <<'NODE' > /tmp/reject.flags
import fs from "node:fs";
const issue = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const issue = JSON.parse(fs.readFileSync("/tmp/reject.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");
@@ -129,11 +145,16 @@ jobs:
source /tmp/reject.flags
# Do nothing unless state/rejected is truly present now (anti payload weird)
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
echo " state/rejected not present -> skip"
exit 0
fi
if [[ "${HAS_APPROVED:-0}" == "1" && "${HAS_REJECTED:-0}" == "1" ]]; then
MSG="⚠️ Conflit d'état sur le ticket #${ISSUE_NUMBER} : labels **state/approved** et **state/rejected** présents.\n\n➡ Action manuelle requise : retirer l'un des deux labels avant relance."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
@@ -142,20 +163,16 @@ jobs:
exit 0
fi
# comment reject
MSG="❌ Ticket #${ISSUE_NUMBER} refusé (label state/rejected)."
PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X POST \
curl -fsS -X POST \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
--data-binary "$PAYLOAD"
# close issue
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
-X PATCH \
curl -fsS -X PATCH \
-H "Authorization: token $FORGE_TOKEN" \
-H "Content-Type: application/json" \
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \