ci: hard-gate anno apply/reject + fix JSON parsing
This commit is contained in:
@@ -22,6 +22,8 @@ concurrency:
|
||||
|
||||
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
|
||||
@@ -31,6 +33,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --version
|
||||
curl --version | head -n 1
|
||||
|
||||
- name: Derive context (event.json / workflow_dispatch)
|
||||
env:
|
||||
@@ -77,73 +80,47 @@ jobs:
|
||||
|
||||
const labelName =
|
||||
ev?.label?.name ||
|
||||
(typeof ev?.label === "string" ? ev.label : "") ||
|
||||
"";
|
||||
ev?.label ||
|
||||
"workflow_dispatch";
|
||||
|
||||
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 = "";
|
||||
}
|
||||
const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
|
||||
? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
|
||||
: (cloneUrl ? new URL(cloneUrl).origin : "");
|
||||
|
||||
function sh(s){ return JSON.stringify(String(s ?? "")); }
|
||||
function sh(s){ return JSON.stringify(String(s)); }
|
||||
|
||||
process.stdout.write([
|
||||
`OWNER=${sh(owner)}`,
|
||||
`REPO=${sh(repo)}`,
|
||||
`ISSUE_NUMBER=${sh(issueNumber)}`,
|
||||
`LABEL_NAME=${sh(labelName)}`,
|
||||
`API_BASE=${sh(apiBase)}`,
|
||||
`SKIP=0`
|
||||
`API_BASE=${sh(apiBase)}`
|
||||
].join("\n") + "\n");
|
||||
NODE
|
||||
|
||||
echo "✅ context:"
|
||||
sed -n '1,160p' /tmp/reject.env
|
||||
sed -n '1,120p' /tmp/reject.env
|
||||
|
||||
- name: Gate fast (only if label is state/rejected or workflow_dispatch)
|
||||
env:
|
||||
INPUT_ISSUE: ${{ inputs.issue }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/reject.env
|
||||
|
||||
if [[ -n "${INPUT_ISSUE:-}" ]]; then
|
||||
echo "✅ workflow_dispatch => proceed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "${LABEL_NAME:-}" && "$LABEL_NAME" != "state/rejected" ]]; then
|
||||
echo "ℹ️ triggering label='$LABEL_NAME' (not state/rejected) => skip"
|
||||
echo "SKIP=1" >> /tmp/reject.env
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ℹ️ label unknown or rejected => continue to API gating"
|
||||
|
||||
- name: Comment + close (only if issue has state/rejected; conflict-guard approved+rejected)
|
||||
- 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; }
|
||||
|
||||
curl -fsS \
|
||||
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Accept: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
> /tmp/issue.json
|
||||
-o /tmp/reject.issue.json
|
||||
|
||||
node --input-type=module - <<'NODE' > /tmp/reject.flags
|
||||
# conflict guard: approved + rejected => do nothing, comment warning
|
||||
node --input-type=module - /tmp/reject.issue.json > /tmp/reject.flags <<'NODE'
|
||||
import fs from "node:fs";
|
||||
const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
|
||||
const issue = JSON.parse(fs.readFileSync(process.argv[2], "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");
|
||||
@@ -152,16 +129,11 @@ jobs:
|
||||
|
||||
source /tmp/reject.flags
|
||||
|
||||
# If issue does not actually have state/rejected -> do nothing (normal label churn)
|
||||
if [[ "${HAS_REJECTED:-0}" != "1" ]]; then
|
||||
echo "ℹ️ issue has no state/rejected => 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 -X POST \
|
||||
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
|
||||
-X POST \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
|
||||
@@ -170,16 +142,20 @@ 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 -X POST \
|
||||
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
|
||||
-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"
|
||||
|
||||
curl -fsS -X PATCH \
|
||||
# close issue
|
||||
curl -fsS --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 \
|
||||
-X PATCH \
|
||||
-H "Authorization: token $FORGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
|
||||
|
||||
Reference in New Issue
Block a user