diff --git a/schemas/adventure.schema.json b/schemas/adventure.schema.json index cabdc3f9..22553a71 100644 --- a/schemas/adventure.schema.json +++ b/schemas/adventure.schema.json @@ -115,7 +115,7 @@ }, "level": { "type": "object", - "required": ["id", "name", "difficulty", "learnings", "devcontainerPath", "discussionUrl"], + "required": ["id", "name", "difficulty", "topics", "learnings", "devcontainerPath", "discussionUrl", "intro", "objective", "toolbox", "howToPlay", "verification"], "additionalProperties": false, "properties": { "id": { "type": "string" }, diff --git a/scripts/generate-adventures.mjs b/scripts/generate-adventures.mjs index a9e51a28..833ec2af 100644 --- a/scripts/generate-adventures.mjs +++ b/scripts/generate-adventures.mjs @@ -136,11 +136,17 @@ function validateAdventure(data, id) { if (!["Beginner", "Intermediate", "Expert"].includes(level.difficulty)) { errors.push(`${prefix}: Invalid difficulty "${level.difficulty}"`); } + if (!level.topics || level.topics.length === 0) errors.push(`${prefix}: Missing topics`); if (!level.learnings || level.learnings.length === 0) { errors.push(`${prefix}: Missing learnings`); } if (!level.devcontainerPath) errors.push(`${prefix}: Missing devcontainerPath`); if (!level.discussionUrl) errors.push(`${prefix}: Missing discussionUrl`); + if (!level.intro || level.intro.length === 0) errors.push(`${prefix}: Missing intro`); + if (!level.objective || level.objective.length === 0) errors.push(`${prefix}: Missing objective`); + if (!level.toolbox || level.toolbox.length === 0) errors.push(`${prefix}: Missing toolbox`); + if (!level.howToPlay || level.howToPlay.length === 0) errors.push(`${prefix}: Missing howToPlay`); + if (!level.verification) errors.push(`${prefix}: Missing verification`); } } return errors; diff --git a/scripts/new-adventure.mjs b/scripts/new-adventure.mjs index 628d602f..b3583770 100644 --- a/scripts/new-adventure.mjs +++ b/scripts/new-adventure.mjs @@ -99,28 +99,55 @@ const levelEntries = levels .map((level) => { const difficulty = difficultyMap[level]; return ` - id: ${level} + # required | e.g. "Stand up the Lab" / "Outcome by Cohort" / "Lights On" name: "TODO: Level name" difficulty: ${difficulty} topics: + # required | technology and tool names shown as pill tags on the level card. + # e.g. OpenFeature, flagd, Spring Boot - "TODO: Add topic tags" learnings: + # required | full sentences describing concrete skills or insights gained. + # e.g. "How an OpenFeature client and provider work together: the SDK is provider-agnostic and plugs in via dependency only" - "TODO: Add learning 1" - "TODO: Add learning 2" - devcontainerPath: ".devcontainer/TODO/devcontainer.json" - discussionUrl: "/t/TODO" + devcontainerPath: ".devcontainer/TODO/devcontainer.json" # required + discussionUrl: "/t/TODO" # required intro: + # required | one or two sentences: what the player will wire up and what they will prove works. + # e.g. "Wire the OpenFeature Java SDK into a Spring Boot service so flag evaluations are resolved by a flagd sidecar. Prove that editing flags.json flips the response without restarting the app." - "TODO: Add intro paragraph" backstory: + # optional | narrative context that sets the scene for this specific level. + # e.g. "The lab is on its first shift and it isn't reading the chart. Every subject who walks through the door gets the same hard-coded reading, no matter what the director signed off on." - "TODO: Add backstory" objective: + # required | verifiable outcomes a player can check with a command. + # e.g. "curl http://localhost:8080/ returns a vision_state resolved from flags.json (not the hard-coded fallback)" - "TODO: Add objective 1" + # optional | uncomment to describe who this level is aimed at and what prior knowledge helps. + # audience: "Best suited for: Platform engineers, SREs, and developers curious about Kubernetes security. No prior Kyverno experience needed, but familiarity with basic kubectl and YAML will help." + # optional | uncomment to describe the technical setup (services, ports, how they connect). + # architecture: + # - "TODO: e.g. Two containers run side-by-side: the app on http://localhost:8080 and a sidecar on :8013." + # - "TODO: e.g. Edit config.json through the IDE; the file watcher picks up changes within a second." toolbox: + # required | list the CLI tools and services available in the Codespace. Add a url field for external docs. + # e.g. name: "./mvnw" / description: "Maven wrapper; builds and runs the Spring Boot service" - name: "TODO" description: "TODO: Add tool description" howToPlay: + # required | walk the player from the broken state to a working solution, one titled step at a time. + # Use fenced code blocks for commands. First step: confirm the broken state. Last step: run the verifier. - title: "TODO: Step 1" body: "TODO: Add instructions" + helpfulLinks: + # optional | reference docs the player will need. label is the link text; url must be a full https:// URL. + # e.g. label: "OpenFeature Java SDK" / url: https://openfeature.dev/docs/reference/technologies/server/java/ + - label: "TODO: Doc title" + url: "https://TODO" verification: + # required | keep the defaults unless the verification script name or message differs. command: "./verify.sh" description: "Once you think you've solved the challenge, run the verification script."`; }) @@ -129,26 +156,33 @@ const levelEntries = levels const yamlContent = `id: ${id} title: "${title}" month: "${month}" +# required | 2-3 sentences covering what technology is used and what each level does. +# e.g. "Three levels of OpenFeature with flagd as the provider, in a Java + Spring Boot service. +# Wire the SDK (Beginner), add cohort targeting (Intermediate), then instrument with OpenTelemetry (Expert)." story: "TODO: Add adventure story summary" tags: + # required | technology and tool names. Mirror the topics used across all levels. + # e.g. OpenFeature, flagd, Spring Boot, Java, OpenTelemetry - "TODO: Add tags" -# Uncomment and fill in if the adventure has an external contributor: +# optional | uncomment and fill in if the adventure has an external contributor: # contributor: # name: "TODO: Contributor name" # url: "https://TODO" # about: "TODO: Short bio" backstory: + # optional | sets the scene for the whole adventure. Can be 1-3 paragraphs. + # e.g. "The Aletheia Institute is running a multi-phase vision-enhancement trial. The lab is a Spring Boot service whose one job is to record the vision_state of every subject..." - "TODO: Add adventure backstory paragraph 1" -# Uncomment and fill in for a 'What you will be using' section: +# optional | uncomment and fill in for a 'What you will be using' section: # context: # title: "What you'll be using" # body: # - "TODO: Explain the main technology" -# Uncomment and fill in for reward info: +# optional | uncomment and fill in for reward info: # rewards: # deadline: "TODO: Day, DD Month YYYY at HH:MM CET" # eligibility: "TODO: Eligibility criteria" @@ -215,6 +249,11 @@ writeFileSync(sitemapPath, sitemapContent); console.log(` Patched: public/sitemap.xml`); // Done +const levelList = + levels.length === 1 + ? levels[0] + : levels.slice(0, -1).join(", ") + ", and " + levels[levels.length - 1]; + console.log(`\n\x1b[32mDone!\x1b[0m Adventure "${title}" scaffolded.\n`); console.log("Next steps:"); console.log(` 1. Fill in the TODOs in src/data/adventures/${id}/adventure.yaml`); @@ -222,3 +261,4 @@ console.log(` 2. Update discussion URLs in the YAML and matching *-posts.json f console.log(` 3. Run: npm run generate`); console.log(` 4. Run: node scripts/refresh-discussions.mjs`); console.log(` 5. Run: npm run lint && npm test && npm run build && npm run test:e2e`); +console.log(` 6. Commit: git commit -s -m "feat(adventures): add ${title} adventure with ${levelList} levels"`); diff --git a/src/data/adventures/building-cloudhaven.generated.ts b/src/data/adventures/building-cloudhaven.generated.ts index 6bca9615..71703410 100644 --- a/src/data/adventures/building-cloudhaven.generated.ts +++ b/src/data/adventures/building-cloudhaven.generated.ts @@ -80,6 +80,10 @@ If you changed the backend configuration, run \`tofu init -migrate-state\` first { label: "OpenTofu backend configuration", url: "https://opentofu.org/docs/language/settings/backends/configuration/" }, { label: "Google Cloud provider", url: "https://registry.terraform.io/providers/hashicorp/google/latest/docs" }, ], + verification: { + command: "./smoke-test.sh", + description: "Once you think you've solved the challenge, run the smoke test to verify your solution.", + }, }, { id: "intermediate", @@ -158,6 +162,10 @@ make apply { label: "Input validation rules", url: "https://opentofu.org/docs/language/values/variables/#custom-validation-rules" }, { label: "Moved blocks", url: "https://opentofu.org/docs/language/modules/develop/refactoring/" }, ], + verification: { + command: "./smoke-test.sh", + description: "Once you think you've solved the challenge, run the smoke test to verify your solution.", + }, }, { id: "expert", @@ -219,6 +227,10 @@ cd adventures/02-building-cloudhaven/expert { label: "Trivy action", url: "https://github.com/aquasecurity/trivy-action" }, { label: "TF-via-PR action", url: "https://github.com/OP5dev/TF-via-PR" }, ], + verification: { + command: "./smoke-test.sh", + description: "Once you think you've solved the challenge, run the smoke test to verify your solution.", + }, }, ], }; diff --git a/src/data/adventures/building-cloudhaven/adventure.yaml b/src/data/adventures/building-cloudhaven/adventure.yaml index dfe50870..2c912807 100644 --- a/src/data/adventures/building-cloudhaven/adventure.yaml +++ b/src/data/adventures/building-cloudhaven/adventure.yaml @@ -104,6 +104,9 @@ levels: url: https://opentofu.org/docs/language/settings/backends/configuration/ - label: Google Cloud provider url: https://registry.terraform.io/providers/hashicorp/google/latest/docs + verification: + command: "./smoke-test.sh" + description: "Once you think you've solved the challenge, run the smoke test to verify your solution." - id: intermediate name: The Modular Metropolis difficulty: Intermediate @@ -197,6 +200,9 @@ levels: url: https://opentofu.org/docs/language/values/variables/#custom-validation-rules - label: Moved blocks url: https://opentofu.org/docs/language/modules/develop/refactoring/ + verification: + command: "./smoke-test.sh" + description: "Once you think you've solved the challenge, run the smoke test to verify your solution." - id: expert name: The Guardian Protocols difficulty: Expert @@ -283,3 +289,6 @@ levels: url: https://github.com/aquasecurity/trivy-action - label: TF-via-PR action url: https://github.com/OP5dev/TF-via-PR + verification: + command: "./smoke-test.sh" + description: "Once you think you've solved the challenge, run the smoke test to verify your solution." diff --git a/src/data/adventures/echoes-lost-in-orbit.generated.ts b/src/data/adventures/echoes-lost-in-orbit.generated.ts index 319352ac..66b164bb 100644 --- a/src/data/adventures/echoes-lost-in-orbit.generated.ts +++ b/src/data/adventures/echoes-lost-in-orbit.generated.ts @@ -79,6 +79,10 @@ kubectl apply -n argocd -f adventures/01-echoes-lost-in-orbit/beginner/manifests adventures/01-echoes-lost-in-orbit/beginner/smoke-test.sh \`\`\`` }, ], + verification: { + command: "adventures/01-echoes-lost-in-orbit/beginner/smoke-test.sh", + description: "Once you think you've solved the challenge, run the smoke test to verify your solution.", + }, }, { id: "intermediate", @@ -173,6 +177,10 @@ adventures/01-echoes-lost-in-orbit/intermediate/smoke-test.sh { label: "PromQL basics", url: "https://prometheus.io/docs/prometheus/latest/querying/basics/" }, { label: "kube-state-metrics exposed metrics", url: "https://github.com/kubernetes/kube-state-metrics/tree/main/docs#exposed-metrics" }, ], + verification: { + command: "adventures/01-echoes-lost-in-orbit/intermediate/smoke-test.sh", + description: "Once you think you've solved the challenge, run the smoke test to verify your solution.", + }, }, { id: "expert", @@ -266,6 +274,10 @@ adventures/01-echoes-lost-in-orbit/expert/smoke-test.sh { label: "Argo Rollouts analysis", url: "https://argo-rollouts.readthedocs.io/en/stable/features/analysis/" }, { label: "PromQL basics", url: "https://prometheus.io/docs/prometheus/latest/querying/basics/" }, ], + verification: { + command: "adventures/01-echoes-lost-in-orbit/expert/smoke-test.sh", + description: "Once you think you've solved the challenge, run the smoke test to verify your solution.", + }, }, ], }; diff --git a/src/data/adventures/echoes-lost-in-orbit/adventure.yaml b/src/data/adventures/echoes-lost-in-orbit/adventure.yaml index 9af617b2..0face394 100644 --- a/src/data/adventures/echoes-lost-in-orbit/adventure.yaml +++ b/src/data/adventures/echoes-lost-in-orbit/adventure.yaml @@ -105,6 +105,9 @@ levels: ```sh adventures/01-echoes-lost-in-orbit/beginner/smoke-test.sh ``` + verification: + command: "adventures/01-echoes-lost-in-orbit/beginner/smoke-test.sh" + description: "Once you think you've solved the challenge, run the smoke test to verify your solution." - id: intermediate name: The Silent Canary difficulty: Intermediate @@ -236,6 +239,9 @@ levels: url: https://prometheus.io/docs/prometheus/latest/querying/basics/ - label: kube-state-metrics exposed metrics url: https://github.com/kubernetes/kube-state-metrics/tree/main/docs#exposed-metrics + verification: + command: "adventures/01-echoes-lost-in-orbit/intermediate/smoke-test.sh" + description: "Once you think you've solved the challenge, run the smoke test to verify your solution." - id: expert name: Hyperspace Operations & Transport difficulty: Expert @@ -382,3 +388,6 @@ levels: url: https://argo-rollouts.readthedocs.io/en/stable/features/analysis/ - label: PromQL basics url: https://prometheus.io/docs/prometheus/latest/querying/basics/ + verification: + command: "adventures/01-echoes-lost-in-orbit/expert/smoke-test.sh" + description: "Once you think you've solved the challenge, run the smoke test to verify your solution." diff --git a/src/data/adventures/types.ts b/src/data/adventures/types.ts index 13857e28..0482ce0a 100644 --- a/src/data/adventures/types.ts +++ b/src/data/adventures/types.ts @@ -41,7 +41,7 @@ export type AdventureLevel = { name: string; difficulty: "Beginner" | "Intermediate" | "Expert"; // Short topic tags shown on the adventure overview card (e.g. ["Argo CD", "GitOps"]). - topics?: string[]; + topics: string[]; learnings: string[]; codespacesUrl: string; discussionUrl: string; @@ -50,11 +50,11 @@ export type AdventureLevel = { // Short narrative hook shown directly under the page title. hook?: string; // Brief intro paragraph(s) shown under the page title before the main content. - intro?: string[]; + intro: string[]; // Narrative backstory paragraphs shown as a collapsible scenario section. backstory?: string[]; // Concrete acceptance criteria shown as the "Objective" card. - objective?: string[]; + objective: string[]; // Audience line shown inside the Start CTA card (e.g. "Best for platform engineers, SREs…"). audience?: string; // Long-form narrative shown as a styled scenario block under the hero. @@ -66,13 +66,13 @@ export type AdventureLevel = { // Accessible alt text for the architecture diagram image. diagramAlt?: string; // Tools pre-installed in the Codespace, rendered as a row of cards. - toolbox?: ToolboxItem[]; + toolbox: ToolboxItem[]; // Numbered walkthrough rendered as a vertical stepper. - howToPlay?: WalkthroughStep[]; + howToPlay: WalkthroughStep[]; // Reference documentation links shown after the walkthrough. helpfulLinks?: HelpfulLink[]; // Verification card rendered as the final section. - verification?: VerificationInfo; + verification: VerificationInfo; // Mock community stats shown in the CommunitySidebar. Real data will replace // these once we aggregate certificate posts and cross-challenge contribution. solvedCount?: number; @@ -134,7 +134,7 @@ export type AdventureLevelSummary = { id: string; name: string; difficulty: "Beginner" | "Intermediate" | "Expert"; - topics?: string[]; + topics: string[]; learnings: string[]; }; diff --git a/src/test/filteredLevelCard.test.tsx b/src/test/filteredLevelCard.test.tsx index c3d5d8f4..a10bc971 100644 --- a/src/test/filteredLevelCard.test.tsx +++ b/src/test/filteredLevelCard.test.tsx @@ -12,9 +12,15 @@ const LEVEL: AdventureLevel = { id: "beginner", name: "Beginner Challenge", difficulty: "Beginner", + topics: ["Kubernetes", "GitOps"], learnings: ["Deploy a service", "Configure observability", "Write a test"], codespacesUrl: "https://codespaces.example.com/level/1", discussionUrl: "https://community.example.com/t/topic/42/1", + intro: ["Fix the broken deployment and restore service."], + objective: ["The service responds on port 8080.", "All health checks pass."], + toolbox: [{ name: "kubectl", description: "Kubernetes CLI" }], + howToPlay: [{ title: "Confirm the broken state", body: "Run kubectl get pods and observe the error." }], + verification: { command: "./verify.sh", description: "Run the verification script to confirm your solution." }, }; const LEVEL_MANY_LEARNINGS: AdventureLevel = {