From 4eeadaa1fd71f2175b80ee292a0f40dee000fe34 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Thu, 2 Apr 2026 11:48:47 +0200 Subject: [PATCH 1/4] feat: add idea validation script and GitHub Actions workflow - validate-idea.sh: checks template structure, required sections, and unfilled placeholders - validate-idea.yml: triggers on PRs adding ideas/*.md, validates PR title and file format Signed-off-by: Sinduri Guntupalli --- .github/workflows/validate-idea.yml | 88 +++++++++++++++ scripts/validate-idea.sh | 167 ++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 .github/workflows/validate-idea.yml create mode 100755 scripts/validate-idea.sh diff --git a/.github/workflows/validate-idea.yml b/.github/workflows/validate-idea.yml new file mode 100644 index 00000000..bc48fa89 --- /dev/null +++ b/.github/workflows/validate-idea.yml @@ -0,0 +1,88 @@ +name: Validate Adventure Idea + +# Runs on PRs that touch ideas/*.md. +# The * glob excludes dot-folders, so ideas/.implemented/ is already filtered +# by the path pattern. We also filter it explicitly in step 4. +on: + pull_request: + paths: + - 'ideas/*.md' + +jobs: + validate: + name: Validate idea file format + runs-on: ubuntu-latest # A fresh Ubuntu VM is spun up for each run. + + steps: + + # Check PR title before checkout — fast fail with no runner cost. + - name: Check PR title format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "PR title: $PR_TITLE" + echo "$PR_TITLE" | grep -qE "^Adventure Idea: .+" || { + echo "❌ PR title must start with 'Adventure Idea: ' followed by the adventure name." + echo " Example: Adventure Idea: 📚 The Digital Codex" + exit 1 + } + echo "✅ PR title format is valid" + + # fetch-depth: 0 is required so git diff can compare against the base branch. + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # The checkout action does not restore executable bits from git. + - name: Make validation script executable + run: chmod +x scripts/validate-idea.sh + + # Only validate files *added* by this PR (not modified/deleted). + # Excludes the template and .implemented/ as a belt-and-suspenders + # on top of the workflow path filter. + # || true prevents grep from failing the step when no files match. + - name: Find added idea files + id: find-files + env: + BASE_REF: ${{ github.base_ref }} + run: | + IDEA_FILES=$( + git diff --name-only --diff-filter=A "origin/$BASE_REF" HEAD \ + | grep "^ideas/" \ + | grep "\.md$" \ + | grep -v "adventure-idea-template.md" \ + | grep -v "\.implemented/" \ + || true + ) + + if [[ -z "$IDEA_FILES" ]]; then + echo "No new idea files detected in this PR — nothing to validate." + else + echo "New idea files to validate:" + echo "$IDEA_FILES" + fi + + { + echo "IDEA_FILES<> "$GITHUB_ENV" + + - name: Validate idea files + run: | + set -euo pipefail + + if [[ -z "$IDEA_FILES" ]]; then + echo "No new idea files to validate. Skipping." + exit 0 + fi + + while IFS= read -r file; do + # Skip blank lines that can appear from the heredoc. + [[ -z "$file" ]] && continue + ./scripts/validate-idea.sh "$file" + done <<< "$IDEA_FILES" + + echo "" + echo "✅ All idea files passed validation." diff --git a/scripts/validate-idea.sh b/scripts/validate-idea.sh new file mode 100755 index 00000000..7b7c4c54 --- /dev/null +++ b/scripts/validate-idea.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# validate-idea.sh — Checks that an adventure idea file follows the required template format. +# +# Usage: +# ./scripts/validate-idea.sh ideas/my-idea.md +# +# Exit codes: +# 0 — all checks passed +# 1 — one or more checks failed + +# --------------------------------------------------------------------------- +# Strict mode +# --------------------------------------------------------------------------- +# -e is intentionally NOT set — we want all checks to run and collect every +# failure before exiting, so the contributor sees everything at once. +set -uo pipefail + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +ERRORS=() + +# fail — collects a failure; execution continues so all checks run +# hard_fail — for fatal pre-conditions where continuing makes no sense +fail() { + ERRORS+=("❌ $1") +} + +pass() { + echo "✅ $1" +} + +hard_fail() { + echo "❌ $1" >&2 + exit 1 +} + +# --------------------------------------------------------------------------- +# Argument handling +# --------------------------------------------------------------------------- +[[ $# -ne 1 ]] && hard_fail "Usage: $0 " + +FILE="$1" +[[ -f "$FILE" ]] || hard_fail "File not found: $FILE" + +echo "" +echo "Validating: $FILE" +echo "-------------------------------------------" + +# --------------------------------------------------------------------------- +# Check 1 — File guard +# --------------------------------------------------------------------------- +# The workflow path filter handles this in CI, but local runs have no filter. +[[ "$FILE" == "ideas/adventure-idea-template.md" ]] \ + && hard_fail "Do not validate the template file itself." + +[[ "$FILE" =~ ideas/\.implemented/ ]] \ + && hard_fail "Skipping already-implemented idea: $FILE" + +pass "File guard passed" + +# --------------------------------------------------------------------------- +# Check 2 — Required top-level sections +# --------------------------------------------------------------------------- +ERRORS_BEFORE=${#ERRORS[@]} + +grep -q "^## Overview" "$FILE" \ + || fail "Missing required section: '## Overview' — use ATX heading style (## Overview), not bold text (**Overview**) or underline-style headings" + +grep -q "^## Levels" "$FILE" \ + || fail "Missing required section: '## Levels' — use ATX heading style (## Levels), not bold text or underline-style headings" + +[[ ${#ERRORS[@]} -eq $ERRORS_BEFORE ]] && pass "Required top-level sections present" + +# --------------------------------------------------------------------------- +# Check 3 — Required Overview fields +# --------------------------------------------------------------------------- +ERRORS_BEFORE=${#ERRORS[@]} + +grep -q "^\*\*Theme:\*\*" "$FILE" \ + || fail "Missing required Overview field: '**Theme:**'" + +grep -q "^\*\*Skills:\*\*" "$FILE" \ + || fail "Missing required Overview field: '**Skills:**'" + +grep -q "^\*\*Technologies:\*\*" "$FILE" \ + || fail "Missing required Overview field: '**Technologies:**'" + +[[ ${#ERRORS[@]} -eq $ERRORS_BEFORE ]] && pass "Required Overview fields present" + +# --------------------------------------------------------------------------- +# Check 4 — At least one difficulty level heading +# --------------------------------------------------------------------------- +ERRORS_BEFORE=${#ERRORS[@]} + +grep -qE "^### (🟢|🟡|🔴)" "$FILE" \ + || fail "No level headings found. Expected at least one '### 🟢', '### 🟡', or '### 🔴' heading — use ATX heading style (### 🟢 Beginner: Name), not bold text or underline-style headings" + +[[ ${#ERRORS[@]} -eq $ERRORS_BEFORE ]] && pass "At least one difficulty level heading found" + +# --------------------------------------------------------------------------- +# Check 5 — Required H4 subsections (global presence check) +# --------------------------------------------------------------------------- +# Confirms all six required subsection types appear somewhere in the file. +# Global check only — won't catch a level missing a section if another has it. + +ERRORS_BEFORE=${#ERRORS[@]} + +HINT="— use ATX heading style (#### Section Name), not bold text or underline-style headings" + +grep -q "^#### Description" "$FILE" \ + || fail "Missing required subsection: '#### Description' $HINT" + +grep -q "^#### Story" "$FILE" \ + || fail "Missing required subsection: '#### Story' $HINT" + +grep -q "^#### The Problem" "$FILE" \ + || fail "Missing required subsection: '#### The Problem' $HINT" + +grep -q "^#### Objective" "$FILE" \ + || fail "Missing required subsection: '#### Objective' $HINT" + +grep -q "^#### What You'll Learn" "$FILE" \ + || fail "Missing required subsection: '#### What You'll Learn' $HINT" + +grep -q "^#### Tools & Infrastructure" "$FILE" \ + || fail "Missing required subsection: '#### Tools & Infrastructure' $HINT" + +[[ ${#ERRORS[@]} -eq $ERRORS_BEFORE ]] && pass "All required level sections present" + +# --------------------------------------------------------------------------- +# Check 6 — No unfilled template placeholders (Python one-liner) +# --------------------------------------------------------------------------- +# Shell regex can't cleanly distinguish [placeholder] from [link text](url). +# Python's negative lookahead (?!\() handles this: matches [text] not followed +# by '(', which filters out Markdown links. [x] and [ ] (checkboxes) are excluded. + +ERRORS_BEFORE=${#ERRORS[@]} + +PLACEHOLDER_OUTPUT=$(python3 -c " +import re, sys +content = open(sys.argv[1]).read() +hits = [m for m in re.findall(r'\[([^\]\n]+)\](?!\()', content) if m not in ('x', ' ')] +if hits: + print(' '.join(hits)) + sys.exit(1) +" "$FILE") || fail "Unfilled template placeholders — replace: $PLACEHOLDER_OUTPUT" + +[[ ${#ERRORS[@]} -eq $ERRORS_BEFORE ]] && pass "No unfilled template placeholders" + +# --------------------------------------------------------------------------- +# Final summary +# --------------------------------------------------------------------------- +echo "" +if [[ ${#ERRORS[@]} -gt 0 ]]; then + echo "Found ${#ERRORS[@]} problem(s) in: $FILE" + echo "" + for msg in "${ERRORS[@]}"; do + echo " $msg" + done + echo "" + exit 1 +else + echo "✅ All checks passed for: $FILE" + echo "" +fi From 913920a3742d4ff5a120eacce7bbb5b0530ac4d3 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Thu, 2 Apr 2026 11:51:35 +0200 Subject: [PATCH 2/4] docs: clarify level block deletion instruction in template Signed-off-by: Sinduri Guntupalli --- ideas/adventure-idea-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ideas/adventure-idea-template.md b/ideas/adventure-idea-template.md index 9ee4da79..36b1c83f 100644 --- a/ideas/adventure-idea-template.md +++ b/ideas/adventure-idea-template.md @@ -41,7 +41,7 @@