fix: add members read permission for team membership checks #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | ||
| name: Deployment Guard | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| # Team-based bypass configuration | ||
| trusted_github_teams: | ||
| description: 'Comma-separated list of GitHub team slugs that can bypass all validations (e.g., platform-engineers,dotSysadmins)' | ||
| required: false | ||
| type: string | ||
| default: '' | ||
| # Validation toggles - enable/disable specific checks | ||
| enable_file_allowlist: | ||
| description: 'Enable file allowlist validation' | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| enable_image_only_check: | ||
| description: 'Enable image-only change validation (blocks if other fields changed)' | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| enable_image_validation: | ||
| description: 'Enable image repository, format, and version validation' | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| # File allowlist configuration | ||
| allowed_files_pattern: | ||
| description: 'Glob pattern for allowed files (e.g., kubernetes/dotcms/**/statefulset.yaml)' | ||
| required: false | ||
| type: string | ||
| default: '**/*' | ||
| # Image validation configuration | ||
| allowed_image_repositories: | ||
| description: 'Comma-separated list of allowed image repositories (e.g., dotcms/dotcms)' | ||
| required: false | ||
| type: string | ||
| default: '' | ||
| allowed_version_pattern: | ||
| description: 'Regex pattern for allowed version tags (default: any)' | ||
| required: false | ||
| type: string | ||
| default: '.*' | ||
| verify_image_existence: | ||
| description: 'Whether to verify image exists in registry' | ||
| required: false | ||
| type: boolean | ||
| default: false | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| # Required for checking team membership via GitHub API | ||
| # Note: This permission allows reading organization membership data | ||
| members: read | ||
| jobs: | ||
| check-team-membership: | ||
| name: Check Team Membership | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| is-team-member: ${{ steps.check.outputs.is_member }} | ||
| pr-author: ${{ steps.check.outputs.author }} | ||
| should-bypass: ${{ steps.check.outputs.should_bypass }} | ||
| matched-team: ${{ steps.check.outputs.matched_team }} | ||
| steps: | ||
| - name: Check if PR author is in trusted teams | ||
| id: check | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const trustedTeamsInput = '${{ inputs.trusted_github_teams }}'; | ||
| // If no teams configured, don't bypass | ||
| if (!trustedTeamsInput) { | ||
| console.log('ℹ️ No trusted teams configured, running full validation'); | ||
| core.setOutput('is_member', false); | ||
| core.setOutput('should_bypass', false); | ||
| core.setOutput('author', context.payload.pull_request.user.login); | ||
| core.setOutput('matched_team', ''); | ||
| return; | ||
| } | ||
| const actor = context.payload.pull_request.user.login; | ||
| const org = context.repo.owner; | ||
| // Parse comma-separated list of teams | ||
| const trustedTeams = trustedTeamsInput.split(',').map(t => t.trim()).filter(t => t); | ||
| core.setOutput('author', actor); | ||
| console.log(`Checking if ${actor} is a member of any trusted team...`); | ||
| console.log(`Trusted teams: ${trustedTeams.join(', ')}`); | ||
| // Check membership in each team | ||
| let isMemberOfAny = false; | ||
| let matchedTeam = ''; | ||
| for (const team of trustedTeams) { | ||
| try { | ||
| console.log(` Checking team: ${team}`); | ||
| const response = await github.rest.teams.getMembershipForUserInOrg({ | ||
| org: org, | ||
| team_slug: team, | ||
| username: actor | ||
| }); | ||
| if (response.data.state === 'active') { | ||
| isMemberOfAny = true; | ||
| matchedTeam = team; | ||
| console.log(` ✅ User ${actor} is a member of ${team}`); | ||
| break; // Found membership, no need to check other teams | ||
| } else { | ||
| console.log(` ℹ️ User ${actor} is not an active member of ${team}`); | ||
| } | ||
| } catch (error) { | ||
| if (error.status === 404) { | ||
| console.log(` ℹ️ User ${actor} is NOT a member of ${team} (or team does not exist)`); | ||
| } else { | ||
| console.log(` ⚠️ Error checking ${team}: ${error.message}`); | ||
| // Don't throw - continue checking other teams | ||
| } | ||
| } | ||
| } | ||
| // Set outputs | ||
| core.setOutput('is_member', isMemberOfAny); | ||
| core.setOutput('should_bypass', isMemberOfAny); | ||
| core.setOutput('matched_team', matchedTeam); | ||
| // Final result | ||
| if (isMemberOfAny) { | ||
| console.log(''); | ||
| console.log(`✅ RESULT: User ${actor} is a member of ${matchedTeam}`); | ||
| console.log('🚀 ALL VALIDATIONS WILL BE BYPASSED - Team member is fully trusted'); | ||
| } else { | ||
| console.log(''); | ||
| console.log(`ℹ️ RESULT: User ${actor} is NOT a member of any trusted team`); | ||
| console.log('🔒 Full validation will be enforced'); | ||
| } | ||
| validate-changed-files: | ||
| name: Validate Changed Files | ||
| runs-on: ubuntu-latest | ||
| needs: check-team-membership | ||
| if: | | ||
| inputs.enable_file_allowlist && | ||
| needs.check-team-membership.outputs.should-bypass == 'false' | ||
| outputs: | ||
| allowed-files-check: ${{ steps.check-files.outputs.result }} | ||
| changed-files: ${{ steps.get-files.outputs.files }} | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Get changed files | ||
| id: get-files | ||
| run: | | ||
| # Get list of changed files in the PR | ||
| if [ "${{ github.event_name }}" = "pull_request" ]; then | ||
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | ||
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | ||
| else | ||
| BASE_SHA="origin/${{ github.event.repository.default_branch }}" | ||
| HEAD_SHA="HEAD" | ||
| fi | ||
| CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -E '\.ya?ml$' || true) | ||
| if [ -z "$CHANGED_FILES" ]; then | ||
| echo "No YAML files changed" | ||
| echo "files=" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "Changed files:" | ||
| echo "$CHANGED_FILES" | ||
| # Convert to JSON array for output | ||
| FILES_JSON=$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n") | map(select(length > 0))') | ||
| echo "files=$FILES_JSON" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Check files against allowlist | ||
| id: check-files | ||
| run: | | ||
| CHANGED_FILES='${{ steps.get-files.outputs.files }}' | ||
| ALLOWED_PATTERN='${{ inputs.allowed_files_pattern }}' | ||
| if [ "$CHANGED_FILES" = "" ] || [ "$CHANGED_FILES" = "[]" ]; then | ||
| echo "No files to validate" | ||
| echo "result=pass" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| echo "Validating files against pattern: $ALLOWED_PATTERN" | ||
| # Convert glob pattern to regex | ||
| # First handle special case: .ya?ml → \.(yaml|yml) | ||
| # Then replace **/ with placeholder (as a unit, including the slash) | ||
| # Then replace remaining * with [^/]* (match any character except /) | ||
| # Then replace placeholder with ([^/]+/)* (match zero or more path segments) | ||
| # Note: Using # as delimiter to avoid conflicts with | | ||
| # Note: In grep -E, | should NOT be escaped (it's a regex alternation operator) | ||
| PATTERN_REGEX=$(echo "$ALLOWED_PATTERN" | sed 's#\.ya?ml#\\.(yaml|yml)#g' | sed 's#\*\*/#__DOUBLESTAR__#g' | sed 's#\*#[^/]*#g' | sed 's#__DOUBLESTAR__#([^/]+/)*#g') | ||
| echo "$CHANGED_FILES" | jq -r '.[]' | while IFS= read -r file; do | ||
| if ! echo "$file" | grep -qE "^${PATTERN_REGEX}$"; then | ||
| echo "❌ File not allowed: $file" | ||
| echo "$file" >> /tmp/disallowed_files.txt | ||
| else | ||
| echo "✅ File allowed: $file" | ||
| fi | ||
| done | ||
| if [ -f /tmp/disallowed_files.txt ]; then | ||
| echo "result=fail" >> "$GITHUB_OUTPUT" | ||
| echo "❌ Some files are not in the allowlist" | ||
| exit 1 | ||
| else | ||
| echo "result=pass" >> "$GITHUB_OUTPUT" | ||
| echo "✅ All files are in the allowlist" | ||
| fi | ||
| validate-image-only-changed: | ||
| name: Validate Only Image Changed | ||
| runs-on: ubuntu-latest | ||
| needs: [check-team-membership, validate-changed-files] | ||
| if: | | ||
| always() && | ||
| inputs.enable_image_only_check && | ||
| needs.check-team-membership.outputs.should-bypass == 'false' && | ||
| (inputs.enable_file_allowlist == false || needs.validate-changed-files.outputs.allowed-files-check == 'pass') | ||
| outputs: | ||
| image-only-check: ${{ steps.check-changes.outputs.result }} | ||
| new-images: ${{ steps.check-changes.outputs.images }} | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Install yq | ||
| run: | | ||
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 | ||
| sudo chmod +x /usr/local/bin/yq | ||
| - name: Check only image field changed | ||
| id: check-changes | ||
| run: | | ||
| CHANGED_FILES='${{ needs.validate-changed-files.outputs.changed-files }}' | ||
| if [ "$CHANGED_FILES" = "" ] || [ "$CHANGED_FILES" = "[]" ]; then | ||
| echo "No files to validate" | ||
| echo "result=pass" >> "$GITHUB_OUTPUT" | ||
| echo "images=[]" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | ||
| echo "$CHANGED_FILES" | jq -r '.[]' | while IFS= read -r file; do | ||
| echo "==================================================" | ||
| echo "Validating: $file" | ||
| echo "==================================================" | ||
| # Get old and new YAML content | ||
| OLD_YAML=$(git show "$BASE_SHA:$file" 2>/dev/null || echo "") | ||
| NEW_YAML=$(cat "$file") | ||
| if [ -z "$OLD_YAML" ]; then | ||
| echo "⚠️ File is new, skipping comparison" | ||
| continue | ||
| fi | ||
| # Extract the main dotCMS container image | ||
| OLD_IMAGE=$(echo "$OLD_YAML" | yq '.spec.template.spec.containers[] | select(.name == "dotcms") | .image' 2>/dev/null || echo "") | ||
| NEW_IMAGE=$(echo "$NEW_YAML" | yq '.spec.template.spec.containers[] | select(.name == "dotcms") | .image' 2>/dev/null || echo "") | ||
| if [ -z "$OLD_IMAGE" ] || [ -z "$NEW_IMAGE" ]; then | ||
| echo "❌ Could not extract dotCMS container image" | ||
| echo "false" > /tmp/validation_failed.txt | ||
| continue | ||
| fi | ||
| echo "Old image: $OLD_IMAGE" | ||
| echo "New image: $NEW_IMAGE" | ||
| # Normalize both YAMLs by replacing the dotCMS image with placeholder | ||
| OLD_NORMALIZED=$(echo "$OLD_YAML" | yq '(.spec.template.spec.containers[] | select(.name == "dotcms") | .image) = "PLACEHOLDER"') | ||
| NEW_NORMALIZED=$(echo "$NEW_YAML" | yq '(.spec.template.spec.containers[] | select(.name == "dotcms") | .image) = "PLACEHOLDER"') | ||
| # Compare normalized YAMLs | ||
| if [ "$OLD_NORMALIZED" != "$NEW_NORMALIZED" ]; then | ||
| echo "❌ Changes detected beyond image field in: $file" | ||
| echo "" | ||
| echo "Diff of normalized files:" | ||
| diff <(echo "$OLD_NORMALIZED") <(echo "$NEW_NORMALIZED") || true | ||
| echo "false" > /tmp/validation_failed.txt | ||
| else | ||
| echo "✅ Only image changed: $OLD_IMAGE → $NEW_IMAGE" | ||
| echo "$NEW_IMAGE" >> /tmp/new_images.txt | ||
| fi | ||
| echo "" | ||
| done | ||
| if [ -f /tmp/validation_failed.txt ]; then | ||
| echo "result=fail" >> "$GITHUB_OUTPUT" | ||
| echo "images=[]" >> "$GITHUB_OUTPUT" | ||
| exit 1 | ||
| else | ||
| if [ -f /tmp/new_images.txt ]; then | ||
| IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/new_images.txt) | ||
| echo "images=$IMAGES_JSON" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "images=[]" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| echo "result=pass" >> "$GITHUB_OUTPUT" | ||
| echo "✅ All files have only image field changes" | ||
| fi | ||
| validate-images: | ||
| name: Validate Images | ||
| runs-on: ubuntu-latest | ||
| needs: [check-team-membership, validate-image-only-changed] | ||
| if: | | ||
| always() && | ||
| inputs.enable_image_validation && | ||
| needs.check-team-membership.outputs.should-bypass == 'false' && | ||
| (inputs.enable_image_only_check == false || needs.validate-image-only-changed.outputs.image-only-check == 'pass') | ||
| outputs: | ||
| image-validation: ${{ steps.validate.outputs.result }} | ||
| steps: | ||
| - name: Validate image format, repository, tag, and existence | ||
| id: validate | ||
| run: | | ||
| NEW_IMAGES='${{ needs.validate-image-only-changed.outputs.new-images }}' | ||
| ALLOWED_REPOS='${{ inputs.allowed_image_repositories }}' | ||
| VERSION_PATTERN='${{ inputs.allowed_version_pattern }}' | ||
| VERIFY_EXISTENCE='${{ inputs.verify_image_existence }}' | ||
| if [ "$NEW_IMAGES" = "" ] || [ "$NEW_IMAGES" = "[]" ]; then | ||
| echo "No new images to validate" | ||
| echo "result=pass" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| echo "$NEW_IMAGES" | jq -r '.[]' | while IFS= read -r image; do | ||
| echo "==================================================" | ||
| echo "Validating image: $image" | ||
| echo "==================================================" | ||
| # 1. Validate format (repo/name:tag) | ||
| if ! [[ "$image" =~ ^[a-zA-Z0-9_/-]+:[a-zA-Z0-9._-]+$ ]]; then | ||
| echo "❌ Invalid image format: $image" | ||
| echo "false" > /tmp/validation_failed.txt | ||
| continue | ||
| fi | ||
| echo "✅ Image format is valid" | ||
| # 2. Extract repository and tag | ||
| REPO="${image%:*}" | ||
| TAG="${image##*:}" | ||
| echo " Repository: $REPO" | ||
| echo " Tag: $TAG" | ||
| # 3. Check repository is in allowlist (if configured) | ||
| if [ -n "$ALLOWED_REPOS" ]; then | ||
| REPO_ALLOWED=false | ||
| IFS=',' read -ra ALLOWED <<< "$ALLOWED_REPOS" | ||
| for allowed_repo in "${ALLOWED[@]}"; do | ||
| # Trim whitespace | ||
| allowed_repo=$(echo "$allowed_repo" | xargs) | ||
| if [[ "$REPO" == "$allowed_repo" ]]; then | ||
| REPO_ALLOWED=true | ||
| break | ||
| fi | ||
| done | ||
| if [ "$REPO_ALLOWED" = false ]; then | ||
| echo "❌ Repository not allowed: $REPO" | ||
| echo " Allowed repositories: $ALLOWED_REPOS" | ||
| echo "false" > /tmp/validation_failed.txt | ||
| continue | ||
| fi | ||
| echo "✅ Repository is allowed" | ||
| else | ||
| echo "ℹ️ Repository validation skipped (no allowlist configured)" | ||
| fi | ||
| # 4. Validate tag matches version pattern | ||
| if ! [[ "$TAG" =~ $VERSION_PATTERN ]]; then | ||
| echo "❌ Tag does not match version pattern: $TAG" | ||
| echo " Expected pattern: $VERSION_PATTERN" | ||
| echo " This typically means: date-based versions YY.MM.DD where YY >= 25" | ||
| echo "false" > /tmp/validation_failed.txt | ||
| continue | ||
| fi | ||
| echo "✅ Tag matches version pattern" | ||
| # 5. Verify image exists in registry (optional) | ||
| if [ "$VERIFY_EXISTENCE" = "true" ]; then | ||
| echo "Verifying image exists in registry..." | ||
| if docker manifest inspect "$image" >/dev/null 2>&1; then | ||
| echo "✅ Image exists in registry" | ||
| else | ||
| echo "❌ Image does not exist in registry: $image" | ||
| echo "false" > /tmp/validation_failed.txt | ||
| continue | ||
| fi | ||
| fi | ||
| echo "✅ Image validated successfully: $image" | ||
| echo "" | ||
| done | ||
| if [ -f /tmp/validation_failed.txt ]; then | ||
| echo "result=fail" >> "$GITHUB_OUTPUT" | ||
| exit 1 | ||
| else | ||
| echo "result=pass" >> "$GITHUB_OUTPUT" | ||
| echo "✅ All images validated successfully" | ||
| fi | ||
| finalize: | ||
| name: Final Status | ||
| runs-on: ubuntu-latest | ||
| needs: [check-team-membership, validate-changed-files, validate-image-only-changed, validate-images] | ||
| if: always() | ||
| steps: | ||
| - name: Check all validations | ||
| run: | | ||
| SHOULD_BYPASS="${{ needs.check-team-membership.outputs.should-bypass }}" | ||
| PR_AUTHOR="${{ needs.check-team-membership.outputs.pr-author }}" | ||
| MATCHED_TEAM="${{ needs.check-team-membership.outputs.matched-team }}" | ||
| echo "==================================================" | ||
| echo "Deployment Guard - Validation Summary" | ||
| echo "==================================================" | ||
| # Check if team member bypass is active | ||
| if [ "$SHOULD_BYPASS" = "true" ]; then | ||
| echo "✅ VALIDATION BYPASSED" | ||
| echo "" | ||
| echo "PR Author: $PR_AUTHOR" | ||
| echo "Team Member: YES" | ||
| echo "Matched Team: $MATCHED_TEAM" | ||
| echo "" | ||
| echo "🚀 This PR author is a member of a trusted team." | ||
| echo " All validations have been bypassed." | ||
| echo " Changes are approved based on team trust." | ||
| echo "" | ||
| echo "==================================================" | ||
| exit 0 | ||
| fi | ||
| # If not bypassed, check validation results | ||
| echo "PR Author: $PR_AUTHOR" | ||
| echo "Team Member: NO" | ||
| echo "Status: Running full validation" | ||
| echo "" | ||
| FILES_CHECK="${{ needs.validate-changed-files.outputs.allowed-files-check }}" | ||
| IMAGE_ONLY_CHECK="${{ needs.validate-image-only-changed.outputs.image-only-check }}" | ||
| IMAGE_VALIDATION="${{ needs.validate-images.outputs.image-validation }}" | ||
| ENABLE_FILE_ALLOWLIST="${{ inputs.enable_file_allowlist }}" | ||
| ENABLE_IMAGE_ONLY="${{ inputs.enable_image_only_check }}" | ||
| ENABLE_IMAGE_VALIDATION="${{ inputs.enable_image_validation }}" | ||
| echo "Enabled checks:" | ||
| echo " - File allowlist: $ENABLE_FILE_ALLOWLIST" | ||
| echo " - Image-only changes: $ENABLE_IMAGE_ONLY" | ||
| echo " - Image validation: $ENABLE_IMAGE_VALIDATION" | ||
| echo "" | ||
| echo "Results:" | ||
| if [ "$ENABLE_FILE_ALLOWLIST" = "true" ]; then | ||
| echo " - Files allowlist check: ${FILES_CHECK:-skipped}" | ||
| fi | ||
| if [ "$ENABLE_IMAGE_ONLY" = "true" ]; then | ||
| echo " - Image-only change check: ${IMAGE_ONLY_CHECK:-skipped}" | ||
| fi | ||
| if [ "$ENABLE_IMAGE_VALIDATION" = "true" ]; then | ||
| echo " - Image validation: ${IMAGE_VALIDATION:-skipped}" | ||
| fi | ||
| echo "==================================================" | ||
| # Check file allowlist (if enabled) | ||
| if [ "$ENABLE_FILE_ALLOWLIST" = "true" ] && [ "$FILES_CHECK" != "pass" ]; then | ||
| echo "❌ BLOCKED: Modified files are not in the allowlist" | ||
| echo "" | ||
| echo "Only the following files can be modified:" | ||
| echo " - ${{ inputs.allowed_files_pattern }}" | ||
| echo "" | ||
| echo "Please ensure you're only modifying allowed files." | ||
| exit 1 | ||
| fi | ||
| # Check image-only changes (if enabled) | ||
| if [ "$ENABLE_IMAGE_ONLY" = "true" ] && [ "$IMAGE_ONLY_CHECK" != "pass" ]; then | ||
| echo "❌ BLOCKED: Changes detected beyond the image field" | ||
| echo "" | ||
| echo "Only the container image field can be modified." | ||
| echo "No other changes are allowed (resources, env vars, volumes, etc.)" | ||
| echo "" | ||
| echo "Please revert any non-image changes and try again." | ||
| exit 1 | ||
| fi | ||
| # Check image validation (if enabled) | ||
| if [ "$ENABLE_IMAGE_VALIDATION" = "true" ] && [ "$IMAGE_VALIDATION" != "pass" ]; then | ||
| echo "❌ BLOCKED: Image validation failed" | ||
| echo "" | ||
| echo "Image validation requirements:" | ||
| if [ -n "${{ inputs.allowed_image_repositories }}" ]; then | ||
| echo " - Repository must be: ${{ inputs.allowed_image_repositories }}" | ||
| fi | ||
| echo " - Tag must match pattern: ${{ inputs.allowed_version_pattern }}" | ||
| if [ "${{ inputs.verify_image_existence }}" = "true" ]; then | ||
| echo " - Image must exist in the registry" | ||
| fi | ||
| echo "" | ||
| echo "Please use a valid image and try again." | ||
| exit 1 | ||
| fi | ||
| echo "✅ All enabled validations passed!" | ||
| # Show validated images if available | ||
| NEW_IMAGES='${{ needs.validate-image-only-changed.outputs.new-images }}' | ||
| if [ -n "$NEW_IMAGES" ] && [ "$NEW_IMAGES" != "[]" ]; then | ||
| echo "" | ||
| echo "The following images were validated and approved:" | ||
| echo "$NEW_IMAGES" | jq -r '.[]' 2>/dev/null | while IFS= read -r img; do | ||
| echo " ✅ $img" | ||
| done | ||
| fi | ||
| - name: Create PR comment | ||
| if: github.event_name == 'pull_request' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const shouldBypass = '${{ needs.check-team-membership.outputs.should-bypass }}' === 'true'; | ||
| const isTeamMember = '${{ needs.check-team-membership.outputs.is-team-member }}' === 'true'; | ||
| const prAuthor = '${{ needs.check-team-membership.outputs.pr-author }}'; | ||
| const matchedTeam = '${{ needs.check-team-membership.outputs.matched-team }}'; | ||
| const trustedTeams = '${{ inputs.trusted_github_teams }}'; | ||
| let commentBody = '## 🛡️ Deployment Guard Validation\n\n'; | ||
| // Team membership status | ||
| commentBody += '### 👤 Author Verification\n'; | ||
| if (shouldBypass && isTeamMember) { | ||
| commentBody += `✅ **TRUSTED TEAM MEMBER** - @${prAuthor}\n\n`; | ||
| commentBody += `- Member of: \`${matchedTeam}\`\n`; | ||
| if (trustedTeams) { | ||
| commentBody += `- Trusted teams: \`${trustedTeams}\`\n`; | ||
| } | ||
| commentBody += `- Status: 🚀 **All validations bypassed**\n`; | ||
| commentBody += `- Reason: Team members are fully trusted\n\n`; | ||
| commentBody += '---\n\n'; | ||
| commentBody += '### ✅ Validation Complete\n\n'; | ||
| commentBody += 'This PR was automatically approved based on team membership.\n\n'; | ||
| commentBody += 'No additional validation was required.\n'; | ||
| } else { | ||
| commentBody += `ℹ️ **NON-TEAM MEMBER** - @${prAuthor}\n\n`; | ||
| commentBody += `- Status: 🔒 **Full validation enforced**\n`; | ||
| commentBody += `- All deployment rules must be satisfied\n\n`; | ||
| const filesCheck = '${{ needs.validate-changed-files.outputs.allowed-files-check }}'; | ||
| const imageOnlyCheck = '${{ needs.validate-image-only-changed.outputs.image-only-check }}'; | ||
| const imageValidation = '${{ needs.validate-images.outputs.image-validation }}'; | ||
| const newImages = JSON.parse('${{ needs.validate-image-only-changed.outputs.new-images }}' || '[]'); | ||
| // Files check | ||
| commentBody += '### 📁 File Allowlist Check\n'; | ||
| if (filesCheck === 'pass') { | ||
| commentBody += '✅ **PASSED** - All modified files are in the allowlist\n\n'; | ||
| } else if (filesCheck === 'fail') { | ||
| commentBody += '❌ **FAILED** - Some files are not in the allowlist\n\n'; | ||
| commentBody += `Only files matching \`${{ inputs.allowed_files_pattern }}\` can be modified.\n\n`; | ||
| } else { | ||
| commentBody += 'ℹ️ **SKIPPED** - File allowlist check not enabled\n\n'; | ||
| } | ||
| // Image-only check | ||
| commentBody += '### 🔄 Image-Only Change Check\n'; | ||
| if (imageOnlyCheck === 'pass') { | ||
| commentBody += '✅ **PASSED** - Only image field was modified\n\n'; | ||
| } else if (imageOnlyCheck === 'fail') { | ||
| commentBody += '❌ **FAILED** - Changes detected beyond image field\n\n'; | ||
| commentBody += 'Only the dotCMS container image can be modified. No other fields are allowed.\n\n'; | ||
| } else { | ||
| commentBody += 'ℹ️ **SKIPPED** - Image-only check not enabled\n\n'; | ||
| } | ||
| // Image validation | ||
| commentBody += '### 🐳 Image Validation\n'; | ||
| if (imageValidation === 'pass') { | ||
| commentBody += '✅ **PASSED** - All images validated successfully\n\n'; | ||
| if (newImages.length > 0) { | ||
| commentBody += '**Validated images:**\n'; | ||
| newImages.forEach(img => { | ||
| commentBody += `- \`${img}\`\n`; | ||
| }); | ||
| } | ||
| } else if (imageValidation === 'fail') { | ||
| commentBody += '❌ **FAILED** - Image validation failed\n\n'; | ||
| commentBody += `**Requirements:**\n`; | ||
| if ('${{ inputs.allowed_image_repositories }}') { | ||
| commentBody += `- Repository must be: \`${{ inputs.allowed_image_repositories }}\`\n`; | ||
| } | ||
| commentBody += `- Tag must match pattern: \`${{ inputs.allowed_version_pattern }}\`\n`; | ||
| if ('${{ inputs.verify_image_existence }}' === 'true') { | ||
| commentBody += `- Image must exist in the registry\n`; | ||
| } | ||
| commentBody += '\n'; | ||
| } else { | ||
| commentBody += 'ℹ️ **SKIPPED** - Image validation not enabled\n\n'; | ||
| } | ||
| // Final status | ||
| commentBody += '\n---\n\n'; | ||
| if (filesCheck === 'pass' && imageOnlyCheck === 'pass' && imageValidation === 'pass') { | ||
| commentBody += '### ✅ All validations passed!\n\n'; | ||
| commentBody += 'This PR meets all deployment requirements.\n'; | ||
| } else { | ||
| commentBody += '### ❌ Validation failed\n\n'; | ||
| commentBody += 'Please address the issues above and push new commits to re-trigger validation.\n'; | ||
| } | ||
| } | ||
| // Post comment | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: context.issue.number, | ||
| body: commentBody | ||
| }); | ||