Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/actions/team-guard/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: 'Team membership guard'
description: >-
Checks that a GitHub user is an active member of an organization team.
By default the step fails when the user is not a member, which blocks all
dependent jobs/steps. Set require_membership to false to only expose the
result as an output and let the caller decide what to do.
branding:
icon: 'shield'
color: 'red'

inputs:
team_slug:
description: 'Slug of the team the actor must belong to (e.g. prestashop-sa)'
required: true
token:
description: 'Token with read:org scope used for the membership API call (e.g. JARVIS_TOKEN)'
required: true
actor:
description: 'GitHub login to verify. Defaults to the user who triggered the workflow.'
required: false
default: ''
org:
description: 'Organization owning the team. Defaults to the owner of the calling repository.'
required: false
default: ''
require_membership:
description: 'When true (default), the step fails if the actor is not an active team member. Set to false to only expose the is_member output.'
required: false
default: 'true'

outputs:
is_member:
description: '"true" when the actor is an active member of the team, "false" otherwise'
value: ${{ steps.check.outputs.is_member }}

runs:
using: 'composite'
steps:
- name: Check team membership
id: check
shell: bash
env:
TOKEN: ${{ inputs.token }}
ACTOR: ${{ inputs.actor || github.actor }}
TEAM: ${{ inputs.team_slug }}
ORG: ${{ inputs.org || github.repository_owner }}
REQUIRE: ${{ inputs.require_membership }}
run: |
fail_or_report() {
echo "is_member=false" >> "$GITHUB_OUTPUT"
if [ "$REQUIRE" = "true" ]; then
exit 1
fi
exit 0
}

if [ -z "$TOKEN" ]; then
echo "::error::The token input is empty — the secret may not be configured or not accessible from the calling repository. It must be a token with read:org scope."
echo "⛔ Team guard: token is empty, cannot check membership." >> "$GITHUB_STEP_SUMMARY"
fail_or_report
fi

API_URL="https://api.github.com/orgs/${ORG}/teams/${TEAM}/memberships/${ACTOR}"
echo "Checking membership: ${API_URL}"

RESPONSE=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github+json" \
"$API_URL")
STATUS=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [ "$STATUS" = "200" ]; then
ROLE=$(echo "$BODY" | jq -r '.role // "unknown"')
STATE=$(echo "$BODY" | jq -r '.state // "unknown"')
if [ "$STATE" = "active" ]; then
echo "is_member=true" >> "$GITHUB_OUTPUT"
echo "✅ ${ACTOR} is a member of ${ORG}/${TEAM} (role=${ROLE}, state=${STATE})." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "::error::${ACTOR} has a pending (state=${STATE}) membership in ${ORG}/${TEAM} — only active members are allowed."
echo "⛔ ${ACTOR} is not an active member of ${ORG}/${TEAM} (state=${STATE})." >> "$GITHUB_STEP_SUMMARY"
fail_or_report
fi

echo "::error::${ACTOR} is not a member of ${ORG}/${TEAM} (HTTP ${STATUS})."
echo "⛔ ${ACTOR} is not a member of ${ORG}/${TEAM} (HTTP ${STATUS})." >> "$GITHUB_STEP_SUMMARY"
fail_or_report
48 changes: 9 additions & 39 deletions .github/workflows/ai-guarded-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,48 +89,18 @@ jobs:
needs: check-label
if: needs.check-label.outputs.already-reviewed != 'true'
outputs:
is-member: ${{ steps.check.outputs.is-member }}
is-member: ${{ steps.check.outputs.is_member }}
steps:
- name: Check team membership
id: check
env:
TOKEN: ${{ secrets.org-read-token }}
ACTOR: ${{ inputs.actor }}
TEAM: ${{ inputs.team-slug }}
run: |
if [ -z "$TOKEN" ]; then
echo "::error::org-read-token secret is empty — the secret may not be configured or not accessible from the calling repository."
echo "⛔ **Debug:** org-read-token secret is empty. Check that the secret exists and is accessible." >> "$GITHUB_STEP_SUMMARY"
echo "is-member=false" >> "$GITHUB_OUTPUT"
exit 0
fi
TOKEN_PREFIX="${TOKEN:0:4}"
echo "::debug::Token is set (starts with ${TOKEN_PREFIX}…, length=${#TOKEN})"
echo "📋 **Debug:** Token is set (length=${#TOKEN}, prefix=${TOKEN_PREFIX}…)" >> "$GITHUB_STEP_SUMMARY"

API_URL="https://api.github.com/orgs/PrestaShop/teams/$TEAM/memberships/$ACTOR"
echo "📋 **Debug:** Calling $API_URL" >> "$GITHUB_STEP_SUMMARY"

RESPONSE=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github+json" \
"$API_URL")
STATUS=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

echo "📋 **Debug:** HTTP status=$STATUS" >> "$GITHUB_STEP_SUMMARY"

if [ "$STATUS" = "200" ]; then
ROLE=$(echo "$BODY" | jq -r '.role // "unknown"')
STATE=$(echo "$BODY" | jq -r '.state // "unknown"')
echo "📋 **Debug:** Membership role=$ROLE, state=$STATE" >> "$GITHUB_STEP_SUMMARY"
echo "is-member=true" >> "$GITHUB_OUTPUT"
echo "✅ $ACTOR is a member of $TEAM (role=$ROLE, state=$STATE) — Claude review will run." >> "$GITHUB_STEP_SUMMARY"
else
echo "📋 **Debug:** Response body: $BODY" >> "$GITHUB_STEP_SUMMARY"
echo "is-member=false" >> "$GITHUB_OUTPUT"
echo "⛔ $ACTOR is not a member of $TEAM (HTTP $STATUS) — skipping Claude review." >> "$GITHUB_STEP_SUMMARY"
fi
uses: PrestaShop/.github/.github/actions/team-guard@master
with:
team_slug: ${{ inputs.team-slug }}
actor: ${{ inputs.actor }}
org: PrestaShop
token: ${{ secrets.org-read-token }}
# The gate job decides what to do with the result, so do not fail here
require_membership: false

# Step 3: Evaluate guard
gate:
Expand Down