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
126 changes: 126 additions & 0 deletions .github/workflows/sync-adventure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Sync Adventure from Challenges Repo

on:
workflow_dispatch:
inputs:
adventure_url:
description: >
URL of the adventure folder in the challenges repo
(e.g. https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta)
required: true
type: string

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Install dependencies
run: npm ci

- name: Validate URL
env:
ADVENTURE_URL: ${{ inputs.adventure_url }}
run: |
if ! echo "$ADVENTURE_URL" | grep -qE '^https://github\.com/[^/]+/[^/]+/tree/[^/]+/adventures/'; then
echo "Error: URL must point to an adventure folder under adventures/ in the challenges repo"
echo "Example: https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta"
exit 1
fi

- name: Fetch and transform adventure YAML
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ADVENTURE_URL: ${{ inputs.adventure_url }}
run: node scripts/sync-adventure.mjs

- name: Validate generated YAML
run: node scripts/generate-adventures.mjs --validate-only

- name: Regenerate TypeScript
run: node scripts/generate-adventures.mjs

- name: Create pull request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SLUG=$(cat /tmp/adventure-slug)
NAME=$(cat /tmp/adventure-name)
LEVELS=$(cat /tmp/adventure-levels)
MODE=$(cat /tmp/adventure-mode)
BRANCH="feat/adventure-${SLUG}"
SOURCE_URL="${{ inputs.adventure_url }}"

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git add src/data/adventures/${SLUG}/
git commit -m "feat: sync adventure ${SLUG} from challenges repo (${MODE})"

git push origin --delete "$BRANCH" 2>/dev/null || true
git push origin "$BRANCH"

if [ "$MODE" = "update" ]; then
PR_TITLE="feat(adventure): add levels to ${NAME} — ${LEVELS}"
else
PR_TITLE="feat(adventure): add ${NAME}"
fi

{
printf '## %s: %s\n\n' "$MODE" "$NAME"
printf 'Auto-synced from the challenges repo.\n\n'
printf '**Source:** %s\n\n' "$SOURCE_URL"
printf '**Levels in this PR:** `%s`\n\n' "$LEVELS"
printf '---\n\n'
printf '### Before merging\n\n'
printf -- '- [ ] Add `contributor:` block to `src/data/adventures/%s/adventure.yaml`\n' "$SLUG"
printf -- ' ```yaml\n'
printf -- ' contributor:\n'
printf -- ' name: "Full Name"\n'
printf -- ' url: "https://example.com"\n'
printf -- ' about: "One sentence bio."\n'
printf -- ' ```\n'
printf -- '- [ ] Confirm `month:` is correct for the planned release\n'
printf -- '- [ ] Update `rewards.deadline:` from `TODO` to ISO 8601 (e.g. `2026-07-01T23:59:00+01:00`)\n'
printf -- '- [ ] Review `topics:` on each level — auto-set to all adventure tags, refine to level-specific subset if needed\n'
printf -- '- [ ] Update `community_url:` in each level once the Discourse threads are created\n'
printf -- '- [ ] Update `discussionUrl` in each `*-posts.json` stub\n\n'
printf '### Routes and config (manual steps)\n\n'
printf -- '- [ ] Add adventure and level routes to `src/routes.ts`\n'
printf -- '- [ ] Add URLs to `public/sitemap.xml`\n'
printf -- '- [ ] Add URLs to the `prerender` array in `react-router.config.ts`\n'
printf -- '- [ ] Add adventure to `ADVENTURE_CATEGORIES` in `scripts/refresh-leaderboard.mjs`, then run `node scripts/refresh-leaderboard.mjs`\n'
printf -- '- [ ] Run `node scripts/refresh-discussions.mjs`\n'
printf -- '- [ ] Add each level URL to `ROUTES` in `e2e/smoke.spec.ts` and `src/test/seo.test.ts`\n'
printf -- '- [ ] Add each level to `pages` array in `src/test/prerender.test.ts` with expected `<title>`\n'
printf -- '- [ ] Update routes table in `README.md`\n\n'
printf '### Checks\n\n'
printf '```sh\n'
printf 'npm run lint && npm test && npm run build && npm run test:e2e\n'
printf '```\n'
} > /tmp/pr-body.md

EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING_PR" ]; then
echo "Updating existing PR #${EXISTING_PR}"
gh pr edit "$EXISTING_PR" --title "$PR_TITLE" --body-file /tmp/pr-body.md
else
gh pr create \
--title "$PR_TITLE" \
--body-file /tmp/pr-body.md \
--head "$BRANCH" \
--base main
fi
63 changes: 49 additions & 14 deletions schemas/adventure.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@
"title": "Adventure",
"description": "Schema for an OffOn adventure YAML file.",
"type": "object",
"required": ["slug", "name", "month", "story", "technologies", "levels"],
"required": ["slug", "title", "month", "tags", "levels"],
"additionalProperties": false,
"properties": {
"slug": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
"description": "Kebab-case URL slug. Must match the folder name."
},
"title": {
"type": "string",
"description": "Display title for the adventure."
},
"name": {
"type": "string",
"description": "Display name for the adventure."
"description": "Alias for title. Use title or name, not both."
},
"emoji": {
"type": "string",
"description": "Emoji representing this adventure (e.g. '⚖️'). The generator maps it to a Lucide icon automatically. Use this OR icon, not both."
},
"icon": {
"type": "string",
"description": "Lucide React icon name representing this adventure (e.g. 'FlaskConical'). Used as a visual identity marker in the adventure header."
"description": "Lucide React icon name (e.g. 'FlaskConical'). Overrides emoji-derived icon when both are present."
},
"month": {
"type": "string",
Expand All @@ -29,11 +37,15 @@
"type": "string",
"description": "One-paragraph summary of the adventure."
},
"technologies": {
"story": {
"type": "string",
"description": "One-paragraph card summary. Omit to derive from backstory[0]."
},
"tags": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Technology names used in this adventure (e.g. ['OpenFeature', 'Spring Boot'])."
"description": "Technology tags (e.g. ['OpenFeature', 'Spring Boot'])."
},
"contributor": {
"$ref": "#/$defs/contributor"
Expand Down Expand Up @@ -79,7 +91,7 @@
"required": ["deadline", "tiers"],
"additionalProperties": false,
"properties": {
"deadline": { "type": "string", "description": "ISO 8601 datetime (e.g. '2026-05-26T23:59:00+01:00')." },
"deadline": { "type": "string", "description": "ISO 8601 datetime (e.g. '2026-05-26T23:59:00+01:00'). Use 'TODO' as a placeholder — the generator will warn and skip formatting." },
"eligibility": { "type": "string", "description": "Omit to use the standard eligibility text." },
"tiers": {
"type": "array",
Expand Down Expand Up @@ -111,14 +123,20 @@
},
"level": {
"type": "object",
"required": ["id", "name", "difficulty", "topics", "learnings", "devcontainer_path", "discussion_url", "intro", "objective", "toolbox", "how_to_play", "verification"],
"required": ["level", "name", "topics", "devcontainer", "objective", "toolbox", "how_to_play", "verification"],
"additionalProperties": false,
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"level": { "type": "string", "description": "Level identifier: beginner, intermediate, or expert." },
"name": { "type": "string", "description": "Display name for the level." },
"title": { "type": "string", "description": "Alias for name. Use name or title, not both." },
"emoji": {
"type": "string",
"description": "Difficulty emoji: 🟢 Beginner, 🟡 Intermediate, 🔴 Expert. Use this OR difficulty, not both."
},
"difficulty": {
"type": "string",
"enum": ["Beginner", "Intermediate", "Expert"]
"enum": ["Beginner", "Intermediate", "Expert"],
"description": "Explicit difficulty. Omit when using emoji."
},
"topics": {
"type": "array",
Expand All @@ -127,24 +145,40 @@
"learnings": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
"minItems": 1,
"description": "Key learnings. Use this OR what_you_learn."
},
"devcontainer_path": {
"what_you_learn": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Alias for learnings."
},
"devcontainer": {
"type": "string",
"description": "Path to the devcontainer.json relative to the challenges repo root (e.g. '.devcontainer/04-blind-by-design_01-beginner/devcontainer.json'). The codegen script builds the full Codespaces URL from this."
"description": "Short devcontainer name (e.g. 'lex-imperfecta_beginner'). Generator expands to '.devcontainer/{value}/devcontainer.json'."
},
"discussion_url": {
"type": "string",
"description": "Full Discourse topic URL or a path relative to COMMUNITY_URL (e.g. '/t/topic-slug/1419')."
},
"community_url": {
"type": "string",
"description": "Alias for discussion_url."
},
"deadline": {
"type": "string",
"description": "Submission deadline for this level as an ISO 8601 string (e.g. '2025-12-10T09:00:00+01:00'). Only shown when rewards are active."
},
"hook": { "type": "string" },
"summary": {
"type": "string",
"description": "Alias for intro. Single string; generator wraps in array."
},
"intro": {
"type": "array",
"items": { "type": "string" }
"items": { "type": "string" },
"description": "Brief intro paragraph(s). Use this OR summary."
},
"backstory": {
"type": "array",
Expand Down Expand Up @@ -205,6 +239,7 @@
"required": ["title", "content"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "description": "Optional step identifier from the challenges repo. Ignored by the generator." },
"title": { "type": "string" },
"content": { "type": "string", "description": "Markdown content. Can contain code blocks." }
}
Expand Down
Loading
Loading