Skip to content
Merged
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
2 changes: 1 addition & 1 deletion schemas/adventure.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
6 changes: 6 additions & 0 deletions scripts/generate-adventures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 45 additions & 5 deletions scripts/new-adventure.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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."`;
})
Expand All @@ -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"
Expand Down Expand Up @@ -215,10 +249,16 @@ 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`);
console.log(` 2. Update discussion URLs in the YAML and matching *-posts.json files`);
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"`);
12 changes: 12 additions & 0 deletions src/data/adventures/building-cloudhaven.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
},
},
],
};
9 changes: 9 additions & 0 deletions src/data/adventures/building-cloudhaven/adventure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."
12 changes: 12 additions & 0 deletions src/data/adventures/echoes-lost-in-orbit.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
},
},
],
};
9 changes: 9 additions & 0 deletions src/data/adventures/echoes-lost-in-orbit/adventure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."
14 changes: 7 additions & 7 deletions src/data/adventures/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -134,7 +134,7 @@ export type AdventureLevelSummary = {
id: string;
name: string;
difficulty: "Beginner" | "Intermediate" | "Expert";
topics?: string[];
topics: string[];
learnings: string[];
};

Expand Down
6 changes: 6 additions & 0 deletions src/test/filteredLevelCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading