diff --git a/.github/workflows/ci.yml b/.github/workflows/checks.yml similarity index 53% rename from .github/workflows/ci.yml rename to .github/workflows/checks.yml index 6a914bc..f5354e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/checks.yml @@ -1,4 +1,4 @@ -name: CI +name: Checks on: push: branches: @@ -32,24 +32,3 @@ jobs: go-version-file: go.mod - run: make install-tools - run: make test - - release: - name: Release - runs-on: ubuntu-latest - needs: test - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - name: go-with-cache - uses: magnetikonline/action-golang-cache@v4 - with: - go-version-file: go.mod - - run: make install-tools - - uses: go-semantic-release/action@v1 - with: - bin: ./bin/semantic-release - hooks: goreleaser - allow-initial-development-versions: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0f990af --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release +on: + workflow_run: + workflows: + - Checks + branches: + - main + types: + - completed + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: go-with-cache + uses: magnetikonline/action-golang-cache@v4 + with: + go-version-file: go.mod + - run: make install-tools + - uses: go-semantic-release/action@v1 + with: + bin: ./bin/semantic-release + hooks: goreleaser + allow-initial-development-versions: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cmd/module.go b/cmd/module.go index 3d3e82e..f7fa0a9 100644 --- a/cmd/module.go +++ b/cmd/module.go @@ -42,10 +42,24 @@ var moduleCmd = &cobra.Command{ BackendType: backend, } - err = tfmodule.CreateModule(createModuleOptions) + fullModulePath, err := tfmodule.CreateModuleDir(createModuleOptions) if err != nil { return err } + defaultFiles, err := tfmodule.CreateDefaultModuleFiles(fullModulePath) + if err != nil { + return fmt.Errorf("error creating moduleName files: %w", err) + } + err = tfmodule.PopulateVersionsFile(defaultFiles[tfmodule.VersionsFile], createModuleOptions) + if err != nil { + return fmt.Errorf("error populating the versions.tf file: %w", err) + } + + defer func() { + for _, file := range defaultFiles { + _ = file.Close() + } + }() return nil }, } diff --git a/go.mod b/go.mod index a32deb3..5bf5237 100644 --- a/go.mod +++ b/go.mod @@ -295,7 +295,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -380,7 +380,7 @@ require ( go.uber.org/zap v1.24.0 // indirect gocloud.dev v0.34.0 // indirect golang.org/x/crypto v0.16.0 // indirect - golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect diff --git a/go.sum b/go.sum index 2a04d2a..c999863 100644 --- a/go.sum +++ b/go.sum @@ -1075,8 +1075,8 @@ github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAv github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -1423,8 +1423,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= -golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 h1:jWGQJV4niP+CCmFW9ekjA9Zx8vYORzOUH2/Nl5WPuLQ= diff --git a/internal/releases/releases.go b/internal/releases/releases.go index 2902450..af4e2ba 100644 --- a/internal/releases/releases.go +++ b/internal/releases/releases.go @@ -1,13 +1,42 @@ package releases import ( + "encoding/json" "fmt" ) -func GetLatestProviderRelease(provider string) (map[string]interface{}, error) { - return makeGetRequest(fmt.Sprintf("https://registry.terraform.io/v1/providers/%s", provider)) +type ProviderData struct { + Name string + Version string +} +type TerraformData struct { + Name string + Version string +} + +func GetLatestProviderRelease(provider string) (*ProviderData, error) { + providerResponse, err := makeGetRequest(fmt.Sprintf("https://registry.terraform.io/v1/providers/%s", provider)) + if err != nil { + return nil, err + } + var providerData ProviderData + err = json.Unmarshal(providerResponse, &providerData) + if err != nil { + return nil, err + } + + return &providerData, nil } -func GetLatestTerraformRelease() (map[string]interface{}, error) { - return makeGetRequest("https://api.releases.hashicorp.com/v1/releases/terraform/latest") +func GetLatestTerraformRelease() (*TerraformData, error) { + terraformResponse, err := makeGetRequest("https://api.releases.hashicorp.com/v1/releases/terraform/latest") + if err != nil { + return nil, err + } + var terraformData TerraformData + err = json.Unmarshal(terraformResponse, &terraformData) + if err != nil { + return nil, err + } + return &terraformData, nil } diff --git a/internal/releases/util.go b/internal/releases/util.go index a64dbf2..7b68dce 100644 --- a/internal/releases/util.go +++ b/internal/releases/util.go @@ -1,12 +1,11 @@ package releases import ( - "encoding/json" "io" "net/http" ) -func makeGetRequest(apiURL string) (map[string]interface{}, error) { +func makeGetRequest(apiURL string) ([]byte, error) { req, err := http.NewRequest(http.MethodGet, apiURL, nil) if err != nil { return nil, err @@ -28,12 +27,5 @@ func makeGetRequest(apiURL string) (map[string]interface{}, error) { return nil, err } - var response map[string]interface{} - - err = json.Unmarshal(body, &response) - if err != nil { - return nil, err - } - - return response, nil + return body, nil } diff --git a/internal/tfmodule/tfmodule.go b/internal/tfmodule/tfmodule.go index 78fe1f2..e66f226 100644 --- a/internal/tfmodule/tfmodule.go +++ b/internal/tfmodule/tfmodule.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "regexp" "strings" "github.com/hashicorp/hcl/v2/hclwrite" @@ -14,7 +15,7 @@ import ( "github.com/fitz7/tfnew/internal/releases" ) -const versionsFile = "versions.tf" +const VersionsFile = "versions.tf" var defaultFilenames = []string{versionsFile, "variables.tf", "outputs.tf", "main.tf"} @@ -26,34 +27,18 @@ type CreateModuleOptions struct { BackendType string } -func CreateModule(cmo CreateModuleOptions) error { +func CreateModuleDir(cmo CreateModuleOptions) (string, error) { fullPathWithModuleName := fmt.Sprintf("%s/%s", fsutils.FindProjectRootDir(), cmo.Name) err := os.Mkdir(fullPathWithModuleName, 0o755) if err != nil { - return fmt.Errorf("error creating directory: %w", err) + return "", fmt.Errorf("error creating directory: %w", err) } - defaultFiles, err := createDefaultModuleFiles(fullPathWithModuleName) - if err != nil { - return fmt.Errorf("error creating moduleName files: %w", err) - } - - defer func() { - for _, file := range defaultFiles { - _ = file.Close() - } - }() - - err = populateVersionsFile(defaultFiles[versionsFile], cmo) - if err != nil { - return fmt.Errorf("error populating the versions.tf file: %w", err) - } - - return nil + return fullPathWithModuleName, nil } -func createDefaultModuleFiles(path string) (map[string]*os.File, error) { +func CreateDefaultModuleFiles(path string) (map[string]*os.File, error) { defaultFiles := make(map[string]*os.File) for _, filename := range defaultFilenames { @@ -68,7 +53,7 @@ func createDefaultModuleFiles(path string) (map[string]*os.File, error) { return defaultFiles, nil } -func populateVersionsFile(versionsFile *os.File, cmo CreateModuleOptions) error { +func PopulateVersionsFile(versionsFile *os.File, cmo CreateModuleOptions) error { f := hclwrite.NewEmptyFile() body := f.Body() @@ -176,24 +161,17 @@ func addRequiredProvidersBlock(cmo CreateModuleOptions, body *hclwrite.Body) err requiredProvidersBody := body.AppendNewBlock("required_providers", []string{}).Body() for _, provider := range cmo.RequiredProviders { - latestProviderRelease, err := releases.GetLatestProviderRelease(provider) + latestProviderData, err := releases.GetLatestProviderRelease(provider) if err != nil { return err } - providerName, ok := latestProviderRelease["name"].(string) - if !ok { - return fmt.Errorf("could not find name for provider: %s", provider) - } - - latestProviderVersion, ok := latestProviderRelease["version"].(string) - if !ok { - return fmt.Errorf("could not find version for provider: %s", provider) + minorProviderVersion, err := truncatePatchVersion(latestProviderData.Version) + if err != nil { + return fmt.Errorf("%s provider version for: %s", latestProviderData.Name, err.Error()) } - minorProviderVersion := truncatePatchVersion(latestProviderVersion) - - requiredProvidersBody.SetAttributeValue(providerName, cty.ObjectVal(map[string]cty.Value{ + requiredProvidersBody.SetAttributeValue(latestProviderData.Name, cty.ObjectVal(map[string]cty.Value{ "source": cty.StringVal(provider), "version": cty.StringVal(fmt.Sprintf("~> %s", minorProviderVersion)), })) @@ -214,19 +192,21 @@ func getTerraformVersion(rootModule bool) (string, error) { return "", errors.New("failed to fetch terraform version") } - latestTerraformVersion, ok := latestTerraformRelease["version"].(string) - if !ok { - return "", err + minorTerraformVersion, err := truncatePatchVersion(latestTerraformRelease.Version) + if err != nil { + return "", fmt.Errorf("terraform %s", err.Error()) } - minorTerraformVersion := truncatePatchVersion(latestTerraformVersion) - terraformVersion = fmt.Sprintf("~> %s", minorTerraformVersion) } return terraformVersion, nil } -func truncatePatchVersion(version string) string { - return strings.Join(strings.Split(version, ".")[:2], ".") +func truncatePatchVersion(version string) (string, error) { + versionMatch := regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`) + if !versionMatch.MatchString(version) { + return "", errors.New("version returned is not valid semver") + } + return strings.Join(strings.Split(version, ".")[:2], "."), nil } diff --git a/internal/tfmodule/tfmodule_test.go b/internal/tfmodule/tfmodule_test.go new file mode 100644 index 0000000..e6c89cb --- /dev/null +++ b/internal/tfmodule/tfmodule_test.go @@ -0,0 +1,60 @@ +package tfmodule + +import ( + "testing" +) + +func Test_truncatePatchVersion(t *testing.T) { + type args struct { + version string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "regular version", + args: args{version: "1.2.3"}, + want: "1.2", + wantErr: false, + }, + { + name: "minor only version", + args: args{version: "1.2"}, + want: "", + wantErr: true, + }, + { + name: "major only version", + args: args{version: "1"}, + want: "", + wantErr: true, + }, + { + name: "regular big", + args: args{version: "12.34.56"}, + want: "12.34", + wantErr: false, + }, + { + name: "bad version", + args: args{version: "02.34.56"}, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := truncatePatchVersion(tt.args.version) + if (err != nil) != tt.wantErr { + t.Errorf("truncatePatchVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("truncatePatchVersion() = %v, want %v", got, tt.want) + } + }) + } +}