Skip to content
Open
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
325 changes: 325 additions & 0 deletions .github/workflows/award-project-badge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
name: Award Project Contributor Badges

on:
workflow_call:
inputs:
pr_number:
description: "Merged Pull Request number in the caller repository"
required: true
type: number
dry_run:
description: "Simulate badge evaluation and skip live awards/labels"
required: false
type: boolean
default: false
secrets:
SLACK_BOT_TOKEN:
description: "Slack Bot Token for /award-badge dispatches"
required: false

permissions:
contents: read
issues: write

concurrency:
group: badge-award-${{ github.repository }}-${{ inputs.pr_number }}
cancel-in-progress: false

jobs:
evaluate-and-award:
name: Evaluate and Award Badges
runs-on: ubuntu-latest
steps:
- name: Validate authorized repository allowlist
env:
TARGET_REPO: ${{ github.repository }}
run: |
set -euo pipefail
NORMALIZED_REPO=$(echo "${TARGET_REPO}" | tr '[:upper:]' '[:lower:]')
case "${NORMALIZED_REPO}" in
"layer5io/sistent"|"meshery/meshery"|"meshery/meshery-operator"|"meshery/meshsync"|"layer5io/docs"|"meshery/meshery.io"|"layer5io/layer5")
echo "Repository '${TARGET_REPO}' is authorized for Track 2 badge automation."
;;
*)
echo "::error::Repository '${TARGET_REPO}' is not an authorized Track 2 participating repository. Failing workflow."
exit 1
;;
esac

- name: Checkout trusted recognition engine
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 pinned SHA
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
path: .recognition-engine
sparse-checkout: |
utils
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Verify PR status and collect metadata
id: collect-meta
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail
echo "Fetching Pull Request #${PR_NUMBER} in ${TARGET_REPO}..."

GH_RESP_FILE=$(mktemp)
GH_ERR_FILE=$(mktemp)
set +e
gh api --include "repos/${TARGET_REPO}/pulls/${PR_NUMBER}" > "${GH_RESP_FILE}" 2> "${GH_ERR_FILE}"
GH_EXIT_CODE=$?
set -e

HTTP_STATUS=""
if [ -s "${GH_RESP_FILE}" ]; then
HTTP_STATUS=$(head -n 1 "${GH_RESP_FILE}" | awk '{print $2}')
fi

if [ "${HTTP_STATUS}" = "200" ]; then
sed -e '1,/^\r\{0,1\}$/d' "${GH_RESP_FILE}" > .pr-info.json
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
elif [ "${HTTP_STATUS}" = "404" ]; then
echo "::warning::Pull Request #${PR_NUMBER} not found in ${TARGET_REPO} (HTTP 404). Exiting without dispatch."
echo "## ⚠️ Badge Evaluation Skipped" >> "$GITHUB_STEP_SUMMARY"
echo "Pull Request #${PR_NUMBER} was not found in \`${TARGET_REPO}\` (HTTP 404). Zero badges awarded." >> "$GITHUB_STEP_SUMMARY"
echo "skip=true" >> "$GITHUB_OUTPUT"
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
exit 0
elif [ "${HTTP_STATUS}" = "403" ]; then
echo "::error::GitHub API access forbidden (HTTP 403) while querying PR #${PR_NUMBER} in ${TARGET_REPO}"
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
exit 1
elif [ "${HTTP_STATUS}" = "429" ]; then
echo "::error::GitHub API rate limit exceeded (HTTP 429) while querying PR #${PR_NUMBER} in ${TARGET_REPO}"
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
exit 1
elif [[ "${HTTP_STATUS}" =~ ^5[0-9]{2}$ ]]; then
echo "::error::GitHub API server error (HTTP ${HTTP_STATUS}) while querying PR #${PR_NUMBER} in ${TARGET_REPO}"
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
exit 1
elif [ -n "${HTTP_STATUS}" ]; then
echo "::error::Unexpected GitHub API HTTP status (${HTTP_STATUS}) while querying PR #${PR_NUMBER} in ${TARGET_REPO}"
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
exit 1
else
GH_ERR_MSG=$(cat "${GH_ERR_FILE}")
echo "::error::GitHub API network/transport error while querying PR #${PR_NUMBER} in ${TARGET_REPO}: ${GH_ERR_MSG}"
rm -f "${GH_RESP_FILE}" "${GH_ERR_FILE}"
exit 1
fi

IS_MERGED=$(jq -r '.merged // false' .pr-info.json)
if [ "${IS_MERGED}" != "true" ]; then
echo "::warning::Pull Request #${PR_NUMBER} in ${TARGET_REPO} is not merged (merged=${IS_MERGED}). Skipping badge evaluation."
echo "## ⚠️ Badge Evaluation Skipped" >> "$GITHUB_STEP_SUMMARY"
echo "Pull Request #${PR_NUMBER} in \`${TARGET_REPO}\` is not in a merged state. Zero badges awarded." >> "$GITHUB_STEP_SUMMARY"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Pull Request #${PR_NUMBER} verified as merged. Fetching files, commits, and labels..."
gh api "repos/${TARGET_REPO}/pulls/${PR_NUMBER}/files" --paginate --slurp | jq 'add // []' > .pr-files.json
gh api "repos/${TARGET_REPO}/pulls/${PR_NUMBER}/commits" --paginate --slurp | jq 'add // []' > .pr-commits.json
gh api "repos/${TARGET_REPO}/issues/${PR_NUMBER}/labels" --paginate --slurp | jq 'add // []' > .existing-labels.json

node -e '
const fs = require("fs");
const pr = JSON.parse(fs.readFileSync(".pr-info.json"));
const files = JSON.parse(fs.readFileSync(".pr-files.json"));
const commits = JSON.parse(fs.readFileSync(".pr-commits.json"));
fs.writeFileSync(".pr-metadata.json", JSON.stringify({ pr, files, commits }, null, 2));
'
echo "skip=false" >> "$GITHUB_OUTPUT"

- name: Run badge award orchestrator
if: ${{ steps.collect-meta.outputs.skip == 'false' }}
env:
TARGET_REPO: ${{ github.repository }}
run: |
set -euo pipefail
node .recognition-engine/utils/award-orchestrator.js \
--metadata=".pr-metadata.json" \
--existing-labels=".existing-labels.json" \
--repo="${TARGET_REPO}" \
--out=".evaluation-result.json" \
--dispatch-out=".dispatch-context.json"

- name: Publish evaluation step summary
if: ${{ steps.collect-meta.outputs.skip == 'false' }}
run: |
node -e '
const fs = require("fs");
const res = JSON.parse(fs.readFileSync(".evaluation-result.json"));
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, res.summaryMarkdown + "\n");
'

- name: Verify Slack credentials for production run
if: ${{ steps.collect-meta.outputs.skip == 'false' && inputs.dry_run == false }}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
run: |
PENDING_COUNT=$(node -e '
const fs = require("fs");
const res = JSON.parse(fs.readFileSync(".evaluation-result.json"));
console.log(res.pendingAwards.length);
')

if [ "${PENDING_COUNT}" -gt 0 ] && [ -z "${SLACK_BOT_TOKEN:-}" ]; then
echo "::error::SLACK_BOT_TOKEN secret is required for production badge awards but was not provided. Failing workflow to prevent silent omission."
exit 1
fi

- name: Sequentially dispatch awards and apply tracking labels
if: ${{ steps.collect-meta.outputs.skip == 'false' }}
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ github.repository }}
PR_NUMBER: ${{ inputs.pr_number }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
CANONICAL_SLACK_CHANNEL: "CLDRKJZ0T"
IS_DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail

AWARDS_JSON=$(node -e '
const fs = require("fs");
const res = JSON.parse(fs.readFileSync(".dispatch-context.json"));
console.log(JSON.stringify(res.pendingAwards || []));
')

EMAIL=$(node -e '
const fs = require("fs");
const res = JSON.parse(fs.readFileSync(".dispatch-context.json"));
console.log(res.recipientEmail || "");
')

MASKED_EMAIL=$(node -e '
const fs = require("fs");
const res = JSON.parse(fs.readFileSync(".dispatch-context.json"));
console.log(res.maskedEmail || "unknown");
')

# Securely wipe dispatch context file containing raw recipient email
rm -f .dispatch-context.json

AWARD_COUNT=$(echo "${AWARDS_JSON}" | jq '. | length')
echo "Pending awards count: ${AWARD_COUNT}"

if [ "${AWARD_COUNT}" -eq 0 ]; then
echo "No pending awards to dispatch."
exit 0
fi

for i in $(seq 0 $((AWARD_COUNT - 1))); do
BADGE_SLUG=$(echo "${AWARDS_JSON}" | jq -r ".[$i].slug")
BADGE_NAME=$(echo "${AWARDS_JSON}" | jq -r ".[$i].name")
LABEL_NAME=$(echo "${AWARDS_JSON}" | jq -r ".[$i].trackingLabel")

echo "--------------------------------------------------------"
echo "Processing award $((i + 1)) of ${AWARD_COUNT}: ${BADGE_NAME} (${BADGE_SLUG})"

# Step 1: Dispatch to Slack (Never echoing raw email to stdout)
if [ "${IS_DRY_RUN}" = "true" ]; then
echo "[DRY-RUN] Would post award command for ${BADGE_SLUG} to recipient (${MASKED_EMAIL})"
else
echo "Dispatching award for ${BADGE_SLUG} to recipient (${MASKED_EMAIL})..."
# The bot posts a message containing "/award-badge <email> <slug>" to the
# canonical Slack channel. The Layer5 Cloud Slack integration monitors this
# channel and consumes the message to issue the badge award.
PAYLOAD=$(jq -n \
--arg ch "${CANONICAL_SLACK_CHANNEL}" \
--arg txt "/award-badge ${EMAIL} ${BADGE_SLUG}" \
'{channel: $ch, text: $txt}')

SLACK_RESP=$(curl -s -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer ${SLACK_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "${PAYLOAD}")

SLACK_OK=$(echo "${SLACK_RESP}" | jq -r '.ok // false')
if [ "${SLACK_OK}" != "true" ]; then
SLACK_ERR=$(echo "${SLACK_RESP}" | jq -r '.error // "unknown"')
echo "::error::Slack dispatch failed for badge ${BADGE_SLUG}: ${SLACK_ERR}"
exit 1
fi
echo "Slack dispatch successful for ${BADGE_SLUG}."
fi

# Step 2: Race-Safe Label Provisioning & Label Write
if [ "${IS_DRY_RUN}" = "true" ]; then
echo "[DRY-RUN] Would create/verify label '${LABEL_NAME}' and apply to PR #${PR_NUMBER}"
else
echo "Ensuring label '${LABEL_NAME}' exists on ${TARGET_REPO}..."
LABEL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GH_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${TARGET_REPO}/labels/${LABEL_NAME}")

if [ "${LABEL_STATUS}" = "200" ]; then
echo "Label '${LABEL_NAME}' already exists on ${TARGET_REPO}."
elif [ "${LABEL_STATUS}" = "404" ]; then
echo "Label '${LABEL_NAME}' does not exist on ${TARGET_REPO}. Creating..."
CREATE_RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -s -w "%{http_code}" -o "${CREATE_RESP_FILE}" \
-X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${TARGET_REPO}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"0E7090\",\"description\":\"Automated contributor badge tracking\"}")

if [ "${HTTP_CODE}" = "201" ]; then
echo "Label '${LABEL_NAME}' created successfully."
elif [ "${HTTP_CODE}" = "422" ]; then
IS_ALREADY_EXISTS=$(jq -r '.errors[]? | select(.code == "already_exists") | .code' "${CREATE_RESP_FILE}")
if [ "${IS_ALREADY_EXISTS}" = "already_exists" ]; then
echo "Label '${LABEL_NAME}' already exists (race condition resolved)."
else
echo "::error::Fatal 422 error creating label '${LABEL_NAME}': $(cat "${CREATE_RESP_FILE}")"
rm -f "${CREATE_RESP_FILE}"
exit 1
fi
elif [ "${HTTP_CODE}" = "403" ]; then
echo "::error::GitHub API forbidden (HTTP 403) while creating label '${LABEL_NAME}' on ${TARGET_REPO}: $(cat "${CREATE_RESP_FILE}")"
rm -f "${CREATE_RESP_FILE}"
exit 1
elif [ "${HTTP_CODE}" = "429" ]; then
echo "::error::GitHub API rate limit exceeded (HTTP 429) while creating label '${LABEL_NAME}' on ${TARGET_REPO}: $(cat "${CREATE_RESP_FILE}")"
rm -f "${CREATE_RESP_FILE}"
exit 1
elif [[ "${HTTP_CODE}" =~ ^5[0-9]{2}$ ]]; then
echo "::error::GitHub API server error (HTTP ${HTTP_CODE}) while creating label '${LABEL_NAME}' on ${TARGET_REPO}: $(cat "${CREATE_RESP_FILE}")"
rm -f "${CREATE_RESP_FILE}"
exit 1
else
echo "::error::Failed to create label '${LABEL_NAME}' (HTTP ${HTTP_CODE}): $(cat "${CREATE_RESP_FILE}")"
rm -f "${CREATE_RESP_FILE}"
exit 1
fi
rm -f "${CREATE_RESP_FILE}"
elif [ "${LABEL_STATUS}" = "403" ]; then
echo "::error::GitHub API access forbidden (HTTP 403) while querying label '${LABEL_NAME}' on ${TARGET_REPO}"
exit 1
elif [ "${LABEL_STATUS}" = "429" ]; then
echo "::error::GitHub API rate limit exceeded (HTTP 429) while querying label '${LABEL_NAME}' on ${TARGET_REPO}"
exit 1
elif [[ "${LABEL_STATUS}" =~ ^5[0-9]{2}$ ]]; then
echo "::error::GitHub API server error (HTTP ${LABEL_STATUS}) while querying label '${LABEL_NAME}' on ${TARGET_REPO}"
exit 1
else
echo "::error::GitHub API error or network failure (HTTP ${LABEL_STATUS}) while querying label '${LABEL_NAME}' on ${TARGET_REPO}"
exit 1
fi

# Apply label to PR
echo "Applying tracking label '${LABEL_NAME}' to PR #${PR_NUMBER}..."
if ! gh issue edit "${PR_NUMBER}" --repo "${TARGET_REPO}" --add-label "${LABEL_NAME}"; then
echo "::error::Slack award dispatched for badge '${BADGE_SLUG}' but GitHub label application failed. Manual tracking label intervention required."
exit 1
fi
echo "Applied tracking label '${LABEL_NAME}'."
fi
done

echo "Badge award processing completed successfully."
23 changes: 23 additions & 0 deletions .github/workflows/badge-engine-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Badge Engine Tests

on:
pull_request:
paths:
- 'utils/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/**'

permissions:
contents: read

jobs:
test-badge-engine:
name: Run Badge Engine Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 pinned SHA

- name: Run badge engine tests
run: npm run test:badge-engine
Loading
Loading