-
Notifications
You must be signed in to change notification settings - Fork 0
Extract CI validation into reusable scripts #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
240ee48
6fc274c
183ecc4
959a08a
2517bb3
fe513d7
a5a0149
ebd46a4
ff4543d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .claude/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| .PHONY: format validate audit ci | ||
|
|
||
| format: ## Format skillshare-hub.json (sort by name, 2-space indent) | ||
| @./scripts/format.sh | ||
|
|
||
| validate: ## Validate skillshare-hub.json format and rules | ||
| @./scripts/validate.sh | ||
|
|
||
| audit: ## Audit new/changed skills against main | ||
| @./scripts/audit.sh main | ||
|
|
||
| ci: validate audit ## Run full CI locally |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,129 @@ | ||||||
| #!/usr/bin/env bash | ||||||
| set -euo pipefail | ||||||
|
|
||||||
| HUB_FILE="skillshare-hub.json" | ||||||
| BASE_REF="${1:-${BASE_SHA:-main}}" | ||||||
| REPORT_FILE="${AUDIT_REPORT:-}" | ||||||
|
|
||||||
| # Detect environment | ||||||
| is_ci() { [ "${CI:-}" = "true" ]; } | ||||||
| log_group() { is_ci && echo "::group::$1" || echo "=== $1 ==="; } | ||||||
| log_endgroup() { is_ci && echo "::endgroup::" || true; } | ||||||
| log_error() { is_ci && echo "::error::$1" || echo "ERROR: $1"; } | ||||||
|
|
||||||
| # --- Find new/changed sources --- | ||||||
| base_sources=$(git show "${BASE_REF}:${HUB_FILE}" 2>/dev/null \ | ||||||
| | jq -r '.skills[] | "\(.name)|\(.source)"' | sort) || base_sources="" | ||||||
|
|
||||||
| pr_sources=$(jq -r '.skills[] | "\(.name)|\(.source)"' "$HUB_FILE" | sort) | ||||||
|
|
||||||
| new_entries=$(comm -13 <(echo "$base_sources") <(echo "$pr_sources")) | ||||||
| new_sources=$(echo "$new_entries" | cut -d'|' -f2 | sort -u) | ||||||
|
|
||||||
| if [ -z "$new_entries" ]; then | ||||||
| echo "No new or changed skill sources to audit" | ||||||
| [ -n "$REPORT_FILE" ] && echo "No new or changed skill sources to audit." > "$REPORT_FILE" | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| echo "New/changed sources to audit:" | ||||||
| echo "$new_sources" | ||||||
| echo "" | ||||||
|
|
||||||
| # --- Check skillshare is available --- | ||||||
| if ! command -v skillshare &>/dev/null; then | ||||||
| echo "ERROR: skillshare CLI not found. Install from https://github.com/runkids/skillshare" | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| # --- Audit each source --- | ||||||
| failed=0 | ||||||
| results=() | ||||||
| details=() | ||||||
|
|
||||||
| while IFS= read -r source; do | ||||||
| [ -z "$source" ] && continue | ||||||
|
|
||||||
| if [[ "$source" == http* ]]; then | ||||||
| clone_url="$source" | ||||||
| else | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a predictable temporary directory name can lead to race conditions if multiple instances of this script run concurrently. It's safer to use
Suggested change
|
||||||
| clone_url="https://github.com/${source}.git" | ||||||
| fi | ||||||
|
|
||||||
| safe_name=$(echo "$source" | tr '/' '-') | ||||||
| clone_dir="/tmp/audit-${safe_name}" | ||||||
|
|
||||||
| log_group "Auditing ${source}" | ||||||
|
|
||||||
| if ! clone_output=$(git clone --depth 1 "$clone_url" "$clone_dir" 2>&1); then | ||||||
| log_error "Failed to clone ${source}" | ||||||
| results+=("| \`${source}\` | :x: Clone failed | - |") | ||||||
| details+=("DETAIL_SEP### \`${source}\`"$'\n'"Clone failed:"$'\n'"\`\`\`"$'\n'"${clone_output}"$'\n'"\`\`\`") | ||||||
| failed=1 | ||||||
| log_endgroup | ||||||
| continue | ||||||
| fi | ||||||
|
|
||||||
| audit_output=$(skillshare audit "$clone_dir" --threshold high 2>&1 | sed 's/\x1b\[[0-9;]*m//g') || true | ||||||
|
|
||||||
| echo "$audit_output" | ||||||
|
|
||||||
| # Extract risk score and label | ||||||
| risk=$(echo "$audit_output" | sed -n 's/.*Risk: \([A-Z]* ([0-9]*\/[0-9]*)\).*/\1/p' | tail -1) | ||||||
| [ -z "$risk" ] && risk="N/A" | ||||||
| risk_label=$(echo "$risk" | awk '{print $1}') | ||||||
|
|
||||||
| if echo "$audit_output" | grep -q "config not found"; then | ||||||
| results+=("| \`${source}\` | :x: No config | - |") | ||||||
| details+=("DETAIL_SEP### \`${source}\`"$'\n'"No skillshare config found. Run \`skillshare init\` in the source repo.") | ||||||
| log_error "No skillshare config found for ${source}. Run 'skillshare init' in the source repo." | ||||||
| failed=1 | ||||||
| elif [[ "$risk_label" == "HIGH" || "$risk_label" == "CRITICAL" ]]; then | ||||||
| results+=("| \`${source}\` | :x: Risk ${risk_label} | ${risk} |") | ||||||
| details+=("DETAIL_SEP### \`${source}\`"$'\n'"\`\`\`"$'\n'"${audit_output}"$'\n'"\`\`\`") | ||||||
| log_error "Risk ${risk_label} for ${source}" | ||||||
| failed=1 | ||||||
| else | ||||||
| results+=("| \`${source}\` | :white_check_mark: Passed | ${risk} |") | ||||||
| details+=("DETAIL_SEP### \`${source}\`"$'\n'"\`\`\`"$'\n'"${audit_output}"$'\n'"\`\`\`") | ||||||
| fi | ||||||
|
|
||||||
| rm -rf "$clone_dir" | ||||||
| log_endgroup | ||||||
| done <<< "$new_sources" | ||||||
|
|
||||||
| # --- Generate report --- | ||||||
| if [ -n "$REPORT_FILE" ]; then | ||||||
| { | ||||||
| echo "## Skill Audit Results" | ||||||
| echo "" | ||||||
| echo "| Source | Status | Risk |" | ||||||
| echo "|--------|--------|------|" | ||||||
| for row in "${results[@]}"; do | ||||||
| echo "$row" | ||||||
| done | ||||||
| echo "" | ||||||
| if [ "$failed" -ne 0 ]; then | ||||||
| echo "> **Failed**: One or more skills did not pass the security audit." | ||||||
| else | ||||||
| echo "> **All skills passed** the security audit." | ||||||
| fi | ||||||
| echo "" | ||||||
| echo "<details>" | ||||||
| echo "<summary>Audit Details</summary>" | ||||||
| echo "" | ||||||
| for detail in "${details[@]}"; do | ||||||
| echo "${detail}" | sed 's/^DETAIL_SEP//' | ||||||
| echo "" | ||||||
| done | ||||||
| echo "</details>" | ||||||
| } > "$REPORT_FILE" | ||||||
| fi | ||||||
|
|
||||||
| if [ "$failed" -ne 0 ]; then | ||||||
| echo "" | ||||||
| log_error "One or more skills failed the security audit. Please fix the flagged issues and push again." | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| echo "All new skills passed the security audit" | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| HUB_FILE="${1:-skillshare-hub.json}" | ||
|
|
||
| if [ ! -f "$HUB_FILE" ]; then | ||
| echo "ERROR: $HUB_FILE not found" | ||
| exit 1 | ||
| fi | ||
|
|
||
| jq '.skills |= sort_by(.name)' "$HUB_FILE" > "${HUB_FILE}.tmp" \ | ||
| && mv "${HUB_FILE}.tmp" "$HUB_FILE" | ||
|
|
||
| echo "Formatted: $HUB_FILE ($(jq '.skills | length' "$HUB_FILE") skills, sorted by name)" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||
|
|
||||||||||||||||||||
| HUB_FILE="${1:-skillshare-hub.json}" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if [ ! -f "$HUB_FILE" ]; then | ||||||||||||||||||||
| echo "ERROR: $HUB_FILE not found" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Check valid JSON | ||||||||||||||||||||
| if ! jq empty "$HUB_FILE" 2>/dev/null; then | ||||||||||||||||||||
| echo "ERROR: Invalid JSON in $HUB_FILE" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| echo "OK: valid JSON" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Check required fields | ||||||||||||||||||||
| missing=$(jq -r '.skills[] | select(.name == "" or .name == null or .description == "" or .description == null or .source == "" or .source == null) | .name // "unnamed"' "$HUB_FILE") | ||||||||||||||||||||
| if [ -n "$missing" ]; then | ||||||||||||||||||||
| echo "ERROR: Skills missing required fields: $missing" | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| echo "OK: required fields present" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Check no duplicate names | ||||||||||||||||||||
| dupes=$(jq -r '[.skills[].name] | group_by(.) | map(select(length > 1)) | flatten | .[]' "$HUB_FILE") | ||||||||||||||||||||
| if [ -n "$dupes" ]; then | ||||||||||||||||||||
| echo "ERROR: Duplicate skill names: $dupes" | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| echo "OK: no duplicate names" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Check name format (lowercase, hyphens only) | ||||||||||||||||||||
| bad_names=$(jq -r '.skills[].name | select(test("^[a-z0-9][a-z0-9-]*$") | not)' "$HUB_FILE") | ||||||||||||||||||||
| if [ -n "$bad_names" ]; then | ||||||||||||||||||||
| echo "ERROR: Invalid skill names (must be lowercase, hyphens): $bad_names" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
|
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there are multiple invalid skill names, they will be displayed on a single line, making the list difficult to parse. Displaying each invalid name on a new line would be more readable.
Suggested change
|
||||||||||||||||||||
| echo "OK: name format valid" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Check formatting (sorted by name, jq default 2-space indent) | ||||||||||||||||||||
| expected=$(jq '.skills |= sort_by(.name)' "$HUB_FILE") | ||||||||||||||||||||
| actual=$(cat "$HUB_FILE") | ||||||||||||||||||||
| if [ "$expected" != "$actual" ]; then | ||||||||||||||||||||
| echo "ERROR: $HUB_FILE is not formatted. Run: make format" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| echo "OK: formatting correct" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| echo "" | ||||||||||||||||||||
| echo "Validation passed: $(jq '.skills | length' "$HUB_FILE") skills" | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script prints the contents of the
new_sourcesvariable directly to the console.new_sourcesis derived from thesourcefield inskillshare-hub.json, which is untrusted input from a pull request. If a malicioussourcecontains a newline followed by a GitHub Actions workflow command (e.g.,::error::), the command will be executed by the GitHub Actions runner. This can be used to spoof CI results or manipulate the workflow.