A Go-based reimplementation of the Puppet Development Kit (PDK), built to be fast, self-contained, and free of Ruby runtime dependencies.
PDK has been an essential tool for Puppet module authors for years. When Perforce moved PDK to a closed-source model, it created a real problem for teams and individuals who depend on open tooling for their workflows. On top of that, PDK carries a heavy Ruby runtime footprint, which adds friction to CI environments and developer machines alike.
jig aims to replace the parts of PDK that matter most: scaffolding new modules, building module packages, and cutting releases. It ships as a single static binary with no external runtime required.
jig is under active development. The table below reflects the current state of planned functionality.
| Command | Subcommand | Status |
|---|---|---|
new |
module |
✅ Working |
new |
class |
✅ Working |
new |
defined_type |
✅ Working |
new |
fact |
✅ Working |
new |
function |
✅ Working |
new |
provider |
✅ Working |
new |
task |
✅ Working |
new |
test |
✅ Working |
new |
transport |
✅ Working |
--skip-interview |
✅ Working | |
| Template override | ✅ Working | |
templates |
dump |
✅ Working |
templates |
resolve |
🔲 Planned |
build |
✅ Working | |
release |
✅ Working | |
validate |
✅ Working | |
test |
unit |
✅ Working |
update |
✅ Working |
Requires Go 1.21 or later.
git clone https://github.com/avitacco/jig.git
cd jig
go build -o jig .Move the resulting binary somewhere in your $PATH:
mv jig /usr/local/bin/No other dependencies or runtimes needed.
Scaffolds a new Puppet module with the standard directory structure and metadata.
jig new module <n> [flags]
jig will walk you through an interactive interview to collect module metadata. Values from your config file are used as defaults. If no config is present, jig falls back to your system username and full name.
Flags:
| Flag | Description |
|---|---|
-u, --forge-user |
Your Puppet Forge username |
-a, --author |
Your full name |
-l, --license |
License type (default: from config, then Apache-2.0) |
-s, --summary |
One-line module summary |
-S, --source |
Source URL for the module |
-f, --force |
Overwrite an existing module directory. The existing directory is backed up with a timestamp before any files are written. |
-i, --skip-interview |
Skip the interactive interview and use flag values or defaults. |
Generates a new Puppet class manifest and its rspec-puppet spec file inside the current module directory.
jig new class <n>
The class name follows standard Puppet naming conventions. Namespaced names
like foo::bar are supported and will generate the correct directory structure
under manifests/. The module name prefix must not be included in the name.
Generates a new Puppet defined type manifest and its rspec-puppet spec file inside the current module directory.
jig new defined_type <n>
Defined type names follow the same conventions as class names. Namespaced
names like foo::bar are supported and will generate the correct directory
structure under manifests/. The module name prefix must not be included in
the name.
Generates a new custom Facter fact and its spec file inside the current module directory.
jig new fact <n>
Fact names may not contain ::. The generated fact is placed in
lib/facter/<name>.rb and its spec in spec/unit/facter/<name>_spec.rb.
Generates a new Puppet language function and its spec file inside the current module directory.
jig new function <n>
Function names follow standard Puppet naming conventions. The module name is
automatically prepended to form the fully qualified function name
(<module>::<name>). The generated function is placed in
functions/<name>.pp and its spec in spec/functions/<name>_spec.rb.
Generates a new Puppet resource type and provider using the Resource API, along with spec files for both, inside the current module directory.
jig new provider <n>
Provider names must start with a lowercase letter and contain only lowercase
letters, numbers, and underscores ([a-z][a-z0-9_]*). Four files are
generated:
lib/puppet/type/<name>.rb— the Resource API type definitionlib/puppet/provider/<name>/<name>.rb— the Resource API simple providerspec/unit/puppet/type/<name>_spec.rb— spec file for the typespec/unit/puppet/provider/<name>/<name>_spec.rb— spec file for the provider
Generates a new Puppet task and its metadata file inside the current module directory.
jig new task <n>
Task names must start with a lowercase letter and contain only lowercase
letters, numbers, and underscores ([a-z][a-z0-9_]*). The special name
init is valid and maps the task to the module itself. Namespaced names
using :: are not valid for tasks.
Generates a unit test for an existing class or defined type inside the current module directory.
jig new test <n>
jig looks up the named resource by finding its manifest under manifests/ and
inspects the file to determine whether it contains a class or a defined type.
The spec file is written to spec/classes/ for classes or spec/defines/ for
defined types. The name follows the same conventions as jig new class and
jig new defined_type -- namespaced names like foo::bar are supported, and
the module name prefix must not be included.
An error is returned if the manifest does not exist, if no matching class or defined type declaration is found in the file, or if a spec file for the named resource already exists.
Generates a new Puppet Resource API transport and its associated files inside the current module directory.
jig new transport <n>
Transport names must start with a lowercase letter and contain only lowercase
letters, numbers, and underscores ([a-z][a-z0-9_]*). Snake case names like
my_device are valid and will be converted to PascalCase (MyDevice) where
required by Ruby class naming conventions. Five files are generated:
lib/puppet/transport/<n>.rb— the transport implementationlib/puppet/transport/schema/<n>.rb— the Resource API transport schemalib/puppet/util/network_device/<n>/device.rb— legacy device compatibility shimspec/unit/puppet/transport/<n>_spec.rb— spec file for the transportspec/unit/puppet/transport/schema/<n>_spec.rb— spec file for the schema
Flags on jig new:
The following flag is available on all jig new subcommands:
| Flag | Description |
|---|---|
-t, --template-dir |
Path to a custom template directory. See Template Overrides below. |
Global flags:
| Flag | Description |
|---|---|
--config |
Path to config file |
--debug |
Enable debug output |
Module naming: jig validates module names against Puppet's naming conventions. Violations produce a warning but do not stop scaffolding.
Extracts all embedded default templates to a directory on disk. This is useful as a starting point for creating your own custom templates. If the destination directory already exists it will be renamed with a timestamp suffix before writing.
jig templates dump <destination>
For example:
jig templates dump ~/.config/jig/templatesYou can then edit the files in the destination directory and point jig at them
using --template-dir or the template_dir config key.
Builds a module package suitable for uploading to the Puppet Forge. The
package is written to pkg/<forge-user>-<module>-<version>.tar.gz relative
to the current directory.
jig build
Metadata validation runs before the build. Errors abort the build; warnings are printed and execution continues.
Validates metadata, sets the version, builds the module package, and publishes it to the Puppet Forge.
jig release [flags]
The release sequence is:
- Validate the version string and module metadata (unless
--skip-validation). - Write the new version into
metadata.json. - Build the module package (unless
--skip-build). - Upload the package to the Forge (unless
--skip-publish).
A Forge API token is required for publishing. Set forge_token in your config
file or pass it via --token. You can generate a token from your account page
on the Puppet Forge.
Flags:
| Flag | Description |
|---|---|
-v, --version |
Version to release, e.g. 1.2.3 (required) |
-k, --token |
Forge API token (overrides forge_token in config) |
--skip-validation |
Skip metadata validation |
--skip-build |
Skip building the module archive |
--skip-publish |
Skip publishing to the Forge |
If --skip-build is set without --skip-publish, jig expects the archive to
already exist under pkg/. An error is returned if it is not found.
Runs validation and linting against the current module. This is a passthrough
command that shells out to bundle exec rake validate lint, so it requires
a Ruby toolchain and the module's bundled gems to be installed.
jig validate [args...]
Any additional arguments are passed through verbatim to the underlying rake invocation.
Runs the module's unit tests. This is a passthrough command that shells out to
bundle exec rake spec, so it requires a Ruby toolchain and the module's
bundled gems to be installed.
jig test unit [args...]
Any additional arguments are passed through verbatim to the underlying rake invocation.
Synchronises the module's managed files from the module's templates. This is a
passthrough command that shells out to bundle exec msync update, so it
requires a Ruby toolchain and the module's bundled gems to be installed.
jig update [args...]
Any additional arguments are passed through verbatim to the underlying msync invocation.
jig embeds default templates for all generated files. If you want to customise them, you can point jig at a directory of your own templates. Any template found in your custom directory takes precedence over the embedded default. Templates not present in your custom directory fall back to the embedded defaults automatically, so you only need to include the files you want to change.
The easiest way to get started is to run jig templates dump to extract the
default templates, then edit the ones you want to change.
Your custom template directory must mirror the structure of jig's embedded templates:
templates/
common/
gitkeep
module/
manifests/
init.pp
spec/
class_spec.rb
spec_helper.rb
default_facts.yml
Gemfile
Rakefile
README.md
CHANGELOG.md
gitignore
pdkignore
rubocop.yml
hiera.yaml
class/
class.pp
class_spec.rb
type/
defined_type.pp
defined_type_spec.rb
fact/
fact.rb
fact_spec.rb
function/
function.pp
function_spec.rb
provider/
type.rb
type_spec.rb
provider.rb
provider_spec.rb
task/
task.sh
metadata.json
transport/
transport.rb
transport_spec.rb
device.rb
schema/
schema.rb
schema_spec.rb
There are three ways to tell jig where your custom templates live, in order of precedence:
Command line flag:
jig new --template-dir /path/to/templates module mymoduleEnvironment variable:
export JIG_TEMPLATE_DIR=/path/to/templates
jig new module mymoduleConfig file (~/.config/jig/config.toml):
template_dir = "/path/to/templates"jig looks for a config file at ~/.config/jig/config.toml. All fields are
optional. If the file does not exist, jig falls back to sensible defaults.
forge_username = "avitacco"
author = "John Doe"
license = "Apache-2.0"
forge_token = "your-forge-token"
template_dir = "/path/to/templates"The config path can be overridden with the --config flag or the
JIG_CONFIG environment variable.
Contributions are welcome. The project is in early stages, so the best place to start is by opening an issue to discuss what you want to work on before sending a PR.
.
├── main.go
├── commands/ # Cobra command definitions
└── internal/
├── build/
├── config/
├── forge/
├── module/ # Module metadata and validation
├── release/
├── scaffold/ # Scaffolding orchestration
└── template/ # Template rendering with fallback logic
└── templates/ # Embedded default templates
Run the full test suite with:
go test ./...Tests live alongside the source files they cover (*_test.go), which is
the standard Go convention. The commands/ and internal/config/ packages
do not currently have tests -- the former is thin Cobra wiring and the latter
is thin Viper wiring, so the internal packages are where the meaningful
coverage lives.
A few patterns used throughout the test suite that contributors should follow:
- Table-driven tests for functions with multiple input variations. Use a
cases := []struct{...}slice andt.Runfor each case. t.TempDir()for any test that touches the filesystem. It is cleaned up automatically after the test and requires nodefer os.Remove.fakeRendererininternal/scaffoldimplements thescaffold.Rendererinterface and can be used to test template rendering paths without hitting the real embedded templates.makeBuildDirininternal/build,makeModuleDirininternal/scaffold, andmakeModuleDirininternal/releaseare shared helpers that create realistic on-disk module structures for tests that need them.fakePublisherininternal/releaseimplements theforge.Publisherinterface for testing the release sequence without making real HTTP calls.- Both characterization tests (pinning current behavior) and adversarial tests (checking rejection of invalid or malicious input) are expected. When adding a new feature, include both.
- Templates are embedded via
go:embed. External templates take precedence over embedded ones, with per-file fallback to embedded defaults when a custom template is not found. Template names are validated to prevent path traversal before any file is read. --forcenever deletes existing files outright. It creates a timestamped backup of the target directory first.- Module name validation uses a
ValidationResulttype with an iota-basedSeverity. Violations at theWarninglevel do not halt execution. Version strings must be valid semver (MAJOR.MINOR.PATCH). URL fields (source,project_page,issues_url) must usehttporhttpsschemes when present; invalid URLs are errors that abort the build and release. - The Forge HTTP client (
internal/forge) is hidden behind aPublisherinterface so the release sequence can be tested without making real network requests. - Component names (module names, class names, defined type names) are validated to reject empty strings, path separators, and traversal sequences before they are used to construct filesystem paths.
os.Getwd()is called only in thecommands/layer. Internal packages receive directory paths as arguments, which keeps them testable without manipulating the process working directory.- Config is handled with Viper.
Some default template files included in this project are derived from the pdk-templates project, copyright Puppet Labs, and are used under the terms of the Apache License, Version 2.0.
See LICENSE.