Spacelift User Guides Library
A library of user guides for Spacelift, stored as code and embedded into the Spacelift backend.
Purpose
This repository serves as the source of truth for Spacelift user guides content. By storing guides as structured YAML files in version control, content can be managed independently from the backend code. The library embeds this content at compile time using Go's embed.FS, providing type safety and compile-time validation that ensures invalid guides are caught during build rather than at runtime. This approach enables content contributors to add or modify guides through standard pull request workflows while maintaining consistency and reliability.
Directory Structure
The repository uses a hierarchical directory structure that mirrors the GraphQL schema (Group → Chapter → Guide):
guides/
├── {group-slug}/
│ ├── group.yaml # Group metadata
│ ├── {chapter-slug}/
│ │ ├── chapter.yaml # Chapter metadata
│ │ ├── {guide-slug}.yaml # Individual guide
│ │ └── ...
│ └── ...
└── ...
Example Structure
guides/
├── getting-started/
│ ├── group.yaml
│ ├── first-stack/
│ │ ├── chapter.yaml
│ │ ├── create-stack.yaml
│ │ ├── configure-vcs.yaml
│ │ └── add-policy.yaml
│ └── collaboration/
│ ├── chapter.yaml
│ ├── invite-users.yaml
│ └── manage-teams.yaml
└── advanced-workflows/
├── group.yaml
├── drift-detection/
│ ├── chapter.yaml
│ └── enable-drift.yaml
└── custom-inputs/
├── chapter.yaml
└── terraform-variables.yaml
Slug Generation
Slugs are automatically derived from the directory structure:
- Group Slug: Directory name (e.g.,
getting-started)
- Chapter Slug: Directory name (e.g.,
first-stack)
- Guide Slug: Filename without extension (e.g.,
create-stack)
Slugs provide human-readable identifiers at each level of the hierarchy. The hierarchical relationships (Group → Chapter → Guide) are maintained through the nested data structure. This approach eliminates manual ID management and ensures uniqueness within each level.
group.yaml
Defines metadata for a group of guides.
name: "Getting Started"
description: "Learn the basics of Spacelift"
skillLevel: BEGINNER # BEGINNER, ENABLER, COMMANDER, or GUARDIAN
ordering: 1
requiredEntitlements: # optional - Spacelift entitlements the account must have for this group
- NOTIFICATION_POLICIES
Required Fields:
name (string): Display name of the group
description (string): Brief description of the group
skillLevel (string): One of BEGINNER, ENABLER, COMMANDER, or GUARDIAN
ordering (int): Display order (lower numbers appear first)
Optional Fields:
requiredEntitlements ([]string): Spacelift product entitlements the user's account must have to complete the guides in this group. Each entry must be one of NOTIFICATION_POLICIES, RUN_STATE_CHANGE_WEBHOOKS. Values mirror the backend Entitlement GraphQL enum (1:1 string match) — the backend maps each entitlement to its own billing / feature-flag model. Omit (or leave empty) when the group is available on every plan (including Free). The recognised set is intentionally small — when adding a group that depends on a new gated feature (e.g. private workers, blueprints, drift detection, push policies), extend the Entitlement constants in library.go, the validEntitlements map, and the enum in schema/group_schema.json, and make sure the new value exists verbatim in the backend Entitlement enum.
chapter.yaml
Defines metadata for a chapter within a group.
name: "Your First Stack"
description: "Create and manage your first stack"
ordering: 1
Required Fields:
name (string): Display name of the chapter
description (string): Brief description of the chapter
ordering (int): Display order within the group (lower numbers appear first)
{guide-slug}.yaml
Defines an individual guide with metadata, steps, and completion information.
ordering: 1
metadata:
title: "Create Your First Stack"
description: "Learn how to create a stack in Spacelift"
labels: ["terraform", "basics"]
difficulty: "easy"
minutesToComplete: 10
steps:
- order: 1
title: "Navigate to Stacks"
instruction: "Click on **Stacks** in the left sidebar to open the stacks page."
hint: "If you don't see the sidebar, click the menu icon in the top-left corner."
docs:
- title: "What is a Stack?"
url: "https://docs.spacelift.io/concepts/stack"
- order: 2
title: "Create New Stack"
instruction: "Click the **Add Stack** button in the top-right corner."
docs:
- title: "Stack Creation Guide"
url: "https://docs.spacelift.io/concepts/stack/creating-a-stack"
completion:
successMessage: "Congratulations! You've created your first stack. Next, learn how to connect it to your VCS."
recommendedGuideIds:
- "getting-started/first-stack/configure-vcs"
- "getting-started/first-stack/add-policy"
Required Fields:
ordering (int): Display order within the chapter (lower numbers appear first)
Optional Top-Level Fields:
prerequisiteGuideSlugs ([]string): Slugs of guides that must be completed first
metadata:
title (string): Display title of the guide
description (string): Brief description
labels ([]string): Tags for categorization
difficulty (string): Difficulty level (e.g., "easy", "medium", "hard")
minutesToComplete (int): Estimated time to complete (must be >= 0)
steps:
order (int): Step number (must be > 0, unique within guide)
title (string): Step title
instruction (string): What the user should do (supports Markdown)
hint (string, optional): Additional help text
docs ([]object, optional): Related documentation links
title (string): Link text
url (string): Documentation URL
completion:
successMessage (string): Message shown when guide is completed
recommendedGuideIds ([]string): IDs of guides to suggest next
Content Style Conventions
Guide instructions, hints, and other user-facing strings support Markdown. To keep the experience consistent across the product, follow these formatting rules.
Actions — bold
Use bold for any UI element the user is acting on — clicking, toggling, enabling, disabling, checking, typing into, or selecting from. That includes buttons, links, toggles, checkboxes, and values being entered into a field (including code identifiers like TF_VAR_env or subnet_id — when the user is typing them in, the action wins over the code style).
instruction: "Click **Create stack**, set Workflow tool to **OpenTofu**, then click **Continue**."
instruction: "Enable **Autodeploy** and click **Save**."
instruction: "Enable both **Read** and **Write** permissions."
instruction: "In the `Output name` field, enter **subnet_id**."
The rule is acting vs. referring. If the user is doing something to a toggle, its label is bold. If the user is checking that a toggle is in a certain state, or the toggle is just being referenced in prose, its label is backticked (see the next section).
User-named entities — references to a stack, policy, context, or space the user created earlier — are also bold, because they behave like proper nouns of UI entities:
instruction: "Open your stack **${main_stack_name}** and click **Trigger**."
Titles — code style (Inconsolata)
Use backticks for anything that names a UI location, label, or literal code identifier — the "where you are" rather than "what you click":
- Page/screen names, tabs, menu paths and breadcrumbs, sections within a page
- Field labels (when used as locators, e.g. "the
Role ARN field")
- Setting/toggle names when referenced or verified, not acted on (e.g. "make sure
Autodeploy is disabled" or "the Autodeploy setting controls…")
- File names (
main.tf)
- Code identifiers when referenced in descriptive prose — variable names, resource types, output names, function names, etc.
- Label literals (
env:production, autoattach:*) when referenced in descriptive prose
instruction: "Navigate to `Ship Infra > Stacks` and open the `Stacks` page."
instruction: "Go to `Settings > Behavior` and verify `Autodeploy` is enabled."
instruction: "Edit `main.tf` to add the `aws_s3_bucket` resource."
hint: "The `data_store_bucket` output should show the bucket name."
Note: when a UI element is being acted on, or a code identifier/label literal is the value being entered into a field, it's bold, not code — see the actions rule above.
Reserve bold for the actual action within that location (e.g. "In the Policies tab, click Attach policy.").
Menu paths and breadcrumbs
When the user navigates down a UI hierarchy (menus, sub-menus, settings paths), write the path as a single breadcrumb inside one backtick span using > as the separator. Do not break it up with words like "then" or with multiple backtick spans.
# Good
instruction: "Open the three-dot menu and go to `Settings > Behavior`."
instruction: "Navigate to `Ship Infra > Stacks` and click **Create stack**."
instruction: "Go to `Settings > Integrations > Cloud integrations`."
# Bad
instruction: "Go to the `Settings` tab, then `Integrations`."
instruction: "Go to `Settings`, then `Behavior`."
If the entry point isn't obvious (e.g. behind a kebab menu or context menu), state it before the breadcrumb: "Open the three-dot menu and go to Settings > Behavior."
For acting on a section that's already visible within the current page (not a deeper menu hop), use "In the X section/tab" instead of a breadcrumb:
instruction: "In the `Depends on` section, click **Add dependencies**."
instruction: "In the `Policies` tab, click **Attach policy**."
For top-level Spacelift pages, the breadcrumb format is also acceptable inside a Markdown link: [Ship Infra > Stacks](/stacks).
Run states, phases, and similar status values are written in ALL CAPS without bold or backticks. They're already visually distinct and match how Spacelift renders them in the UI.
instruction: "Watch the run progress through INITIALIZING → PLANNING → UNCONFIRMED → APPLYING → FINISHED."
hint: "If the run enters the FAILED state, check the logs for details."
System-returned text — italic in double quotes, after a colon
When you quote a string the system produces — a policy denial message, an error notification, a log line the user should look for — wrap it in double quotes and italicize the whole quote. Introduce it with a colon, not a dash, so the quote reads as the thing the system says.
Code identifiers that appear inside the quoted system message (tag names, resource names, etc.) stay as plain text inside the italics — the italic styling already marks the whole span as system output, so don't re-format pieces of it with backticks or bold.
# Good
instruction: "The run should FAIL with a policy denial: *\"your S3 bucket is missing the cost-center tag.\"*"
instruction: "You should see a deny message: *\"S3 bucket ... is missing required 'cost-center' tag.\"*"
# Bad
instruction: "The run should FAIL with a policy denial - your S3 bucket is missing the `cost-center` tag."
instruction: "You should see a deny message: `S3 bucket ... is missing required 'cost-center' tag`."
This is for quoted system text. When you're describing the message in your own words ("a policy denial about the missing cost-center tag"), keep the existing rules — backticks for the code identifier, no italics.
Quick reference
| Element |
Style |
Example |
| Buttons, links, toggles, checkboxes, values being entered (including code identifiers entered as field values) |
Bold |
Click Trigger; Enable Autodeploy; enter subnet_id |
User-named entities (${stack_name} etc.) |
Bold |
Open your stack ${main_stack_name} |
| Page names, tabs, menu paths, sections, field labels |
Code |
Open Settings > Behavior; in the Policies tab |
Menu paths / breadcrumbs (full hierarchy in one span, > as separator — never split with "then") |
Code |
Settings > Integrations > Cloud integrations |
| Setting/toggle names when referenced, not acted on |
Code |
Make sure Autodeploy is enabled |
| File names |
Code |
Edit main.tf |
| Code identifiers referenced in prose |
Code |
The aws_s3_bucket resource; the TF_VAR_ prefix |
| Label literals referenced in prose |
Code |
Stacks with the env:production label |
| Run states, phases, statuses |
ALL CAPS, no formatting |
Wait for FINISHED |
| System-returned text (policy messages, errors, log lines) |
Italic in double quotes, after a colon |
… policy denial: "your S3 bucket is missing the cost-center tag." |
How to Add New Guides
1. Create or Navigate to a Group
If the group doesn't exist, create a new directory under guides/:
mkdir -p guides/my-group
Create group.yaml with the group metadata:
name: "My Group"
description: "Description of this group"
skillLevel: BEGINNER
2. Create a Chapter
Create a chapter directory within the group:
mkdir -p guides/my-group/my-chapter
Create chapter.yaml with the chapter metadata:
name: "My Chapter"
description: "Description of this chapter"
3. Create a Guide
Create a YAML file for your guide in the chapter directory:
touch guides/my-group/my-chapter/my-guide.yaml
Fill in the guide content following the format described above. The filename (without extension) will become the guide's slug.
4. Test Your Changes
Run the tests to validate your guide structure:
go test -v
The library will validate:
- YAML syntax: All YAML files must be valid
- Required fields: All mandatory fields must be present
- Unique slugs: No duplicate slugs within groups, chapters, or guides
- Skill level enum: Must be BEGINNER, ENABLER, COMMANDER, or GUARDIAN
- Difficulty enum: Must be easy, medium, or hard (if specified)
- Step ordering: Steps must be sequentially ordered (1, 2, 3...) with no gaps
- URL validation: Documentation URLs must use http or https schemes
- Label validation: Labels must be non-empty strings
- Referential integrity: RecommendedGuideIds must reference existing guides
- Non-negative values: MinutesToComplete must be >= 0
5. Submit a Pull Request
Once tests pass, commit your changes and create a pull request. The CI pipeline will run validation automatically.
Validation and Testing
Compile-Time Validation
The library validates all content at build time by calling Guides(). Invalid content causes a panic, ensuring problems are caught before deployment:
func Guides() (*Library, error) {
lib, err := parse(guidesFS)
if err != nil {
panic("userguides: " + err.Error())
}
return lib, nil
}
Validation Rules
Structural Validation:
- Required YAML files must exist (group.yaml, chapter.yaml)
- All required fields must be present
- No duplicate slugs within groups, chapters, or guides
- Steps must be sequentially ordered (1, 2, 3...) with no gaps or duplicates
Type Validation:
- SkillLevel: Must be one of
BEGINNER, ENABLER, COMMANDER, GUARDIAN
- Difficulty: Must be one of
easy, medium, hard (if specified)
- RequiredEntitlements (group-level): If specified, each entry must be a valid entitlement (
NOTIFICATION_POLICIES, RUN_STATE_CHANGE_WEBHOOKS); duplicates and empty values are rejected
- MinutesToComplete: Must be >= 0
- Labels: Must be non-empty strings (no whitespace-only labels)
- URLs: Must use
http or https scheme and be well-formed
Referential Integrity:
- RecommendedGuideIds must reference existing guides
- Full guide paths are validated (group/chapter/guide)
Error Handling:
- Validation failures cause a panic at build time (not runtime)
- Error messages include file paths and specific validation failures
- This ensures invalid content breaks the build, not production
Running Tests
# Run all tests
go test -v
# Run specific test
go test -v -run TestGuidesLoad
# Check test coverage
go test -v -cover
CI/CD Pipeline
Automated Validation
The repository uses GitHub Actions to automatically validate all content changes. The CI pipeline runs on:
- Every push to the
main branch
- Every pull request targeting
main
Workflow Steps
The validation workflow (.github/workflows/validate.yml) performs the following checks:
- Checkout Code: Retrieves the repository code
- Setup Go: Installs Go using the version specified in
go.mod
- Download Dependencies: Fetches all required Go modules
- Run Tests: Executes all tests with
go test -v ./...
- Triggers
Guides() which validates all content
- Runs comprehensive validation tests
- Panics on any validation errors
- Verify go.mod: Ensures dependencies are correctly tracked
- Build Library: Compiles the library to catch any build errors
What Gets Validated
Every commit automatically checks:
- ✅ YAML syntax correctness
- ✅ Required fields presence
- ✅ Unique slugs (no duplicates)
- ✅ Valid enums (skill levels, difficulty)
- ✅ Sequential step ordering
- ✅ URL format validation
- ✅ Referential integrity (recommendedGuideIds)
- ✅ Label and field constraints
Pull Request Requirements
- All CI checks must pass before merging
- Any validation failure will block the PR
- Review the CI output for specific error messages
- Fix validation errors and push again to re-trigger CI
Integration with Backend
The Spacelift backend imports this library as a Go module:
import userguidelib "github.com/spacelift-io/spacelift-user-guides-library"
// Load guides
lib, err := userguidelib.Guides()
Content is synced to the database during migrations, similar to policy templates. See the design document for full integration details.
Development Workflow
- Make changes to guides in this repository
- Test locally with
go test -v
- Submit PR for review
- Merge to main branch
- Backend update: Update
go.mod in backend to new library version
- Deploy: Backend deployment syncs new content to database
Schema Versioning
Guide content is stored as JSONB in the database. If the schema needs to evolve, use the schema_version approach:
switch schema_version {
case 1:
json.Decode(content, &v1Guide)
case 2:
json.Decode(content, &v2Guide)
}
Design Document
For architectural details and implementation phases, see the full design document:
User Guides Library - Design Document
License
Copyright © Spacelift, Inc.