Skip to content

Add VCS module publish command - #105

Open
AadarshIBM wants to merge 1 commit into
hashicorp:mainfrom
AadarshIBM:module-publish
Open

Add VCS module publish command#105
AadarshIBM wants to merge 1 commit into
hashicorp:mainfrom
AadarshIBM:module-publish

Conversation

@AadarshIBM

@AadarshIBM AadarshIBM commented Aug 24, 2026

Copy link
Copy Markdown

Description

Adds tfctl module publish for publishing VCS private registry modules from existing OAuth or GitHub App connections.

The command:

  • Requires a repository and exactly one VCS connection ID.
  • Uses tag-based publishing by default.
  • Supports branch-based publishing with an optional initial version.
  • Resolves the organization from the explicit flag, active profile, or Terraform configuration.
  • Supports JSON, Markdown, dry-run, and quiet behavior.
  • Uses tfctl's existing authenticated client because go-tfe/v2 does not yet provide a method for this endpoint.
  • Returns immediately without polling and reports the API status verbatim.
  • Restricts output and logs to safe module metadata.

For repositories that follow the standard terraform-<provider>-<name> naming convention, HCP Terraform automatically determines the module name and provider. Custom name/provider overrides and repositories requiring different VCS identifier and display-identifier values are outside this initial scope and can use tfctl api.

Validation completed:

  • go test ./internal/commands/module ./internal/commands/root -count=1
  • make gen/screenshot
  • make bin
  • make check
  • go test ./... -race
  • git diff --check

Example Output

Tag-based publishing dry run:

$ tfctl module publish \
    --repo acme/terraform-aws-network \
    --oauth-token-id ot-... \
    --organization acme \
    --dry-run

DRY RUN: would publish VCS-backed module from repository "acme/terraform-aws-network" to organization "acme" using tag-based publishing

Branch-based publishing:

$ tfctl module publish \
    --repo acme/terraform-aws-network \
    --github-app-installation-id ghain-... \
    --branch main \
    --initial-version 1.0.0

PR Checklist

  • Prepared a changelog entry for the next set of release notes.
  • Ensured the command is sensitive to these global flags:
    • --json — Forces machine-readable output to stdout.
    • --markdown — Forces Markdown output to stdout.
    • --dry-run — Resolves and validates the request without sending a mutation.
    • --quiet — Suppresses successful output and unessential guidance.
  • Added safe debug logging using the logger from the command context.
  • Regenerated the root command screenshot with make gen/screenshot.
  • Reviewed autocomplete behavior. Repository, connection, branch, and version values do not have reliable local autocomplete, so autocomplete is not provided for them.

PCI review checklist

  • I have documented a clear reason for, and description of, the change I am making.
  • Reverting this pull request fully removes the command. There are no migrations or persisted local state changes.
  • This change does not modify security controls. Request bodies and VCS connection IDs are not included in command output or logs.

@paladin-devops paladin-devops left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some pretty minor feedback, great job adding this command!

Comment on lines +17 to +21
ShortHelp: "Manage private registry modules.",
LongHelp: heredoc.New(inv.IO).Mustf(`
The {{ template "mdCodeOrBold" "%s module" }} command group lets you manage
private registry modules in HCP Terraform and Terraform Enterprise.
`, version.Name),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good description and short help text. 👍🏻

Comment on lines +251 to +255
if opts.Quiet {
logger.Debug("Quiet mode enabled, rendering skipped")
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should drain the body before returning here. This way, if the server were to have keep-alives enabled, then the TCP connection could be returned to the connection pool, instead of being closed, which is the default behavior of Go's HTTP implementation for undrained response body.

Suggested change
if opts.Quiet {
logger.Debug("Quiet mode enabled, rendering skipped")
return nil
}
if opts.Quiet {
logger.Debug("Quiet mode enabled, rendering skipped")
io.Copy(io.Discard, resp.Body)
return nil
}

@@ -0,0 +1,3 @@
kind: ENHANCEMENTS
body: "Added `tfctl module publish` for publishing VCS-backed private registry modules from existing OAuth or GitHub App connections"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Present-tense changelog note.

Suggested change
body: "Added `tfctl module publish` for publishing VCS-backed private registry modules from existing OAuth or GitHub App connections"
body: "tfctl now has `tfctl module publish` for publishing VCS-backed private registry modules from existing OAuth or GitHub App connections."

if response.Data.Links.Self != "" {
result.SelfLink, err = resolvePublishSelfLink(opts.Client.BaseURL, response.Data.Links.Self)
if err != nil {
return fmt.Errorf("failed to resolve registry module self link: %w", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a test to publish_test.go for this error case.

return fields
}

func resolvePublishSelfLink(base *url.URL, self string) (string, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we could add some tests to publish_test.go, maybe in a new function TestResolvePublishSelfLink, which just tests this function's paths, rather than doing so implicitly via TestRunPublishOutputFormats.

})
},
})
opts.Quiet = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Quiet need to be true for this test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants