ci: stabilize anno apply/reject (event parsing + strict gating)
This commit is contained in:
@@ -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."
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user