From acec9cbd5154acdff896f4971d97a56575fccdf6 Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:49:00 +0000 Subject: [PATCH 1/9] feat: add subcategory support to codegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Subcategory field to TerraformProviderConfig struct for YAML override support - Add Subcategory field to TerraformProviderSpecMetadata struct - Implement deriveSubcategoryFromPath() to extract subcategory from spec directory - Generate individual tfplugindocs templates per resource/data source with subcategory - Pass subcategory through to terraform provider metadata Subcategory derivation from directory structure: - specs/network/ → "Network" - specs/objects/ → "Objects" - specs/device/ → "Device" - specs/panorama/ → "Panorama" - specs/policies/ → "Policies" The generated templates in target/terraform/templates/ will have the subcategory hardcoded, eliminating the need for fix-docs.go post-processing script. --- pkg/commands/codegen/codegen.go | 151 ++++++++++++++++++ pkg/properties/normalized.go | 1 + pkg/properties/provider_file.go | 1 + .../terraform_provider_file.go | 1 + 4 files changed, 154 insertions(+) diff --git a/pkg/commands/codegen/codegen.go b/pkg/commands/codegen/codegen.go index 40657e28..d5e11617 100644 --- a/pkg/commands/codegen/codegen.go +++ b/pkg/commands/codegen/codegen.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "log/slog" + "os" + "path/filepath" + "strings" "github.com/paloaltonetworks/pan-os-codegen/pkg/generate" "github.com/paloaltonetworks/pan-os-codegen/pkg/load" @@ -44,6 +47,141 @@ func NewCommand(ctx context.Context, commandType properties.CommandType, args .. }, nil } +// deriveSubcategoryFromPath extracts the subcategory from the spec file path. +// It maps directory names to proper subcategory names. +// For example: specs/network/interface.yaml -> "Network" +func deriveSubcategoryFromPath(specPath string) string { + // Extract the directory name between specs/ and the filename + dir := filepath.Dir(specPath) + parts := strings.Split(filepath.ToSlash(dir), "/") + + // Find the part after "specs" + var category string + for i, part := range parts { + if part == "specs" && i+1 < len(parts) { + category = parts[i+1] + break + } + } + + // Map directory names to subcategory names + subcategoryMap := map[string]string{ + "network": "Network", + "objects": "Objects", + "device": "Device", + "panorama": "Panorama", + "policies": "Policies", + "actions": "", // empty for actions + "schema": "", // empty for schema + } + + if subcategory, ok := subcategoryMap[category]; ok { + return subcategory + } + + // Default to empty string if no mapping found + return "" +} + +// generateTfplugindocsTemplates creates individual documentation templates for each resource/data source +// with the correct subcategory. These templates are used by terraform-plugin-docs when generating documentation. +func generateTfplugindocsTemplates(outputDir string, specMetadata map[string]properties.TerraformProviderSpecMetadata) error { + templatesDir := filepath.Join(outputDir, "templates") + resourcesDir := filepath.Join(templatesDir, "resources") + dataSourcesDir := filepath.Join(templatesDir, "data-sources") + + if err := os.MkdirAll(resourcesDir, 0755); err != nil { + return fmt.Errorf("error creating resources templates directory: %w", err) + } + if err := os.MkdirAll(dataSourcesDir, 0755); err != nil { + return fmt.Errorf("error creating data sources templates directory: %w", err) + } + + resourceCount := 0 + dataSourceCount := 0 + + for resourceSuffix, metadata := range specMetadata { + // Generate template for resources + if metadata.Flags&properties.TerraformSpecResource != 0 { + subcategory := metadata.Subcategory + if subcategory == "" { + subcategory = "Uncategorized" + } + + resourceTemplate := fmt.Sprintf(`--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "%s" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile .ExampleFile }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{ if .HasImport -}} +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" .ImportFile }} +{{- end }} +`, subcategory) + + resourcePath := filepath.Join(resourcesDir, fmt.Sprintf("panos%s.md.tmpl", resourceSuffix)) + if err := os.WriteFile(resourcePath, []byte(resourceTemplate), 0644); err != nil { + return fmt.Errorf("error writing resource template %s: %w", resourcePath, err) + } + resourceCount++ + } + + // Generate template for data sources + if metadata.Flags&properties.TerraformSpecDatasource != 0 { + subcategory := metadata.Subcategory + if subcategory == "" { + subcategory = "Uncategorized" + } + + dataSourceTemplate := fmt.Sprintf(`--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "%s" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile .ExampleFile }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +`, subcategory) + + dataSourcePath := filepath.Join(dataSourcesDir, fmt.Sprintf("panos%s.md.tmpl", resourceSuffix)) + if err := os.WriteFile(dataSourcePath, []byte(dataSourceTemplate), 0644); err != nil { + return fmt.Errorf("error writing data source template %s: %w", dataSourcePath, err) + } + dataSourceCount++ + } + } + + slog.Info("Generated tfplugindocs templates", "resources", resourceCount, "dataSources", dataSourceCount, "templatesDir", templatesDir) + return nil +} + func (c *Command) Setup() error { var err error if c.specs == nil { @@ -94,6 +232,13 @@ func (c *Command) Execute() error { return fmt.Errorf("%s sanity failed: %s", specPath, err) } + // Extract subcategory: use YAML override if present, otherwise derive from path + if c.commandType == properties.CommandTypeTerraform { + if spec.TerraformProviderConfig.Subcategory == "" { + spec.TerraformProviderConfig.Subcategory = deriveSubcategoryFromPath(specPath) + } + } + if c.commandType == properties.CommandTypeTerraform { var singularVariant, pluralVariant bool // For specs that are missing resource_variants, default to generating @@ -198,6 +343,12 @@ func (c *Command) Execute() error { if err != nil { return fmt.Errorf("error generating terraform code: %w", err) } + + // Generate tfplugindocs templates with subcategory support + if err = generateTfplugindocsTemplates(config.Output.TerraformProvider, specMetadata); err != nil { + return fmt.Errorf("error generating tfplugindocs templates: %w", err) + } + slog.Debug("Generated Terraform resources", "resources", resourceList, "dataSources", dataSourceList) } diff --git a/pkg/properties/normalized.go b/pkg/properties/normalized.go index 71479dfe..7d05c3fc 100644 --- a/pkg/properties/normalized.go +++ b/pkg/properties/normalized.go @@ -81,6 +81,7 @@ const ( type TerraformProviderConfig struct { Description string `json:"description" yaml:"description"` + Subcategory string `json:"subcategory" yaml:"subcategory"` Ephemeral bool `json:"ephemeral" yaml:"ephemeral"` Action bool `json:"action" yaml:"action"` CustomValidation bool `json:"custom_validation" yaml:"custom_validation"` diff --git a/pkg/properties/provider_file.go b/pkg/properties/provider_file.go index e4e86245..10a9e815 100644 --- a/pkg/properties/provider_file.go +++ b/pkg/properties/provider_file.go @@ -76,6 +76,7 @@ const ( type TerraformProviderSpecMetadata struct { ResourceSuffix string StructName string + Subcategory string Flags TerraformSpecFlags } diff --git a/pkg/translate/terraform_provider/terraform_provider_file.go b/pkg/translate/terraform_provider/terraform_provider_file.go index ffe8ceb8..88b5bf7c 100644 --- a/pkg/translate/terraform_provider/terraform_provider_file.go +++ b/pkg/translate/terraform_provider/terraform_provider_file.go @@ -102,6 +102,7 @@ func (g *GenerateTerraformProvider) appendResourceType(spec *properties.Normaliz terraformProvider.SpecMetadata[names.MetaName] = properties.TerraformProviderSpecMetadata{ ResourceSuffix: names.MetaName, StructName: names.StructName, + Subcategory: spec.TerraformProviderConfig.Subcategory, Flags: flags, } return nil From 313d1bd67133c75d10a56ec3a9b74fd9a9a8b4ae Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:55:34 +0000 Subject: [PATCH 2/9] feat: add release automation script Add scripts/release.sh to automate the PAN-OS release process with three modes: - --auto: Fully automated (runs codegen, versions, tags, pushes) - --manual: Interactive (prompts for confirmation, no auto-push) - --dry-run: Simulation (shows what would be done) The script handles: - Running codegen in pan-os-codegen - Copying generated code to pango and terraform-provider-panos - Version determination using standard-version (with fallback) - Creating commits and tags - Running gofix and terraform doc generation - Validating subcategories in documentation - Pushing to remote (in auto mode only) Uses npx standard-version for conventional commit-based versioning, with fallback to manual detection if standard-version is unavailable. Includes comprehensive logging with timestamps and colored output. --- scripts/release.sh | 411 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100755 scripts/release.sh diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 00000000..a4bf5265 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,411 @@ +#!/bin/bash +# scripts/release.sh - Automates the PAN-OS release process +# +# This script automates the process of releasing pango and terraform-provider-panos +# by running codegen, copying generated code, and creating version tags. +# +# Usage: +# ./scripts/release.sh [--auto|--manual|--dry-run] +# +# Modes: +# --auto Fully automated mode (runs everything, creates tags, pushes to remote) +# --manual Interactive mode (prompts for confirmation at key steps, no auto-push) +# --dry-run Simulation mode (shows what would be done without making changes) + +set -e # Exit on error +set -o pipefail + +# Configuration +MODE="manual" # Default mode +CODEGEN_DIR="$HOME/workspace/pan-os-codegen" +PANGO_DIR="$HOME/workspace/pango" +TERRAFORM_DIR="$HOME/workspace/terraform-provider-panos" +GOFIX_SCRIPT="$HOME/workspace/zsh-scripts/go-mod-replace.sh" +FIXDOCS_SCRIPT="$HOME/workspace/zsh-scripts/fix-docs.go" +FIXDOCS_CONFIG="$HOME/workspace/zsh-scripts/config.json" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${BLUE}$(date '+%Y-%m-%d %H:%M:%S')${NC} - $1" +} + +log_success() { + echo -e "${GREEN}$(date '+%Y-%m-%d %H:%M:%S')${NC} - $1" +} + +log_warning() { + echo -e "${YELLOW}$(date '+%Y-%m-%d %H:%M:%S')${NC} - $1" +} + +log_error() { + echo -e "${RED}$(date '+%Y-%m-%d %H:%M:%S')${NC} - $1" +} + +# Parse command-line arguments +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --auto) + MODE="auto" + shift + ;; + --manual) + MODE="manual" + shift + ;; + --dry-run) + MODE="dry-run" + shift + ;; + -h|--help) + echo "Usage: $0 [--auto|--manual|--dry-run]" + echo "" + echo "Modes:" + echo " --auto Fully automated mode (runs everything, creates tags, pushes to remote)" + echo " --manual Interactive mode (prompts for confirmation at key steps, no auto-push)" + echo " --dry-run Simulation mode (shows what would be done without making changes)" + exit 0 + ;; + *) + log_error "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac + done +} + +# Execute command based on mode +execute() { + local cmd="$1" + local description="$2" + + if [ "$MODE" = "dry-run" ]; then + log_info "[DRY-RUN] $description" + log_info "[DRY-RUN] Would execute: $cmd" + else + log_info "$description" + eval "$cmd" + fi +} + +# Check if repository is clean +check_repo_clean() { + local repo_dir="$1" + local repo_name=$(basename "$repo_dir") + + cd "$repo_dir" + + if [ -n "$(git status --porcelain)" ]; then + log_warning "Repository $repo_name has uncommitted changes" + + if [ "$MODE" = "auto" ]; then + log_error "Auto mode requires clean repositories. Exiting." + exit 1 + elif [ "$MODE" = "manual" ]; then + echo -n "Stash changes in $repo_name? (y/n): " + read -r response + if [[ "$response" =~ ^[Yy]$ ]]; then + execute "git stash" "Stashing changes in $repo_name" + else + log_error "Please commit or stash changes before continuing" + exit 1 + fi + fi + else + log_success "Repository $repo_name is clean" + fi +} + +# Check if npx is available +check_npx() { + if ! command -v npx &> /dev/null; then + log_error "npx not found. Please install Node.js to use conventional-changelog tooling." + log_error "Alternatively, install with: brew install node" + exit 1 + fi +} + +# Determine next version using conventional commits with standard-version +determine_next_version() { + local repo_dir="$1" + cd "$repo_dir" + + # Get the last tag + local last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + log_info "Last tag: $last_tag" + + # Get commits since last tag + local commits=$(git log --oneline --no-merges "$last_tag..HEAD" --format="%s" 2>/dev/null || echo "") + + if [ -z "$commits" ]; then + log_warning "No new commits since $last_tag" + echo "$last_tag" + return + fi + + # Use npx standard-version to determine next version (dry-run mode) + log_info "Using standard-version to determine next version..." + local next_version=$(npx --yes standard-version --dry-run --silent 2>&1 | grep "tagging release" | sed -n 's/.*tagging release \(v[0-9.]*\)/\1/p' | head -1) + + # If standard-version didn't work, fall back to manual detection + if [ -z "$next_version" ]; then + log_warning "standard-version failed, using fallback version detection" + + # Parse current version + local version="${last_tag#v}" + IFS='.' read -r major minor patch <<< "$version" + + # Check for breaking changes (major bump) + if echo "$commits" | grep -qE "^[^:]+!:|BREAKING CHANGE:"; then + major=$((major + 1)) + minor=0 + patch=0 + # Check for features (minor bump) + elif echo "$commits" | grep -qE "^feat:"; then + minor=$((minor + 1)) + patch=0 + # Default to patch bump + else + patch=$((patch + 1)) + fi + + next_version="v${major}.${minor}.${patch}" + fi + + log_info "Next version: $next_version" + echo "$next_version" +} + +# Run codegen +run_codegen() { + log_info "Running codegen..." + cd "$CODEGEN_DIR" + + execute "make clean" "Cleaning previous build" + execute "make codegen" "Generating code" + + log_success "Codegen completed" +} + +# Copy generated code +copy_generated_code() { + local source_dir="$1" + local dest_dir="$2" + local name="$3" + + log_info "Copying generated code to $name..." + + if [ "$MODE" = "dry-run" ]; then + log_info "[DRY-RUN] Would copy: $source_dir -> $dest_dir" + else + cp -r "$source_dir"/* "$dest_dir"/ + log_success "Copied generated code to $name" + fi +} + +# Create commit and tag +create_commit_and_tag() { + local repo_dir="$1" + local version="$2" + local message="$3" + + cd "$repo_dir" + + if [ "$MODE" = "manual" ]; then + echo -n "Create commit and tag $version? (y/n): " + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + log_warning "Skipping commit and tag creation" + return + fi + fi + + execute "git add -A" "Staging all changes" + execute "git commit -m '$message'" "Creating commit" + execute "git tag -a '$version' -m 'Release $version'" "Creating tag $version" + + log_success "Created commit and tag $version" +} + +# Push to remote +push_to_remote() { + local repo_dir="$1" + local repo_name="$2" + + cd "$repo_dir" + + if [ "$MODE" = "auto" ]; then + execute "git push origin HEAD" "Pushing commits to remote" + execute "git push origin --tags" "Pushing tags to remote" + log_success "Pushed $repo_name to remote" + else + log_info "Skipping push in $MODE mode (push manually when ready)" + fi +} + +# Release pango +release_pango() { + log_info "=== Releasing pango ===" + + # Copy generated code + copy_generated_code "$CODEGEN_DIR/target/pango" "$PANGO_DIR" "pango" + + # Determine next version + local next_version=$(determine_next_version "$PANGO_DIR") + + if [ -z "$next_version" ] || [ "$next_version" = "$(cd $PANGO_DIR && git describe --tags --abbrev=0 2>/dev/null)" ]; then + log_warning "No version bump needed for pango" + return + fi + + # Create commit and tag + create_commit_and_tag "$PANGO_DIR" "$next_version" "chore(release): auto-generated $next_version" + + # Push to remote + push_to_remote "$PANGO_DIR" "pango" + + log_success "Pango release completed: $next_version" +} + +# Validate subcategories in docs +validate_subcategories() { + local docs_dir="$TERRAFORM_DIR/docs" + + log_info "Validating subcategories in documentation..." + + if [ ! -d "$docs_dir" ]; then + log_warning "Docs directory not found, skipping validation" + return 0 + fi + + local missing=$(grep -r "subcategory: \$" "$docs_dir" 2>/dev/null || true) + + if [ -n "$missing" ]; then + log_error "Found missing subcategories:" + echo "$missing" + + if [ "$MODE" = "auto" ]; then + log_error "Auto mode requires all subcategories to be present. Exiting." + exit 1 + elif [ "$MODE" = "manual" ]; then + echo -n "Continue anyway? (y/n): " + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + exit 1 + fi + fi + return 1 + else + log_success "All subcategories present" + return 0 + fi +} + +# Release terraform provider +release_terraform_provider() { + log_info "=== Releasing terraform-provider-panos ===" + + # Copy generated code + copy_generated_code "$CODEGEN_DIR/target/terraform" "$TERRAFORM_DIR" "terraform-provider-panos" + + cd "$TERRAFORM_DIR" + + # Run gofix + if [ -f "$GOFIX_SCRIPT" ]; then + log_info "Running gofix to update pango dependency..." + execute "bash '$GOFIX_SCRIPT'" "Running go-mod-replace.sh" + else + log_warning "gofix script not found at $GOFIX_SCRIPT, skipping" + fi + + # Run go mod tidy + execute "go mod tidy" "Running go mod tidy" + + # Generate docs + log_info "Generating terraform docs..." + execute "go generate ./..." "Running go generate" + + # Validate subcategories + validate_subcategories + + # Determine next version + local next_version=$(determine_next_version "$TERRAFORM_DIR") + + if [ -z "$next_version" ] || [ "$next_version" = "$(cd $TERRAFORM_DIR && git describe --tags --abbrev=0 2>/dev/null)" ]; then + log_warning "No version bump needed for terraform-provider-panos" + return + fi + + # Create commit and tag + create_commit_and_tag "$TERRAFORM_DIR" "$next_version" "chore(release): auto-generated $next_version" + + # Push to remote + push_to_remote "$TERRAFORM_DIR" "terraform-provider-panos" + + log_success "Terraform provider release completed: $next_version" +} + +# Display summary +display_summary() { + log_info "=== Release Summary ===" + echo "" + echo "Mode: $MODE" + echo "" + echo "Pango:" + echo " Directory: $PANGO_DIR" + echo " Latest tag: $(cd $PANGO_DIR && git describe --tags --abbrev=0 2>/dev/null || echo 'none')" + echo "" + echo "Terraform Provider:" + echo " Directory: $TERRAFORM_DIR" + echo " Latest tag: $(cd $TERRAFORM_DIR && git describe --tags --abbrev=0 2>/dev/null || echo 'none')" + echo "" + + if [ "$MODE" = "auto" ]; then + log_success "Releases pushed to remote" + else + log_info "Changes are local only. Push manually when ready:" + echo " cd $PANGO_DIR && git push origin HEAD && git push origin --tags" + echo " cd $TERRAFORM_DIR && git push origin HEAD && git push origin --tags" + fi +} + +# Main function +main() { + parse_args "$@" + + log_info "Running in $MODE mode..." + echo "" + + # Check for required tools + check_npx + + # Check all repositories are clean + check_repo_clean "$CODEGEN_DIR" + check_repo_clean "$PANGO_DIR" + check_repo_clean "$TERRAFORM_DIR" + + # Run codegen + run_codegen + + # Release pango + release_pango + + # Release terraform provider + release_terraform_provider + + # Display summary + display_summary + + log_success "Release automation completed!" +} + +# Run main +main "$@" From 49106095849acd97b12854f5f73bf12d94160bfb Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:06:47 +0000 Subject: [PATCH 3/9] fix: correct Makefile config path and accumulate metadata flags - Fix Makefile to use correct config path (cmd/codegen/config.yaml) - Accumulate flags when same resource has both resource and datasource - Add debug logging for template generation This ensures tfplugindocs templates are generated correctly with proper subcategories for all resources and data sources. --- Makefile | 2 +- pkg/commands/codegen/codegen.go | 3 +++ pkg/translate/terraform_provider/terraform_provider_file.go | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 62233783..5fd9fa95 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ assets: $(ASSETS_DST) .PHONY: codegen codegen: codegen-stamp codegen-stamp: target/codegen $(CODEGEN_SPECS) - CODEGEN_LOG_LEVEL=$(CODEGEN_LOG_LEVEL) ./target/codegen -config config.yaml + CODEGEN_LOG_LEVEL=$(CODEGEN_LOG_LEVEL) ./target/codegen -config cmd/codegen/config.yaml touch $@ $(GENERATED_OUT_PATH)/%: assets/% diff --git a/pkg/commands/codegen/codegen.go b/pkg/commands/codegen/codegen.go index d5e11617..891afceb 100644 --- a/pkg/commands/codegen/codegen.go +++ b/pkg/commands/codegen/codegen.go @@ -101,6 +101,8 @@ func generateTfplugindocsTemplates(outputDir string, specMetadata map[string]pro dataSourceCount := 0 for resourceSuffix, metadata := range specMetadata { + slog.Debug("Processing spec metadata", "resourceSuffix", resourceSuffix, "subcategory", metadata.Subcategory, "flags", metadata.Flags) + // Generate template for resources if metadata.Flags&properties.TerraformSpecResource != 0 { subcategory := metadata.Subcategory @@ -345,6 +347,7 @@ func (c *Command) Execute() error { } // Generate tfplugindocs templates with subcategory support + slog.Debug("Generating tfplugindocs templates", "metadataCount", len(specMetadata)) if err = generateTfplugindocsTemplates(config.Output.TerraformProvider, specMetadata); err != nil { return fmt.Errorf("error generating tfplugindocs templates: %w", err) } diff --git a/pkg/translate/terraform_provider/terraform_provider_file.go b/pkg/translate/terraform_provider/terraform_provider_file.go index 88b5bf7c..062c00ca 100644 --- a/pkg/translate/terraform_provider/terraform_provider_file.go +++ b/pkg/translate/terraform_provider/terraform_provider_file.go @@ -99,6 +99,11 @@ func (g *GenerateTerraformProvider) appendResourceType(spec *properties.Normaliz case properties.ResourceCustom, properties.ResourceConfig: } + // Accumulate flags if metadata already exists for this resource + if existing, ok := terraformProvider.SpecMetadata[names.MetaName]; ok { + flags |= existing.Flags + } + terraformProvider.SpecMetadata[names.MetaName] = properties.TerraformProviderSpecMetadata{ ResourceSuffix: names.MetaName, StructName: names.StructName, From a8cb8050217479a1ca64fc7bb5b38a95a2adea79 Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:50:54 +0000 Subject: [PATCH 4/9] fix: remove panos prefix from template filenames terraform-plugin-docs automatically prepends the provider name when looking up templates. Template files should be named without the provider prefix: - Before: panos_address.md.tmpl (looked for panos_panos_address) - After: address.md.tmpl (correctly looks for panos_address) This fixes the 'does not exist' error when generating terraform docs. --- pkg/commands/codegen/codegen.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/commands/codegen/codegen.go b/pkg/commands/codegen/codegen.go index 891afceb..91c6f93b 100644 --- a/pkg/commands/codegen/codegen.go +++ b/pkg/commands/codegen/codegen.go @@ -138,7 +138,10 @@ Import is supported using the following syntax: {{- end }} `, subcategory) - resourcePath := filepath.Join(resourcesDir, fmt.Sprintf("panos%s.md.tmpl", resourceSuffix)) + // Remove leading underscore from resourceSuffix for template filename + // terraform-plugin-docs automatically prepends the provider name (panos_) + templateName := strings.TrimPrefix(resourceSuffix, "_") + resourcePath := filepath.Join(resourcesDir, fmt.Sprintf("%s.md.tmpl", templateName)) if err := os.WriteFile(resourcePath, []byte(resourceTemplate), 0644); err != nil { return fmt.Errorf("error writing resource template %s: %w", resourcePath, err) } @@ -172,7 +175,10 @@ description: |- {{ .SchemaMarkdown | trimspace }} `, subcategory) - dataSourcePath := filepath.Join(dataSourcesDir, fmt.Sprintf("panos%s.md.tmpl", resourceSuffix)) + // Remove leading underscore from resourceSuffix for template filename + // terraform-plugin-docs automatically prepends the provider name (panos_) + templateName := strings.TrimPrefix(resourceSuffix, "_") + dataSourcePath := filepath.Join(dataSourcesDir, fmt.Sprintf("%s.md.tmpl", templateName)) if err := os.WriteFile(dataSourcePath, []byte(dataSourceTemplate), 0644); err != nil { return fmt.Errorf("error writing data source template %s: %w", dataSourcePath, err) } From 1a26abc63fa8f881f9bb831b28c1633e1665f3bf Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:56:26 +0000 Subject: [PATCH 5/9] feat(codegen): Mark sensitive variables with sensitive flag Add sensitive: true to API key in terraform provider config and to private-key field in certificate-import spec to ensure these values are handled securely in generated Terraform code. Co-Authored-By: Claude Sonnet 4.6 --- cmd/codegen/config.yaml | 1 + specs/device/certificate-import.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/cmd/codegen/config.yaml b/cmd/codegen/config.yaml index e582f675..1fe03ef3 100644 --- a/cmd/codegen/config.yaml +++ b/cmd/codegen/config.yaml @@ -45,6 +45,7 @@ terraform_provider_config: description: "The API key for PAN-OS. Either specify this or give both username and password." env_name: "PANOS_API_KEY" optional: true + sensitive: true type: string protocol: description: "The protocol (https or http)." diff --git a/specs/device/certificate-import.yaml b/specs/device/certificate-import.yaml index 43a46b21..3a2f5296 100644 --- a/specs/device/certificate-import.yaml +++ b/specs/device/certificate-import.yaml @@ -243,6 +243,9 @@ spec: type: string profiles: - xpath: ["private-key"] + codegen_overrides: + terraform: + sensitive: true - name: passphrase type: string profiles: From 4c5e18edfa53c1932607225ab79a0f5cb3032959 Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:04:18 +0100 Subject: [PATCH 6/9] feat(ci): Add automated release pipeline - release.yml: GitHub Actions workflow that generates code, runs tests, pushes pango SDK, and creates a provider PR with release notes - determine-version.sh: Detects next version from conventional commits with custom release rules (breaking=minor, feat=patch) - generate-release-notes.sh: Generates markdown release notes grouped by commit type Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 297 ++++++++++++++++++++++++++++++ scripts/determine-version.sh | 120 ++++++++++++ scripts/generate-release-notes.sh | 99 ++++++++++ 3 files changed, 516 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100755 scripts/determine-version.sh create mode 100755 scripts/generate-release-notes.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f0424784 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,297 @@ +# Release pipeline for PAN-OS Terraform provider and pango Go SDK. +# +# Generates code, runs tests, pushes pango to main, and creates a PR +# in terraform-provider-panos. Merging that PR triggers GoReleaser +# via the auto-release workflow in the provider repo. +# +# Prerequisites: +# - GitHub App configured with contents:write on pango and terraform-provider-panos +# - Repository secrets: CODEGEN_APP_ID, CODEGEN_PRIVATE_KEY, CODEGEN_INSTALLATION_ID + +name: Release +run-name: "Release ${{ inputs.version_override || 'auto-detect' }}" + +on: + workflow_dispatch: + inputs: + version_override: + description: "Override auto-detected version (e.g. v2.1.0). Leave empty for auto-detection." + required: false + type: string + +permissions: + contents: write + +jobs: + generate-and-test: + name: Generate & Test + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + last_tag: ${{ steps.version.outputs.last_tag }} + since_date: ${{ steps.version.outputs.since_date }} + steps: + - name: Checkout pan-os-codegen + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 + with: + go-version: "1.23" + + - name: Generate code + run: make codegen + + - name: Run codegen tests + run: make test/codegen + + - name: Run pango SDK tests + run: make test/pango + + - name: Determine version + id: version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + LAST_TAG=$(gh release view --repo PaloAltoNetworks/terraform-provider-panos --json tagName -q '.tagName' 2>/dev/null || echo "v0.0.0") + SINCE_DATE=$(gh release view "$LAST_TAG" --repo PaloAltoNetworks/terraform-provider-panos --json publishedAt -q '.publishedAt' 2>/dev/null || echo "") + + if [ -n "${{ inputs.version_override }}" ]; then + VERSION="${{ inputs.version_override }}" + else + VERSION=$(bash scripts/determine-version.sh --last-tag "$LAST_TAG") + if [ "$VERSION" = "NO_BUMP" ]; then + echo "::error::No version-bumping commits found since $LAST_TAG" + exit 1 + fi + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT + echo "since_date=$SINCE_DATE" >> $GITHUB_OUTPUT + echo "## Version" >> $GITHUB_STEP_SUMMARY + echo "- Current: $LAST_TAG" >> $GITHUB_STEP_SUMMARY + echo "- Next: $VERSION" >> $GITHUB_STEP_SUMMARY + + - name: Generate release notes + run: | + bash scripts/generate-release-notes.sh \ + "${{ steps.version.outputs.version }}" \ + "${{ steps.version.outputs.since_date }}" \ + > target/release-notes.md + echo "## Release Notes" >> $GITHUB_STEP_SUMMARY + cat target/release-notes.md >> $GITHUB_STEP_SUMMARY + + - name: Validate subcategories + run: | + MISSING_VALUE=$(grep -rlE '^subcategory:\s*("")?\s*$' \ + target/terraform/docs/resources/ \ + target/terraform/docs/data-sources/ 2>/dev/null || true) + + MISSING_FIELD=$(find target/terraform/docs/resources target/terraform/docs/data-sources \ + -name "*.md" ! -exec grep -q "^subcategory:" {} \; -print 2>/dev/null || true) + + PROBLEMS="${MISSING_VALUE}${MISSING_FIELD}" + if [ -n "$PROBLEMS" ]; then + echo "::error::Resources missing subcategory:" + echo "$PROBLEMS" + exit 1 + fi + echo "All resources have valid subcategories" + + - name: Upload generated code + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: generated-code + path: | + target/pango/ + target/terraform/ + target/release-notes.md + retention-days: 3 + if-no-files-found: error + + push-pango: + name: Push Pango SDK + needs: generate-and-test + runs-on: ubuntu-latest + outputs: + has_changes: ${{ steps.push.outputs.has_changes }} + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_PRIVATE_KEY }} + installation-id: ${{ secrets.CODEGEN_INSTALLATION_ID }} + owner: PaloAltoNetworks + repositories: pango + + - name: Checkout pango + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: PaloAltoNetworks/pango + token: ${{ steps.app-token.outputs.token }} + path: pango + + - name: Download generated code + uses: actions/download-artifact@v4 + with: + name: generated-code + path: generated + + - name: Sync and push pango + id: push + run: | + # Copy generated SDK over (preserving non-generated files like .git) + rsync -av --exclude '.git' generated/pango/ pango/ + + cd pango + + if git diff --quiet && [ -z "$(git status --porcelain)" ]; then + echo "No changes in pango SDK" + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "chore: auto-generated pango SDK" + git push + + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "Pango SDK pushed to main" + + create-provider-pr: + name: Create Provider PR + needs: [generate-and-test, push-pango] + runs-on: ubuntu-latest + outputs: + pr_url: ${{ steps.create-pr.outputs.pr_url }} + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_PRIVATE_KEY }} + installation-id: ${{ secrets.CODEGEN_INSTALLATION_ID }} + owner: PaloAltoNetworks + repositories: terraform-provider-panos + + - name: Set up Go + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 + with: + go-version: "1.23" + + - name: Checkout provider + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: PaloAltoNetworks/terraform-provider-panos + token: ${{ steps.app-token.outputs.token }} + path: provider + + - name: Download generated code + uses: actions/download-artifact@v4 + with: + name: generated-code + path: generated + + - name: Sync generated code to provider + run: | + # Copy generated terraform code over, excluding repo-specific files + rsync -av --exclude '.git' --exclude '.github' --exclude '.goreleaser.yml' \ + --exclude 'GNUmakefile' --exclude 'LICENSE' --exclude 'README.md' \ + --exclude 'SUPPORT.md' --exclude 'terraform-registry-manifest.json' \ + --exclude '.gitignore' --exclude 'scripts' \ + generated/terraform/ provider/ + + - name: Update pango dependency and generate docs + working-directory: provider + run: | + # Fetch the latest pango from main (just pushed in previous job) + go get github.com/PaloAltoNetworks/pango@main + go mod tidy + + # Generate terraform plugin documentation + go generate ./... + + - name: Validate subcategories in provider + working-directory: provider + run: | + MISSING=$(grep -rlE '^subcategory:\s*("")?\s*$' \ + docs/resources/ docs/data-sources/ 2>/dev/null || true) + if [ -n "$MISSING" ]; then + echo "::error::Resources missing subcategory after doc generation: $MISSING" + exit 1 + fi + + - name: Create PR + id: create-pr + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ needs.generate-and-test.outputs.version }} + working-directory: provider + run: | + BRANCH="auto-release/${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add . + + if git diff --staged --quiet; then + echo "::error::No changes to commit in provider" + exit 1 + fi + + git commit -m "chore(release): auto-generated ${VERSION}" + git push -u origin "$BRANCH" + + RELEASE_NOTES=$(cat ../generated/release-notes.md) + + PR_URL=$(gh pr create \ + --repo PaloAltoNetworks/terraform-provider-panos \ + --title "chore(release): ${VERSION}" \ + --body "$(cat < + ${RELEASE_NOTES} + + PREOF + )") + + echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT + echo "## Provider PR" >> $GITHUB_STEP_SUMMARY + echo "Created: $PR_URL" >> $GITHUB_STEP_SUMMARY + + tag-codegen: + name: Tag Codegen + needs: [generate-and-test, create-provider-pr] + runs-on: ubuntu-latest + steps: + - name: Checkout pan-os-codegen + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 0 + + - name: Tag release + env: + VERSION: ${{ needs.generate-and-test.outputs.version }} + run: | + git tag "release/${VERSION}" + git push origin "release/${VERSION}" + echo "Tagged pan-os-codegen with release/${VERSION}" diff --git a/scripts/determine-version.sh b/scripts/determine-version.sh new file mode 100755 index 00000000..0e5beff9 --- /dev/null +++ b/scripts/determine-version.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# +# Determines the next version for terraform-provider-panos based on +# conventional commits in pan-os-codegen since the last provider release. +# +# Custom release rules (non-standard semver): +# feat(MAJOR): ... -> major bump +# BREAKING CHANGE in footer -> minor bump (not major!) +# feat: ... -> patch bump (not minor!) +# fix: ... -> patch bump +# +# Usage: +# determine-version.sh # auto-detect from local repos +# determine-version.sh --provider-dir # specify provider repo path +# determine-version.sh --last-tag # specify last tag directly (for CI) + +set -euo pipefail + +PROVIDER_DIR="" +LAST_TAG="" + +while [[ $# -gt 0 ]]; do + case $1 in + --provider-dir) PROVIDER_DIR="$2"; shift 2 ;; + --last-tag) LAST_TAG="$2"; shift 2 ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +# Resolve the last tag +if [ -z "$LAST_TAG" ]; then + if [ -n "$PROVIDER_DIR" ] && [ -d "$PROVIDER_DIR/.git" ]; then + LAST_TAG=$(cd "$PROVIDER_DIR" && git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + elif command -v gh &>/dev/null; then + LAST_TAG=$(gh release view --repo PaloAltoNetworks/terraform-provider-panos --json tagName -q '.tagName' 2>/dev/null || echo "v0.0.0") + else + echo "Error: cannot determine last tag. Provide --last-tag or --provider-dir." >&2 + exit 1 + fi +fi + +CURRENT_VERSION="${LAST_TAG#v}" + +# Determine the anchor date for codegen commits +if [ "$LAST_TAG" = "v0.0.0" ]; then + SINCE_FLAG="" +else + TAG_DATE="" + if [ -n "$PROVIDER_DIR" ] && [ -d "$PROVIDER_DIR/.git" ]; then + TAG_DATE=$(cd "$PROVIDER_DIR" && git log -1 --format="%aI" "$LAST_TAG" 2>/dev/null || echo "") + fi + if [ -z "$TAG_DATE" ] && command -v gh &>/dev/null; then + TAG_DATE=$(gh release view "$LAST_TAG" --repo PaloAltoNetworks/terraform-provider-panos --json publishedAt -q '.publishedAt' 2>/dev/null || echo "") + fi + if [ -n "$TAG_DATE" ]; then + SINCE_FLAG="--after=$TAG_DATE" + else + echo "Warning: could not determine tag date, scanning all commits" >&2 + SINCE_FLAG="" + fi +fi + +IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + +BUMP="none" + +while IFS= read -r line; do + [ -z "$line" ] && continue + + # Highest priority: feat(MAJOR) -> major bump + if echo "$line" | grep -qiE '^feat\(MAJOR\)'; then + BUMP="major" + break + fi + + # BREAKING CHANGE in commit body -> minor bump + if echo "$line" | grep -q 'BREAKING CHANGE'; then + if [ "$BUMP" != "major" ]; then + BUMP="minor" + fi + continue + fi + + # feat (but not feat(MAJOR)) -> patch bump + if echo "$line" | grep -qE '^feat(\(|:)' && ! echo "$line" | grep -qiE '^feat\(MAJOR\)'; then + if [ "$BUMP" = "none" ]; then + BUMP="patch" + fi + continue + fi + + # fix -> patch bump + if echo "$line" | grep -qE '^fix(\(|:)'; then + if [ "$BUMP" = "none" ]; then + BUMP="patch" + fi + continue + fi +done <<< "$(git log $SINCE_FLAG --format="%s%n%b" HEAD 2>/dev/null)" + +case $BUMP in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + none) + echo "NO_BUMP" + exit 0 + ;; +esac + +echo "v${MAJOR}.${MINOR}.${PATCH}" diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh new file mode 100755 index 00000000..909b45c8 --- /dev/null +++ b/scripts/generate-release-notes.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# +# Generates markdown release notes from conventional commits in pan-os-codegen. +# +# Usage: +# generate-release-notes.sh [since-date] +# +# If since-date is provided, only includes commits after that date. +# Otherwise includes all commits. + +set -euo pipefail + +VERSION="${1:?Usage: generate-release-notes.sh [since-date]}" +SINCE_DATE="${2:-}" + +LOG_ARGS="--format=%s" +if [ -n "$SINCE_DATE" ]; then + LOG_ARGS="$LOG_ARGS --after=$SINCE_DATE" +fi + +# Collect commits into arrays by type +FEATS=() +FIXES=() +BREAKING=() + +while IFS= read -r subject; do + [ -z "$subject" ] && continue + + # feat(MAJOR) -> breaking + if echo "$subject" | grep -qiE '^feat\(MAJOR\)'; then + msg=$(echo "$subject" | sed 's/^feat(MAJOR)[!]*: //') + BREAKING+=("- $msg") + continue + fi + + # feat -> feature + if echo "$subject" | grep -qE '^feat(\(|:)'; then + scope=$(echo "$subject" | sed -n 's/^feat(\([^)]*\))[!]*:.*/\1/p') + msg=$(echo "$subject" | sed 's/^feat([^)]*)[!]*: //' | sed 's/^feat[!]*: //') + if [ -n "$scope" ]; then + FEATS+=("- **$scope**: $msg") + else + FEATS+=("- $msg") + fi + continue + fi + + # fix -> bug fix + if echo "$subject" | grep -qE '^fix(\(|:)'; then + scope=$(echo "$subject" | sed -n 's/^fix(\([^)]*\))[!]*:.*/\1/p') + msg=$(echo "$subject" | sed 's/^fix([^)]*)[!]*: //' | sed 's/^fix[!]*: //') + if [ -n "$scope" ]; then + FIXES+=("- **$scope**: $msg") + else + FIXES+=("- $msg") + fi + continue + fi + + # Skip chore, docs, ci, test, refactor, style, build — internal commits +done < <(git log $LOG_ARGS HEAD 2>/dev/null) + +# Also scan commit bodies for BREAKING CHANGE +while IFS= read -r body_line; do + if echo "$body_line" | grep -q 'BREAKING CHANGE:'; then + msg=$(echo "$body_line" | sed 's/BREAKING CHANGE: //') + BREAKING+=("- $msg") + fi +done < <(git log ${SINCE_DATE:+--after=$SINCE_DATE} --format="%b" HEAD 2>/dev/null) + +# Output +echo "## What's Changed in ${VERSION}" +echo "" + +if [ ${#BREAKING[@]} -gt 0 ]; then + echo "### Breaking Changes" + echo "" + printf '%s\n' "${BREAKING[@]}" + echo "" +fi + +if [ ${#FEATS[@]} -gt 0 ]; then + echo "### Features" + echo "" + printf '%s\n' "${FEATS[@]}" + echo "" +fi + +if [ ${#FIXES[@]} -gt 0 ]; then + echo "### Bug Fixes" + echo "" + printf '%s\n' "${FIXES[@]}" + echo "" +fi + +if [ ${#BREAKING[@]} -eq 0 ] && [ ${#FEATS[@]} -eq 0 ] && [ ${#FIXES[@]} -eq 0 ]; then + echo "No notable changes." + echo "" +fi From ade3f614cd76f89bc6fe9539366131ab99088c3d Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:05:45 +0100 Subject: [PATCH 7/9] fix(ci): Pin all GitHub Actions to commit SHAs Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0424784..2d7048da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,7 +121,7 @@ jobs: steps: - name: Generate GitHub App token id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 with: app-id: ${{ secrets.CODEGEN_APP_ID }} private-key: ${{ secrets.CODEGEN_PRIVATE_KEY }} @@ -137,7 +137,7 @@ jobs: path: pango - name: Download generated code - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: generated-code path: generated @@ -174,7 +174,7 @@ jobs: steps: - name: Generate GitHub App token id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 with: app-id: ${{ secrets.CODEGEN_APP_ID }} private-key: ${{ secrets.CODEGEN_PRIVATE_KEY }} @@ -195,7 +195,7 @@ jobs: path: provider - name: Download generated code - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: generated-code path: generated From 5df7b0c129dc5fc224a884776869d46a92918078 Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:47:18 +0100 Subject: [PATCH 8/9] feat(codegen): Add skip_subcategory support for specs without subcategory Specs can now set `skip_subcategory: true` to explicitly opt out of subcategory validation. This produces docs with an empty subcategory and records the resource in a .subcategory-skip file that CI uses to exclude them from validation. Missing subcategory without the flag is now an error. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 31 +++++++++++++++++++----- pkg/commands/codegen/codegen.go | 38 +++++++++++++++++++++++------- pkg/properties/normalized.go | 2 ++ pkg/schema/object/object.go | 1 + specs/actions/commit.yaml | 1 + specs/actions/push_to_devices.yaml | 1 + 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d7048da..86b64d75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,17 +86,28 @@ jobs: - name: Validate subcategories run: | - MISSING_VALUE=$(grep -rlE '^subcategory:\s*("")?\s*$' \ + SKIP_FILE="target/terraform/.subcategory-skip" + + # Find docs with empty or missing subcategory + MISSING=$(grep -rlE '^subcategory:\s*("")?\s*$' \ target/terraform/docs/resources/ \ target/terraform/docs/data-sources/ 2>/dev/null || true) MISSING_FIELD=$(find target/terraform/docs/resources target/terraform/docs/data-sources \ -name "*.md" ! -exec grep -q "^subcategory:" {} \; -print 2>/dev/null || true) - PROBLEMS="${MISSING_VALUE}${MISSING_FIELD}" - if [ -n "$PROBLEMS" ]; then + MISSING="${MISSING}${MISSING_FIELD}" + + # Filter out resources that explicitly opted out via skip_subcategory + if [ -f "$SKIP_FILE" ] && [ -n "$MISSING" ]; then + while IFS= read -r skip; do + MISSING=$(echo "$MISSING" | grep -v "/${skip}.md" || true) + done < "$SKIP_FILE" + fi + + if [ -n "$MISSING" ]; then echo "::error::Resources missing subcategory:" - echo "$PROBLEMS" + echo "$MISSING" exit 1 fi echo "All resources have valid subcategories" @@ -220,10 +231,18 @@ jobs: go generate ./... - name: Validate subcategories in provider - working-directory: provider run: | + SKIP_FILE="generated/terraform/.subcategory-skip" + MISSING=$(grep -rlE '^subcategory:\s*("")?\s*$' \ - docs/resources/ docs/data-sources/ 2>/dev/null || true) + provider/docs/resources/ provider/docs/data-sources/ 2>/dev/null || true) + + if [ -f "$SKIP_FILE" ] && [ -n "$MISSING" ]; then + while IFS= read -r skip; do + MISSING=$(echo "$MISSING" | grep -v "/${skip}.md" || true) + done < "$SKIP_FILE" + fi + if [ -n "$MISSING" ]; then echo "::error::Resources missing subcategory after doc generation: $MISSING" exit 1 diff --git a/pkg/commands/codegen/codegen.go b/pkg/commands/codegen/codegen.go index 91c6f93b..900e76ca 100644 --- a/pkg/commands/codegen/codegen.go +++ b/pkg/commands/codegen/codegen.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "path/filepath" + "sort" "strings" "github.com/paloaltonetworks/pan-os-codegen/pkg/generate" @@ -106,9 +107,6 @@ func generateTfplugindocsTemplates(outputDir string, specMetadata map[string]pro // Generate template for resources if metadata.Flags&properties.TerraformSpecResource != 0 { subcategory := metadata.Subcategory - if subcategory == "" { - subcategory = "Uncategorized" - } resourceTemplate := fmt.Sprintf(`--- page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" @@ -151,9 +149,6 @@ Import is supported using the following syntax: // Generate template for data sources if metadata.Flags&properties.TerraformSpecDatasource != 0 { subcategory := metadata.Subcategory - if subcategory == "" { - subcategory = "Uncategorized" - } dataSourceTemplate := fmt.Sprintf(`--- page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" @@ -187,6 +182,24 @@ description: |- } slog.Info("Generated tfplugindocs templates", "resources", resourceCount, "dataSources", dataSourceCount, "templatesDir", templatesDir) + + // Write skip-list of resources that intentionally have no subcategory + var skipList []string + for suffix, metadata := range specMetadata { + if metadata.Subcategory == "" { + templateName := strings.TrimPrefix(suffix, "_") + skipList = append(skipList, templateName) + } + } + if len(skipList) > 0 { + sort.Strings(skipList) + skipPath := filepath.Join(outputDir, ".subcategory-skip") + if err := os.WriteFile(skipPath, []byte(strings.Join(skipList, "\n")+"\n"), 0644); err != nil { + return fmt.Errorf("error writing subcategory skip list: %w", err) + } + slog.Info("Wrote subcategory skip list", "count", len(skipList), "path", skipPath) + } + return nil } @@ -240,10 +253,17 @@ func (c *Command) Execute() error { return fmt.Errorf("%s sanity failed: %s", specPath, err) } - // Extract subcategory: use YAML override if present, otherwise derive from path + // Extract subcategory: use YAML override if present, otherwise derive from path. + // If skip_subcategory is set, leave it empty intentionally. if c.commandType == properties.CommandTypeTerraform { - if spec.TerraformProviderConfig.Subcategory == "" { - spec.TerraformProviderConfig.Subcategory = deriveSubcategoryFromPath(specPath) + if spec.TerraformProviderConfig.SkipSubcategory { + spec.TerraformProviderConfig.Subcategory = "" + } else if spec.TerraformProviderConfig.Subcategory == "" { + subcategory := deriveSubcategoryFromPath(specPath) + if subcategory == "" { + return fmt.Errorf("%s: no subcategory found — set 'subcategory' in the spec or use 'skip_subcategory: true' if intentional", specPath) + } + spec.TerraformProviderConfig.Subcategory = subcategory } } diff --git a/pkg/properties/normalized.go b/pkg/properties/normalized.go index 7d05c3fc..c10af2ba 100644 --- a/pkg/properties/normalized.go +++ b/pkg/properties/normalized.go @@ -82,6 +82,7 @@ const ( type TerraformProviderConfig struct { Description string `json:"description" yaml:"description"` Subcategory string `json:"subcategory" yaml:"subcategory"` + SkipSubcategory bool `json:"skip_subcategory" yaml:"skip_subcategory"` Ephemeral bool `json:"ephemeral" yaml:"ephemeral"` Action bool `json:"action" yaml:"action"` CustomValidation bool `json:"custom_validation" yaml:"custom_validation"` @@ -725,6 +726,7 @@ func schemaToSpec(object object.Object) (*Normalization, error) { SkipResource: object.TerraformConfig.SkipResource, SkipDatasource: object.TerraformConfig.SkipDatasource, SkipDatasourceListing: object.TerraformConfig.SkipdatasourceListing, + SkipSubcategory: object.TerraformConfig.SkipSubcategory, ResourceType: TerraformResourceType(object.TerraformConfig.ResourceType), XmlNode: object.TerraformConfig.XmlNode, CustomFuncs: object.TerraformConfig.CustomFunctions, diff --git a/pkg/schema/object/object.go b/pkg/schema/object/object.go index e544d49c..4951484c 100644 --- a/pkg/schema/object/object.go +++ b/pkg/schema/object/object.go @@ -43,6 +43,7 @@ type TerraformConfig struct { SkipResource bool `yaml:"skip_resource"` SkipDatasource bool `yaml:"skip_datasource"` SkipdatasourceListing bool `yaml:"skip_datasource_listing"` + SkipSubcategory bool `yaml:"skip_subcategory"` ResourceType TerraformResourceType `yaml:"resource_type"` XmlNode *string `yaml:"xml_node"` CustomFunctions map[string]bool `yaml:"custom_functions"` diff --git a/specs/actions/commit.yaml b/specs/actions/commit.yaml index 71022ba8..4a21e6fd 100644 --- a/specs/actions/commit.yaml +++ b/specs/actions/commit.yaml @@ -1,6 +1,7 @@ name: commit terraform_provider_config: description: Commit Action + skip_subcategory: true skip_resource: true skip_datasource: true resource_type: custom diff --git a/specs/actions/push_to_devices.yaml b/specs/actions/push_to_devices.yaml index dd09c8f2..bc7f7ec1 100644 --- a/specs/actions/push_to_devices.yaml +++ b/specs/actions/push_to_devices.yaml @@ -1,6 +1,7 @@ name: push_to_devices terraform_provider_config: description: Push to Devices Action + skip_subcategory: true skip_resource: true skip_datasource: true resource_type: custom From 6c8285933f55bc834c7006f480244feaf0555353 Mon Sep 17 00:00:00 2001 From: Migara Ekanayake <2110772+migara@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:58:06 +0100 Subject: [PATCH 9/9] fix(examples): Fix undeclared resource reference in virtual_wire example The ethernet interface resources referenced panos_template.template but the template resource is named "tmpl". Co-Authored-By: Claude Opus 4.6 --- .../examples/resources/panos_virtual_wire/resource.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/terraform/examples/resources/panos_virtual_wire/resource.tf b/assets/terraform/examples/resources/panos_virtual_wire/resource.tf index 7e8bf4de..2df80e8d 100644 --- a/assets/terraform/examples/resources/panos_virtual_wire/resource.tf +++ b/assets/terraform/examples/resources/panos_virtual_wire/resource.tf @@ -4,13 +4,13 @@ resource "panos_template" "tmpl" { } resource "panos_ethernet_interface" "iface1" { - location = { template = { name = resource.panos_template.template.name, vsys = "vsys1" } } + location = { template = { name = panos_template.tmpl.name, vsys = "vsys1" } } name = var.interface1 virtual_wire = {} } resource "panos_ethernet_interface" "iface2" { - location = { template = { name = resource.panos_template.template.name, vsys = "vsys1" } } + location = { template = { name = panos_template.tmpl.name, vsys = "vsys1" } } name = var.interface2 virtual_wire = {} }