1- name : Community Profile Validation
1+ name : Showcase Validation & Preview
22
33on :
4- pull_request :
5- types : [labeled, synchronize]
4+ pull_request_target :
5+ types : [labeled, synchronize, opened, reopened, closed ]
66 paths :
77 - " community/**"
88
9- # Read- only permissions — no secrets access
9+ # Top-level: read- only. Individual jobs escalate as needed.
1010permissions :
1111 contents : read
1212 pull-requests : read
1313
14+ concurrency :
15+ group : showcase-${{ github.event.pull_request.number }}
16+ cancel-in-progress : true
17+
1418jobs :
19+ # ── Step 1: Validate the community submission ──
1520 validate :
16- if : contains(github.event.pull_request.labels.*.name, 'showcase')
21+ if : >-
22+ github.event.action != 'closed' &&
23+ contains(github.event.pull_request.labels.*.name, 'showcase')
1724 runs-on : ubuntu-latest
1825 # Fork PRs require manual approval via the 'showcase' environment
1926 environment : ${{ github.event.pull_request.head.repo.full_name != github.repository && 'showcase' || '' }}
27+ outputs :
28+ folder : ${{ steps.validate.outputs.folder }}
2029 steps :
2130 - uses : actions/checkout@v6
2231 with :
32+ ref : ${{ github.event.pull_request.head.sha }}
2333 fetch-depth : 0
2434
2535 - name : Get changed files
2636 id : changed
2737 run : |
28- BASE=${{ github.event.pull_request.base.sha }}
29- HEAD=${{ github.event.pull_request.head.sha }}
30- FILES=$(git diff --name-only "$BASE" "$HEAD")
38+ git fetch origin ${{ github.event.pull_request.base.sha }} --depth=1
39+ FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
3140 echo "files<<EOF" >> "$GITHUB_OUTPUT"
3241 echo "$FILES" >> "$GITHUB_OUTPUT"
3342 echo "EOF" >> "$GITHUB_OUTPUT"
@@ -92,38 +101,31 @@ jobs:
92101 if [ -f "community/$FOLDER/README.md" ]; then
93102 README="community/$FOLDER/README.md"
94103
95- # Check frontmatter exists
96104 if ! head -1 "$README" | grep -q '^---$'; then
97105 ERRORS+=("README.md missing frontmatter (must start with ---)")
98106 else
99- # Extract frontmatter
100107 FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$README" | sed '1d;$d')
101108
102- # Required fields
103109 for FIELD in author description category tags profiles; do
104110 if ! echo "$FRONTMATTER" | grep -q "^${FIELD}:"; then
105111 ERRORS+=("README.md frontmatter missing required field: $FIELD")
106112 fi
107113 done
108114
109- # Author in frontmatter must match folder prefix
110115 AUTHOR=$(echo "$FRONTMATTER" | grep '^author:' | sed 's/^author: *//')
111116 FOLDER_PREFIX=$(echo "$FOLDER" | cut -d'-' -f1)
112117 if [ -n "$AUTHOR" ] && [ "$AUTHOR" != "$FOLDER_PREFIX" ]; then
113118 ERRORS+=("Author '$AUTHOR' in frontmatter does not match folder prefix '$FOLDER_PREFIX'")
114119 fi
115120
116- # Author in frontmatter must match PR author (GitHub username)
117121 if [ -n "$AUTHOR" ] && [ -n "$PR_AUTHOR" ]; then
118- # Case-insensitive comparison (GitHub handles are case-insensitive)
119122 AUTHOR_LOWER=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
120123 PR_AUTHOR_LOWER=$(echo "$PR_AUTHOR" | tr '[:upper:]' '[:lower:]')
121124 if [ "$AUTHOR_LOWER" != "$PR_AUTHOR_LOWER" ]; then
122125 ERRORS+=("Author '$AUTHOR' in frontmatter does not match PR author '$PR_AUTHOR'. You can only submit profiles under your own GitHub handle.")
123126 fi
124127 fi
125128
126- # Category must be one of the allowed values
127129 CATEGORY=$(echo "$FRONTMATTER" | grep '^category:' | sed 's/^category: *//')
128130 ALLOWED="git docker rust k8s python node misc"
129131 if [ -n "$CATEGORY" ] && ! echo "$ALLOWED" | grep -qw "$CATEGORY"; then
@@ -132,29 +134,27 @@ jobs:
132134 fi
133135 fi
134136
135- # ── Rule 8: profiles.toml must be valid TOML structure ──
137+ # ── Rule 8: profiles.toml structure ──
136138 if [ -f "community/$FOLDER/profiles.toml" ]; then
137139 if ! grep -q '\[\[profiles\]\]\|^\[aliases\]' "community/$FOLDER/profiles.toml"; then
138140 ERRORS+=("profiles.toml does not look like a valid am export (missing [[profiles]] or [aliases] section)")
139141 fi
140142 fi
141143
142- # ── Rule 9: Scan for suspicious content in profiles.toml ──
144+ # ── Rule 9: Scan for suspicious content ──
143145 if [ -f "community/$FOLDER/profiles.toml" ]; then
144- # Check for TOML unicode escapes that produce control characters
145146 if grep -qP '\\u\{?00[0-1][0-9a-fA-F]\}?|\\u\{?007[fF]\}?|\\u\{?00[89a-fA-F][0-9a-fA-F]\}?' "community/$FOLDER/profiles.toml" 2>/dev/null; then
146147 WARNINGS+=("profiles.toml contains unicode escape sequences that may produce control characters. Maintainer: please inspect carefully.")
147148 fi
148- # Check for raw control characters (bytes 0x00-0x1F except \t \n \r, and 0x7F)
149149 if grep -qP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' "community/$FOLDER/profiles.toml" 2>/dev/null; then
150- ERRORS+=("SECURITY: profiles.toml contains raw control characters. This may be an attempt to inject malicious terminal commands. ")
150+ ERRORS+=("SECURITY: profiles.toml contains raw control characters.")
151151 fi
152152 fi
153153
154154 # ── Rule 10: Must not modify other people's folders ──
155155 OTHER_FOLDERS=$(echo "$FILES" | grep '^community/' | cut -d'/' -f2 | sort -u | grep -v "^${FOLDER}$" || true)
156156 if [ -n "$OTHER_FOLDERS" ]; then
157- ERRORS+=("SECURITY: Changes detected in other contributor folders: $OTHER_FOLDERS. You may only modify your own folder. ")
157+ ERRORS+=("SECURITY: Changes detected in other contributor folders: $OTHER_FOLDERS.")
158158 fi
159159
160160 # ── Report ──
@@ -163,14 +163,12 @@ jobs:
163163 for WARN in "${WARNINGS[@]}"; do
164164 echo " ⚠ $WARN"
165165 done
166- echo ""
167166 fi
168167
169168 if [ ${#ERRORS[@]} -gt 0 ]; then
170169 echo "::error::Community profile validation failed"
171170 echo ""
172171 echo "❌ Validation failed with ${#ERRORS[@]} error(s):"
173- echo ""
174172 for ERR in "${ERRORS[@]}"; do
175173 echo " • $ERR"
176174 done
@@ -191,7 +189,45 @@ jobs:
191189 - name : Verify import
192190 run : |
193191 FOLDER="${{ steps.validate.outputs.folder }}"
194- TOML="community/$FOLDER/profiles.toml"
195- echo "Testing import of $TOML..."
196- ./target/release/am import "$TOML" --yes
192+ echo "Testing import of community/$FOLDER/profiles.toml..."
193+ ./target/release/am import "community/$FOLDER/profiles.toml" --yes
197194 echo "✅ Import verification passed"
195+
196+ # ── Step 2: Build & deploy preview (only after validation passes) ──
197+ preview :
198+ needs : validate
199+ uses : ./.github/workflows/reusable-preview-website.yml
200+ with :
201+ pr_number : ${{ github.event.pull_request.number }}
202+ head_sha : ${{ github.event.pull_request.head.sha }}
203+ is_fork : ${{ github.event.pull_request.head.repo.full_name != github.repository }}
204+ permissions :
205+ contents : write
206+ pull-requests : write
207+
208+ # ── Cleanup on PR close ──
209+ cleanup :
210+ if : github.event.action == 'closed'
211+ runs-on : ubuntu-latest
212+ permissions :
213+ contents : write
214+ steps :
215+ - name : Checkout gh-pages branch
216+ uses : actions/checkout@v6
217+ with :
218+ ref : gh-pages
219+
220+ - name : Remove preview
221+ run : |
222+ PR_NUMBER=${{ github.event.pull_request.number }}
223+
224+ if [ -d "_preview/${PR_NUMBER}" ]; then
225+ rm -rf "_preview/${PR_NUMBER}"
226+ git add -A
227+ git config user.name "github-actions[bot]"
228+ git config user.email "github-actions[bot]@users.noreply.github.com"
229+ git commit -m "cleanup: remove preview for PR #${PR_NUMBER}"
230+ git push
231+ else
232+ echo "No preview to clean up"
233+ fi
0 commit comments