diff --git a/.github/actions/team-guard/action.yml b/.github/actions/team-guard/action.yml new file mode 100644 index 0000000..a60471f --- /dev/null +++ b/.github/actions/team-guard/action.yml @@ -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 diff --git a/.github/workflows/ai-guarded-review.yml b/.github/workflows/ai-guarded-review.yml index 1084bce..39d1a74 100644 --- a/.github/workflows/ai-guarded-review.yml +++ b/.github/workflows/ai-guarded-review.yml @@ -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: