Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

template-tf-provider

template-tf-provider is the GitHub template Meigma Terraform providers start from. It is a working Plugin Framework provider — a worked resource and data source over a hexagonal package split — wrapped in the toolchain, documentation pipeline, and release machinery a published provider needs.

The release path is enabled in this repository rather than left as a sketch, so the whole thing (GPG signing, registry contract checks, attestation) is exercised here before a generated project inherits it.

Quickstart

  1. Create a repository from this template. Name it terraform-provider-<name>, all lowercase — the Terraform Registry accepts no other pattern.
  2. Clone it and run mise install.
  3. Work through DELETE_ME.md, which covers renaming the module and provider, replacing the example resource, and the one-time repository setup needed before the first release.

Local Bootstrap

Prerequisites:

  • mise — provisions every pinned tool from mise.toml and mise.lock: Go, Moon, Python and uv (for the MkDocs project), golangci-lint, tfplugindocs, terraform and tofu for acceptance tests, and goreleaser/syft/cosign for releases. Run mise install once; there is nothing else to install by hand.

mise.lock records a per-platform download URL and checksum for each tool (and, for the aqua-backed CLIs, cosign/SLSA/GitHub-attestation verification). mise install runs with locked = true, so it fails closed if a tool lacks a pre-resolved, checksummed entry for the current platform. Moon runs every task against these tools as system binaries on PATH and manages no toolchain itself. To bump a tool, edit its version in mise.toml, run mise lock --platform linux-x64,linux-arm64,macos-x64,macos-arm64, and commit both files.

The Example Provider

The provider serves one resource and one data source, both named example_item, backed by a JSON file on disk. They are meant to be replaced. What should survive the replacement is the split they demonstrate:

Package Role
internal/core The domain: the item type, its rules, and the Store port. No Terraform types, no I/O.
internal/client A Store implementation over a JSON file. A real provider puts an HTTP client here.
internal/provider The Terraform adapter: configuration, plan, and state in, domain calls out, diagnostics back.

Dependencies point inward, so the domain tests run without side effects, the provider tests run against a mockery-generated mock, and the storage adapter is tested on its own. How this provider is structured covers why, including the rule that a provider may not silently rewrite a value the user configured.

Development

Moon is the task front door:

moon run root:format
moon run root:lint
moon run root:build
moon run root:test
moon run root:check

check is the aggregate: formatting, linting, build, unit tests, the release script tests, the docs drift check, the registry docs validation, and the MkDocs build. CI runs the same path:

moon ci --summary minimal

moon run root:build writes bin/terraform-provider-example. A provider is a plugin, so running that binary directly only prints an error; to exercise it against a CLI, see Use the provider from a local build.

Acceptance tests

Acceptance tests drive a real Terraform-compatible CLI through plan, apply, import, and destroy. Nothing runs them automatically — not moon ci, not a pull request, not a schedule:

moon run root:testacc                                  # the pinned OpenTofu
TF_ACC_TERRAFORM_PATH=$(mise which terraform) \
  moon run root:testacc                                # the pinned Terraform

Or dispatch the Acceptance Tests workflow from the Actions tab and pick tofu, terraform, or both.

The task is marked runInCI: false, and Moon drops such tasks whenever CI is set in the environment — including for an explicit moon run root:testacc. If you export CI in your shell, the run fails with "No tasks found" rather than skipping quietly. Unset it, or run the task through env -u CI.

Documentation

docs/ is one tree with two consumers. The Terraform and OpenTofu registries read docs/index.md, docs/resources/, and docs/data-sources/ from it in their own format; MkDocs builds the same tree, plus the hand-written tutorial, how-to guides, and explanation, into the published site.

Those three registry paths are generated by tfplugindocs from the provider schema, templates/, and examples/. Never edit them by handmoon run root:check regenerates them into a scratch directory and fails when the committed copies differ.

moon run root:docs-gen       # regenerate index.md, resources/, data-sources/
moon run root:docs-check     # fail if the committed pages have drifted
moon run root:docs-validate  # check the registries' own rules on those pages
moon run root:docs-build     # build the MkDocs site into site/
moon run root:docs-serve     # preview it at http://127.0.0.1:8000

Everything else under docs/ is written by hand and belongs in mkdocs.yml's nav. The MkDocs config, the uv project, and the Python pin all live at the repository root, because MkDocs requires its config outside docs_dir and the registries expect nothing but documentation inside it.

CI and Repository Settings

The CI workflow keeps token permissions minimal, pins external actions, disables checkout credential persistence, and delegates the checks to Moon. It caches Go modules, Go build artifacts, golangci-lint state, and uv downloads through GitHub Actions; Moon remote caching is left as an optional follow-up for repositories that need a shared task-output cache. The GitHub Pages workflow builds the site on pull requests and deploys site/ from the default branch. Dependabot covers GitHub Actions, the Go module, and the uv project.

Repository settings live in .github/repository-settings.toml and are applied by .github/scripts/configure_github_repo.py. They set signed commits, squash-only merges, protected tags, immutable releases, private vulnerability reporting, and Pages publishing from a workflow.

Release Layer

A release publishes exactly what the two registries ingest. Both discover a release by reading its asset names, so for version X.Y.Z (tag vX.Y.Z) the release carries:

Asset Contents
terraform-provider-example_X.Y.Z_<os>_<arch>.zip One per platform (13 in total), each holding a single binary named terraform-provider-example_vX.Y.Z (.exe on Windows)
terraform-provider-example_X.Y.Z_manifest.json Copy of terraform-registry-manifest.json, declaring protocol 6.0
terraform-provider-example_X.Y.Z_SHA256SUMS Checksums for every zip, SBOM, and the manifest
terraform-provider-example_X.Y.Z_SHA256SUMS.sig Detached binary GPG signature the registries verify

The release also ships a CycloneDX SBOM per zip and a cosign bundle over the checksum file. Those are additive; the registries ignore them.

The release path is:

  • Release Please opens and maintains the release PR.
  • Release Please creates the tag and a draft GitHub release after merge.
  • Release Dry Run rehearses the whole GoReleaser path — GPG signing and the contract checks included — against a synthetic version on release PRs, and uploads nothing.
  • GoReleaser builds the zips, SBOMs, and checksum file, and signs the checksum file with the repository's GPG key.
  • The release workflow stages the assets, asserts the registry contract (scripts/check-release-contract.sh), smoke tests the host binary, and uploads everything to the draft.
  • A separate job adds a keyless cosign signature over the checksum file, and the isolated attest.yml reusable workflow generates GitHub-native provenance for every file the checksum file lists.
  • The draft becomes public only after all of the above succeed. A failure anywhere leaves the release a draft, invisible to both registries.

Generating provenance in the isolated attest.yml reusable workflow rather than in the build job keeps the signing identity unreachable by build steps — the SLSA Build L3 isolation requirement — while staying on GitHub's attestation API.

Publishing

Before the first release, provision the signing key and register it with both registries:

scripts/gpg-provision.sh

It generates a dedicated RSA 4096 signing key, stores the private half and its passphrase in the GPG_PRIVATE_KEY and GPG_PASSPHRASE Actions secrets, and prints the public key. Neither registry accepts a release it cannot verify against a key you registered first. Re-run with --force only to rotate the key; the script refuses to overwrite existing secrets otherwise.

How to publish the provider to the registries walks through key registration and the one-time listing in each registry.

Verifying a Release

Import the project's public signing key (the one registered with the registries), then:

gh release download vX.Y.Z --repo meigma/template-tf-provider --dir provider
cd provider

# 1. The signature the registries themselves check.
gpg --verify terraform-provider-example_X.Y.Z_SHA256SUMS.sig \
  terraform-provider-example_X.Y.Z_SHA256SUMS

# 2. Every downloaded file matches the checksum file.
sha256sum -c terraform-provider-example_X.Y.Z_SHA256SUMS

# 3. The checksum file came from this repository's release workflow.
cosign verify-blob \
  --bundle terraform-provider-example_X.Y.Z_SHA256SUMS.sigstore.json \
  --certificate-identity https://github.com/meigma/template-tf-provider/.github/workflows/release.yml@refs/tags/vX.Y.Z \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  terraform-provider-example_X.Y.Z_SHA256SUMS

# 4. GitHub-native build provenance for any artifact.
gh attestation verify terraform-provider-example_X.Y.Z_linux_amd64.zip \
  --repo meigma/template-tf-provider \
  --signer-workflow meigma/template-tf-provider/.github/workflows/attest.yml \
  --source-ref refs/tags/vX.Y.Z \
  --deny-self-hosted-runners

Repositories generated from this template must update the provider name, module path, provider address, and repository slugs in these commands before cutting a release. DELETE_ME.md lists every place those names appear.

Contributing

See CONTRIBUTING.md for contribution guidelines, local setup expectations, and pull request workflow.

Security

See SECURITY.md for supported versions and the private vulnerability reporting path.

License

Add the repository license before publishing a project generated from this template.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

Generated from meigma/template-go