| applyTo | packages/.build-*/** |
|---|---|
| name | package-starter-development |
| description | Guidelines for developing new package starters for Bolt. Use when creating or modifying package starter task collections for specific toolchains. |
This guide provides comprehensive instructions for creating new package starters for the Bolt build system.
A package starter is a pre-built collection of task scripts for a specific toolchain or workflow. Package starters:
- Provide ready-to-use task templates
- Follow consistent patterns and conventions
- Include comprehensive tests
- Support both single and multi-namespace installations
- Can be released independently of core Bolt
Create a package starter when:
- The toolchain is widely used (TypeScript, Python, Docker, etc.)
- Common workflows benefit from standardization (format → lint → test → build)
- External CLI tools provide the functionality (bicep, go, tsc, etc.)
- The tasks would be reused across multiple projects
Each package starter follows this structure:
packages/.build-[toolchain]/
├── Invoke-Format.ps1 # Format task
├── Invoke-Lint.ps1 # Validation task
├── Invoke-Test.ps1 # Testing task (if applicable)
├── Invoke-Build.ps1 # Build task (main pipeline)
├── Create-Release.ps1 # Release packaging script
├── README.md # Package-specific documentation
└── tests/
├── Tasks.Tests.ps1 # Task structure validation
├── Integration.Tests.ps1 # End-to-end integration tests
└── [example-project]/ # Sample files for testing
├── source files
└── test fixtures
Use Invoke-<TaskName>.ps1 pattern where TaskName is PascalCase:
Invoke-Format.ps1- Format source filesInvoke-Lint.ps1- Validate source filesInvoke-Test.ps1- Run testsInvoke-Build.ps1- Build artifacts
Every task must include metadata in the first 30 lines:
# TASK: taskname, alias1, alias2
# DESCRIPTION: Clear, concise description of what the task does
# DEPENDS: dependency1, dependency2Examples:
# TASK: format, fmt
# DESCRIPTION: Formats Go files using go fmt
# DEPENDS:
# TASK: build
# DESCRIPTION: Compiles Go application
# DEPENDS: format, lint, testAlways check for required external tools:
#Requires -Version 7.0
# Check for required CLI tool
$toolCmd = Get-Command [tool-name] -ErrorAction SilentlyContinue
if (-not $toolCmd) {
Write-Error "[Tool] CLI not found. Please install: [installation-url]"
exit 1
}Real-world example from Bicep starter:
$bicepCmd = Get-Command bicep -ErrorAction SilentlyContinue
if (-not $bicepCmd) {
Write-Error "Bicep CLI not found. Please install: https://aka.ms/bicep-install"
exit 1
}Follow this structure for consistency:
#Requires -Version 7.0
# TASK: taskname, alias
# DESCRIPTION: What this task does
# DEPENDS: dependency1, dependency2
# ===== External Tool Check =====
$toolCmd = Get-Command [tool] -ErrorAction SilentlyContinue
if (-not $toolCmd) {
Write-Error "[Tool] not found. Install: [url]"
exit 1
}
# ===== Task Header =====
Write-Host "[Action] files..." -ForegroundColor Cyan
# ===== Discover Files =====
$files = Get-ChildItem -Path $PSScriptRoot/.. -Recurse -Filter "*.[ext]" -File -Force
if ($files.Count -eq 0) {
Write-Host "No files found to process" -ForegroundColor Yellow
exit 0
}
Write-Host "Found $($files.Count) file(s)" -ForegroundColor Gray
Write-Host ""
# ===== Process Files =====
$success = $true
foreach ($file in $files) {
Write-Host " Processing: $($file.Name)" -ForegroundColor Gray
# Execute tool command
& [tool] [args] $file.FullName 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host " ✓ $($file.Name) processed successfully" -ForegroundColor Green
}
else {
Write-Host " ✗ $($file.Name) failed" -ForegroundColor Red
$success = $false
}
}
# ===== Report Results =====
Write-Host ""
if (-not $success) {
Write-Host "✗ Task completed with errors" -ForegroundColor Red
exit 1
}
Write-Host "✓ All files processed successfully!" -ForegroundColor Green
exit 0Always use explicit exit codes:
exit 0- Successexit 1- Failure
Track overall success and fail fast when appropriate:
$success = $true
foreach ($file in $files) {
# Process file
if ($LASTEXITCODE -ne 0) {
$success = $false
}
}
if (-not $success) {
Write-Host "✗ Task failed" -ForegroundColor Red
exit 1
}
Write-Host "✓ Task succeeded" -ForegroundColor Green
exit 0Use consistent color coding:
- Cyan - Task headers
- Gray - Progress/details
- Green - Success (with ✓)
- Yellow - Warnings (with ⚠)
- Red - Errors (with ✗)
Example:
Write-Host "Building application..." -ForegroundColor Cyan
Write-Host " Compiling: main.go" -ForegroundColor Gray
Write-Host " ✓ Compilation successful" -ForegroundColor Green
Write-Host " ⚠ Warning: unused variable" -ForegroundColor Yellow
Write-Host " ✗ Error: syntax error" -ForegroundColor RedCRITICAL: Package starters must work on Windows, Linux, and macOS.
Use PowerShell cmdlets, not Unix commands:
# ✅ CORRECT - Cross-platform
$files = Get-ChildItem -Path $path -Filter "*.go" -Recurse -File -Force
$content = Get-Content -Path $file -First 10
# ❌ WRONG - Unix-only
$files = Get-ChildItem $path | grep "*.go"
$content = cat $file | head -10Use Join-Path for all path construction:
# ✅ CORRECT - Cross-platform
$sourcePath = Join-Path $PSScriptRoot ".." "src"
$testPath = Join-Path $sourcePath "tests"
# ❌ WRONG - Windows-only
$sourcePath = "$PSScriptRoot\..\src"
$testPath = "$sourcePath\tests"Create tests/Tasks.Tests.ps1 to validate task structure:
#Requires -Version 7.0
Describe "[Toolchain] Package Starter - Task Validation" -Tag "[Toolchain]-Tasks" {
BeforeAll {
$packagePath = Join-Path $PSScriptRoot ".."
$taskFiles = Get-ChildItem -Path $packagePath -Filter "Invoke-*.ps1" -File -Force
}
Context "Task Files Exist" {
It "format task should exist" {
$formatTask = $taskFiles | Where-Object { $_.Name -eq "Invoke-Format.ps1" }
$formatTask | Should -Not -BeNullOrEmpty
}
It "lint task should exist" {
$lintTask = $taskFiles | Where-Object { $_.Name -eq "Invoke-Lint.ps1" }
$lintTask | Should -Not -BeNullOrEmpty
}
It "build task should exist" {
$buildTask = $taskFiles | Where-Object { $_.Name -eq "Invoke-Build.ps1" }
$buildTask | Should -Not -BeNullOrEmpty
}
}
Context "Task Metadata" {
It "format task should have TASK metadata" {
$content = Get-Content -Path "$packagePath/Invoke-Format.ps1" -First 30 -Raw
$content | Should -Match "# TASK:"
}
It "build task should declare dependencies" {
$content = Get-Content -Path "$packagePath/Invoke-Build.ps1" -First 30 -Raw
$content | Should -Match "# DEPENDS:"
}
}
}Create tests/Integration.Tests.ps1 for end-to-end testing:
#Requires -Version 7.0
Describe "[Toolchain] Package Starter - Integration Tests" -Tag "[Toolchain]-Tasks" {
BeforeAll {
# Check for tool availability
$toolCmd = Get-Command [tool-name] -ErrorAction SilentlyContinue
if (-not $toolCmd) {
Set-ItResult -Skipped -Because "[Tool] CLI not installed"
}
$packagePath = Join-Path $PSScriptRoot ".."
$testProjectPath = Join-Path $PSScriptRoot "[example-project]"
}
Context "Format Task" {
It "should format files successfully" {
$formatScript = Join-Path $packagePath "Invoke-Format.ps1"
& $formatScript
$LASTEXITCODE | Should -Be 0
}
}
Context "Lint Task" {
It "should validate files successfully" {
$lintScript = Join-Path $packagePath "Invoke-Lint.ps1"
& $lintScript
$LASTEXITCODE | Should -Be 0
}
}
Context "Build Task" {
It "should complete build pipeline" {
$buildScript = Join-Path $packagePath "Invoke-Build.ps1"
& $buildScript
$LASTEXITCODE | Should -Be 0
}
}
}Use consistent tags for test filtering:
[Toolchain]-Tasks- For package-specific tests- Examples:
Bicep-Tasks,Golang-Tasks,TypeScript-Tasks
This allows:
Invoke-Pester -Tag Bicep-Tasks # Only Bicep tests
Invoke-Pester -Tag Golang-Tasks # Only Golang testsCreate Create-Release.ps1 for automated release packaging:
#Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Version,
[Parameter(Mandatory = $false)]
[string]$OutputDirectory = "release"
)
# Validate version format (SemVer)
if ($Version -notmatch '^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$') {
Write-Error "Invalid version format. Use SemVer (e.g., 1.0.0 or 1.0.0-beta)"
exit 1
}
# Define package name
$toolchain = "[toolchain]" # e.g., "bicep", "golang"
$packageName = "bolt-starter-$toolchain-$Version"
$zipFile = "$packageName.zip"
$checksumFile = "$zipFile.sha256"
# Create release directory
$releaseDir = New-Item -Path $OutputDirectory -ItemType Directory -Force
$tempDir = Join-Path $releaseDir "temp-$packageName"
New-Item -Path $tempDir -ItemType Directory -Force
try {
# Copy task files
$taskFiles = Get-ChildItem -Path $PSScriptRoot -Filter "Invoke-*.ps1" -File
foreach ($file in $taskFiles) {
Copy-Item -Path $file.FullName -Destination $tempDir -Force
}
# Create zip archive
$zipPath = Join-Path $releaseDir $zipFile
Compress-Archive -Path "$tempDir/*" -DestinationPath $zipPath -Force
# Generate SHA256 checksum
$hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash
$checksumPath = Join-Path $releaseDir $checksumFile
"$hash $zipFile" | Out-File -FilePath $checksumPath -Encoding ASCII -NoNewline
Write-Host "✓ Created: $zipFile" -ForegroundColor Green
Write-Host "✓ Created: $checksumFile" -ForegroundColor Green
Write-Host " SHA256: $hash" -ForegroundColor Gray
exit 0
}
catch {
Write-Error "Release creation failed: $_"
exit 1
}
finally {
# Clean up temp directory
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force
}
}Create README.md in package directory:
# [Toolchain] Starter Package for Bolt
[Brief description of what this package provides]
## Requirements
- [Tool] [version]+
- **Windows**: [installation command]
- **Linux**: [installation command]
- **macOS**: [installation command]
## Installation
See main [packages/README.md](../README.md) for installation options.
### Quick Install
```powershell
# Interactive download and install
irm https://raw.githubusercontent.com/motowilliams/bolt/main/Download-Starter.ps1 | iex
# Or manual copy from source
Copy-Item -Path "packages/.build-[toolchain]/Invoke-*.ps1" -Destination ".build/" -Force- format (alias: fmt) - [description]
- lint - [description]
- test - [description]
- build - [description]
# Format files
.\bolt.ps1 format
# Validate syntax
.\bolt.ps1 lint
# Run tests
.\bolt.ps1 test
# Full build pipeline (format → lint → test → build)
.\bolt.ps1 buildbuilddepends on:format,lint,testtestdepends on:format,lintlintdepends on:format
Run the test suite:
# Install Pester if needed
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser
# Run package tests
Invoke-Pester -Tag [Toolchain]-Tasks[Provide real-world examples of common workflows]
If you get "[Tool] CLI not found" error:
- Install [Tool]: [installation instructions]
- Verify installation:
[tool] --version - Restart PowerShell session
Tasks search from parent directory of script location. Ensure your files are in the expected structure.
## Multi-Namespace Support
Package starters can be used in multi-namespace mode:
### Installation Pattern
```powershell
# Create namespace subdirectory
New-Item -ItemType Directory -Path ".build/[toolchain]" -Force
# Install tasks
Copy-Item -Path "packages/.build-[toolchain]/Invoke-*.ps1" -Destination ".build/[toolchain]/" -Force
Tasks in namespace subdirectories are automatically prefixed:
.build/bicep/Invoke-Lint.ps1→bicep-lint.build/golang/Invoke-Build.ps1→golang-build
When declaring dependencies in namespace subdirectories:
# .build/bicep/Invoke-Build.ps1
# TASK: build
# DEPENDS: format, lint
# Dependencies resolve as:
# - bicep-format (same namespace, priority)
# - bicep-lint (same namespace, priority)
# - format (fallback if namespace version doesn't exist)
# - lint (fallback if namespace version doesn't exist)Key Rule: Dependencies within the same namespace are resolved first, providing proper isolation.
- Bolt scans
.build/directory (or custom via-TaskDirectory) - Finds all
Invoke-*.ps1files - Reads first 30 lines for metadata
- Registers tasks with names from
# TASK:header - Builds dependency graph from
# DEPENDS:declarations
- User runs:
.\bolt.ps1 build - Bolt resolves dependencies:
format→lint→build - Executes each task in order
- Tracks executed tasks to prevent duplicate execution
- Checks
$LASTEXITCODEafter each task - Stops on first failure (exit code 1)
Tasks auto-complete in PowerShell:
- Restart shell after adding new tasks
- Works in both script mode and module mode
- Namespace-aware (shows
bicep-format,golang-lint, etc.)
# Find all source files recursively
$files = Get-ChildItem -Path $rootPath -Recurse -Filter "*.[ext]" -File -Force
# Exclude certain directories
$files = Get-ChildItem -Path $rootPath -Recurse -Filter "*.[ext]" -File -Force |
Where-Object { $_.FullName -notmatch 'node_modules|vendor|bin|obj' }# Only compile main files, not modules
$files = Get-ChildItem -Path $rootPath -Recurse -Filter "*.bicep" -File -Force |
Where-Object { $_.Name -match '^main.*\.bicep$' }$i = 0
$total = $files.Count
foreach ($file in $files) {
$i++
Write-Host " [$i/$total] Processing: $($file.Name)" -ForegroundColor Gray
# Process file
}# Capture both stdout and stderr
$output = & [tool] [args] $file.FullName 2>&1
# Parse output for diagnostics
$errors = $output | Where-Object { $_ -match 'error|failed' }
if ($errors) {
foreach ($error in $errors) {
Write-Host " ✗ $error" -ForegroundColor Red
}
}CRITICAL: When creating a new package starter, you must update the test wrapper scripts to include your new tests.
The Invoke-Tests.ps1 script at the repository root is the primary test runner. It must be updated to:
-
Add the test path to the discovery paths array:
$config.Run.Path = @( 'tests' # Core Bolt tests 'packages/.build-bicep/tests' # Bicep starter package tests 'packages/.build-golang/tests' # Golang starter package tests 'packages/.build-terraform/tests' # Terraform starter package tests 'packages/.build-[toolchain]/tests' # YOUR NEW PACKAGE )
-
Add the tag to both
ValidateSetattributes:[ValidateSet('Core', 'Security', 'Bicep-Tasks', 'Golang-Tasks', 'Terraform-Tasks', '[Toolchain]-Tasks', ...)]
-
Update the parameter documentation in
.PARAMETER Tagsection:- [Toolchain]-Tasks: [Toolchain] starter package tests (requires [Tool] CLI or Docker)
-
Add an example in the
.EXAMPLEsection:.EXAMPLE .\Invoke-Tests.ps1 -Tag [Toolchain]-Tasks Runs only [Toolchain] starter package tests (requires [Tool] CLI or Docker). -
Update the discovery output in the
Write-Hostsection:Write-Host " - packages/.build-[toolchain]/tests/" -ForegroundColor Gray
-
Update the description in
.DESCRIPTIONsection to list your package.
Example commit message: Update Invoke-Tests.ps1 to include [Toolchain]-Tasks tag and test path
- Test Discovery: Without updating
Invoke-Tests.ps1, your tests won't be discovered by the main test runner - CI Integration: The CI pipeline uses
Invoke-Tests.ps1to run all tests - Developer Experience: Developers expect
.\Invoke-Tests.ps1to run all tests, including new packages - Tag Filtering: Without the tag in
ValidateSet, developers can't filter to just your package's tests
When creating a new package starter, you should prepare for a release to make it available to users.
New package starters typically warrant a minor version bump according to Semantic Versioning:
- Major (X.0.0): Breaking changes to core Bolt functionality
- Minor (0.X.0): New features, new package starters, backward-compatible enhancements ← Use this for new packages
- Patch (0.0.X): Bug fixes, documentation updates, minor improvements
Example: If current version is 0.9.0, the new package starter should bump to 0.10.0
Add your package to the [Unreleased] section in CHANGELOG.md:
## [Unreleased]
### Added
- **[Toolchain] Starter Package**: [Brief description]
- **`format` task** (alias: `fmt`) - Description
- **`lint` task** - Description
- **`test` task** - Description
- **`build` task** - Description
- Dependencies: build → format, lint, test
- Docker Fallback Support: Automatically uses [docker-image] when [Tool] CLI not installed
- Cross-platform compatibility (Windows, Linux, macOS)
- Comprehensive test suite ([N] task validation + [N] integration tests)
- Example [toolchain] project for testing
- Complete documentation in `packages/.build-[toolchain]/README.md`See CHANGELOG.md for examples of how other package starters (Bicep, Golang, Terraform) are documented.
After the PR is merged, a maintainer will:
- Move the
[Unreleased]content to a new version section - Create a git tag (e.g.,
v0.10.0) - Push the tag to trigger the automated release workflow
You don't need to create the tag yourself - this is typically done by maintainers after PR review.
- User Availability: Releases make the package starter downloadable via GitHub releases
- Versioning: Clear version numbers help users track features and changes
- Documentation: CHANGELOG entries provide context for what's included in each release
- Automation: The release workflow automatically packages and publishes when a tag is pushed
Before submitting a pull request:
- Directory structure follows convention
- All task files use
Invoke-*.ps1naming - Comment-based metadata included in all tasks
- External tool checks are in place
- Error handling uses explicit exit codes
- Output formatting uses Bolt color standards
- Cross-platform compatibility (no Unix commands)
- Path construction uses
Join-Path - Tests include both structure and integration tests
- Test tags use
[Toolchain]-Taskspattern -
Invoke-Tests.ps1updated with new tag and test path -
CHANGELOG.mdupdated under[Unreleased]section - Main
README.mdupdated with new package starter section - Main
.gitignoreupdated with toolchain-specific patterns (if needed) - Release script creates valid archives
- Package-specific README.md is complete
- Main packages/README.md updated with new entry
- Example project files included for testing
- Dependencies declared correctly
- Namespace-aware if in subdirectory
- All tests pass:
Invoke-Pester -Tag [Toolchain]-Tasks
- Reference Implementations: Review existing package starters in
packages/ - Bolt Core: See
bolt.ps1for task discovery and execution logic - Main Documentation: packages/README.md, README.md, IMPLEMENTATION.md
- Testing Guide: CONTRIBUTING.md
- Copilot Instructions: .github/copilot-instructions.md
- Review existing package starters as examples
- Read Bolt core documentation for task patterns
- Test with real-world project examples
- Ask questions in GitHub issues or discussions
We welcome new package starters! Follow this guide and submit a pull request. Ensure:
- Code is tested and working
- Documentation is complete
- Cross-platform compatibility verified
- Existing patterns are followed