Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(create-module): use stencil-golang to create modules #146

Merged
merged 6 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ linters:
- errcheck
- errorlint
- exhaustive # Checks exhaustiveness of enum switch statements.
- exportloopref # Checks for pointers to enclosing loop variables.
- copyloopvar
- funlen
- gochecknoinits
- goconst
Expand Down
5 changes: 1 addition & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"go.lintTool": "golangci-lint",
"go.formatTool": "goimports",
"go.useLanguageServer": true,
"go.buildTags": "or_dev",
"go.testFlags": ["-v"],
"files.trimTrailingWhitespace": true,
"editor.formatOnSave": true,
Expand All @@ -14,9 +13,7 @@
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml"
},
"gopls": {
"build.buildFlags": ["-tags=or_dev"]
},
"cSpell.words": ["codegen", "getoutreach", "gogit", "templating", "worktree"],
"cSpell.words": ["codegen", "gogit", "templating", "worktree"],

// Schema support OOTB.
Expand Down
102 changes: 88 additions & 14 deletions cmd/stencil/create_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"fmt"
"os"
"path"
"strings"

"github.com/pkg/errors"
"github.com/urfave/cli/v2"
Expand All @@ -27,21 +28,94 @@
"gopkg.in/yaml.v3"
)

// encodeToFile encodes the provided data [d] to the provided file path,
// it is done by streaming to the created file.
func encodeToFile(d any, outputFilePath string) error {
f, err := os.Create(outputFilePath)
if err != nil {
return err

Check warning on line 36 in cmd/stencil/create_module.go

View check run for this annotation

Codecov / codecov/patch

cmd/stencil/create_module.go#L36

Added line #L36 was not covered by tests
}
defer f.Close()

enc := yaml.NewEncoder(f)
if err := enc.Encode(d); err != nil {
return err

Check warning on line 42 in cmd/stencil/create_module.go

View check run for this annotation

Codecov / codecov/patch

cmd/stencil/create_module.go#L42

Added line #L42 was not covered by tests
}

return enc.Close()
}

// generateTemplateRepository generates a template repository manifest
// (manifest.yaml) based on the provided input.
func generateTemplateRepository(name string, hasNativeExt bool) *configuration.TemplateRepositoryManifest {
tr := &configuration.TemplateRepositoryManifest{
Name: name,
}

tr.Type = configuration.TemplateRepositoryTypes{}
if hasNativeExt {
tr.Type = append(tr.Type, configuration.TemplateRepositoryTypeTemplates, configuration.TemplateRepositoryTypeExt)
}

return tr
}

// generateStencilYaml generates a stencil.yaml manifest based on the
// provided input.
func generateStencilYaml(name string, hasNativeExt bool) *configuration.Manifest {
mf := &configuration.Manifest{
Name: path.Base(name),
Modules: []*configuration.TemplateRepository{{
Name: "github.com/rgst-io/stencil-module",
}},
Arguments: map[string]any{
"org": strings.Split(strings.TrimPrefix(name, "github.com/"), "/")[0],
},
}

if hasNativeExt {
mf.Arguments["commands"] = []string{
"plugin",
}
} else {
mf.Arguments["library"] = true
}

return mf
}

// NewCreateModuleCommand returns a new urfave/cli.Command for the
// create module command.
func NewCreateModuleCommand(log slogext.Logger) *cli.Command {
return &cli.Command{
Name: "module",
Description: "Creates a module with the provided name in the current directory",
ArgsUsage: "create module <name>",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "native-extension",
Usage: "Generate a module with a native extension. ",
},
},
Action: func(c *cli.Context) error {
var manifestFileName = "stencil.yaml"
var stencilManifestName = "stencil.yaml"

// ensure we have a name
if c.NArg() != 1 {
return errors.New("must provide a name for the module")
}

moduleName := c.Args().Get(0)
hasNativeExt := c.Bool("native-extension")

// stencil-golang requires Github right now, so it doesn't make
// sense to generate broken templates on some other VCS provider.
// Note that you can still have template modules on, say, Gitlab,
// but we just can't generate them (yet!).
if !strings.HasPrefix(moduleName, "github.com/") {
return fmt.Errorf("currently, only github based modules are supported")

Check warning on line 116 in cmd/stencil/create_module.go

View check run for this annotation

Codecov / codecov/patch

cmd/stencil/create_module.go#L116

Added line #L116 was not covered by tests
}

allowedFiles := map[string]struct{}{
".git": {},
}
Expand All @@ -58,26 +132,26 @@
}
}

tm := &configuration.Manifest{
Name: path.Base(c.Args().Get(0)),
// create stencil.yaml
if err := encodeToFile(generateStencilYaml(moduleName, hasNativeExt), stencilManifestName); err != nil {
return fmt.Errorf("failed to serialize %s: %w", stencilManifestName, err)

Check warning on line 137 in cmd/stencil/create_module.go

View check run for this annotation

Codecov / codecov/patch

cmd/stencil/create_module.go#L137

Added line #L137 was not covered by tests
}

f, err := os.Create(manifestFileName)
if err != nil {
return err
if err := encodeToFile(generateTemplateRepository(moduleName, hasNativeExt), "manifest.yaml"); err != nil {
return fmt.Errorf("failed to serialize generated manifest.yaml: %w", err)

Check warning on line 141 in cmd/stencil/create_module.go

View check run for this annotation

Codecov / codecov/patch

cmd/stencil/create_module.go#L141

Added line #L141 was not covered by tests
}
defer f.Close()

enc := yaml.NewEncoder(f)
if err := enc.Encode(tm); err != nil {
return err
}
if err := enc.Close(); err != nil {
// Run the standard stencil command.
if err := NewStencilAction(log)(cli.NewContext(c.App, flag.NewFlagSet("", flag.ExitOnError), c)); err != nil {
return err
}

// Run the standard stencil command.
return NewStencilAction(log)(cli.NewContext(c.App, flag.NewFlagSet("", flag.ExitOnError), c))
fmt.Println()
log.Info("Created module successfully", "module", moduleName)
log.Info("- Ensure that 'stencil.yaml' is configured to your liking (e.g., license)")
log.Info("- For configuration options provided by stencil-golang, see the docs:")
log.Info(" https://github.com/rgst-io/stencil-golang")
return nil
},
}
}
53 changes: 33 additions & 20 deletions cmd/stencil/create_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"testing"

"github.com/urfave/cli/v2"
"go.rgst.io/stencil/pkg/configuration"
"go.rgst.io/stencil/pkg/slogext"
"gotest.tools/v3/assert"
"gotest.tools/v3/env"
)

// prepareTestRun sets up the environment for running a stencil command.
Expand All @@ -22,13 +24,14 @@ func prepareTestRun(t *testing.T, dir string) {
b, err := exec.Command("go", "env", "GOMOD").Output()
assert.NilError(t, err)
repoRoot := strings.TrimSuffix(strings.TrimSpace(string(b)), "/go.mod")
chdir(t, repoRoot)

env.ChangeWorkingDir(t, repoRoot)

// Use a temporary directory for the test if one is not provided.
if dir == "" {
dir = t.TempDir()
}
chdir(t, dir)
env.ChangeWorkingDir(t, dir)
}

// testRunApp runs the provided cli.App with the provided arguments.
Expand All @@ -48,27 +51,11 @@ func testRunCommand(t *testing.T, cmd *cli.Command, dir string, args ...string)
return app.Run(append([]string{"test", cmd.Name}, args...))
}

// chdir changes the current working directory to the provided directory
// and sets up a cleanup function to change it back to the original
// directory when the test is done. If the cleanup function fails, the
// test will panic.
func chdir(t *testing.T, dir string) {
origDir, err := os.Getwd()
assert.NilError(t, err)
assert.NilError(t, os.Chdir(dir))
t.Cleanup(func() {
if err := os.Chdir(origDir); err != nil {
// Failed, not safe to run other tests.
panic(err)
}
})
}

func TestCanCreateModule(t *testing.T) {
log := slogext.NewTestLogger(t)
cmd := NewCreateModuleCommand(log)
assert.Assert(t, cmd != nil)
assert.NilError(t, testRunCommand(t, cmd, "", "test-module"))
assert.NilError(t, testRunCommand(t, cmd, "", "github.com/rgst-io/test-module"))

// Ensure it created the expected files.
_, err := os.Stat("stencil.yaml")
Expand All @@ -87,6 +74,32 @@ func TestCreateModuleFailsWhenFilesExist(t *testing.T) {
assert.NilError(t, err)
assert.NilError(t, f.Close())

err = testRunCommand(t, cmd, tmpDir, "test-module")
err = testRunCommand(t, cmd, tmpDir, "github.com/rgst-io/test-module")
assert.ErrorContains(t, err, "directory is not empty, found test-file")
}

// TestCanCreateNativeExtension ensures that we can render a native
// extension through stencil-golang. This is technically more of a test
// of stencil-golang than anything else.
func TestCanCreateNativeExtension(t *testing.T) {
log := slogext.NewTestLogger(t)
cmd := NewCreateModuleCommand(log)
assert.Assert(t, cmd != nil)

assert.NilError(t, testRunCommand(t, cmd, "", "--native-extension", "github.com/rgst-io/test-module"))

// Ensure it created the expected files.
expectedFiles := []string{
filepath.Join("cmd", "plugin", "plugin.go"),
"stencil.yaml",
}
for _, f := range expectedFiles {
_, err := os.Stat(f)
assert.NilError(t, err)
}

// Ensure that we have 'plugin' as one of our types.
tr, err := configuration.LoadDefaultTemplateRepositoryManifest()
assert.NilError(t, err)
assert.Assert(t, tr.Type.Contains(configuration.TemplateRepositoryTypeExt))
}
3 changes: 2 additions & 1 deletion cmd/stencil/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"go.rgst.io/stencil/pkg/stencil"
"gopkg.in/yaml.v3"
"gotest.tools/v3/assert"
"gotest.tools/v3/env"
)

func Test_cleanPath(t *testing.T) {
Expand Down Expand Up @@ -83,7 +84,7 @@ func Test_cleanPath(t *testing.T) {
func Test_describeFile_shouldFunction(t *testing.T) {
tmpDir := t.TempDir()

chdir(t, tmpDir)
env.ChangeWorkingDir(t, tmpDir)

lock := &stencil.Lockfile{
Modules: []*stencil.LockfileModuleEntry{{
Expand Down
19 changes: 11 additions & 8 deletions cmd/stencil/lockfile_prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@ import (
// lockfile prune command.
func NewLockfilePruneCommand(log slogext.Logger) *cli.Command {
return &cli.Command{
Name: "prune",
Usage: "Prunes non-existent files from the lockfile",
Description: "Prunes any non-existent files from the lockfile (will recreate any file.Once files on next run)",
Name: "prune",
Usage: "Prunes non-existent files from the lockfile",
Description: "Prunes any non-existent files from the lockfile (will " +
"recreate any file.Once files on next run)",

Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "file",
Usage: "If any file options are passed, prune only checks the passed filenames for pruning",
Name: "file",
Usage: "If any file options are passed, prune only checks the " +
"passed filenames for pruning",
},
&cli.StringSliceFlag{
Name: "module",
Usage: "If any module options are passed, prune only checks the passed modulenames for pruning",
Name: "module",
Usage: "If any module options are passed, prune only checks the " +
"passed module names for pruning",
},
},
Action: func(c *cli.Context) error {
Expand All @@ -48,7 +51,7 @@ func NewLockfilePruneCommand(log slogext.Logger) *cli.Command {
return errors.Wrap(err, "failed to load lockfile")
}

manifest, err := configuration.NewDefaultManifest()
manifest, err := configuration.LoadDefaultManifest()
if err != nil {
return fmt.Errorf("failed to parse stencil.yaml: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/stencil/stencil.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func NewStencilAction(log slogext.Logger) cli.ActionFunc {
log.Debug("Debug logging enabled")
}

manifest, err := configuration.NewDefaultManifest()
manifest, err := configuration.LoadDefaultManifest()
if err != nil {
return fmt.Errorf("failed to parse stencil.yaml: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/stencil/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewUpgradeCommand(log slogext.Logger) *cli.Command {
log.Debug("Debug logging enabled")
}

manifest, err := configuration.NewDefaultManifest()
manifest, err := configuration.LoadDefaultManifest()
if err != nil {
return fmt.Errorf("failed to parse stencil.yaml: %w", err)
}
Expand Down
7 changes: 1 addition & 6 deletions cmd/stencil/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,7 @@ func TestUpgradeIncludesNewModules(t *testing.T) {
assert.NilError(t, err, "failed to write lockfile")

// Run the upgrade
err = testRunCommand(t, cmd, tmpDir)
if err != nil {
// Right now it errors due to no go.mod, so allow that error to
// occur. It doesn't indicate the upgrade failure.
assert.ErrorContains(t, err, "failed to run post run command")
}
assert.NilError(t, testRunCommand(t, cmd, tmpDir), "expected upgrade to not error")

// Read the lockfile back in and ensure that the module was added.
lf, err := stencil.LoadLockfile(tmpDir)
Expand Down
9 changes: 2 additions & 7 deletions internal/codegen/values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,14 @@ import (
"go.rgst.io/stencil/pkg/configuration"
"go.rgst.io/stencil/pkg/slogext"
"gotest.tools/v3/assert"
"gotest.tools/v3/env"
)

func TestValues(t *testing.T) {
tmpDir, err := os.MkdirTemp(t.TempDir(), "stencil-values-test")
assert.NilError(t, err, "expected os.MkdirTemp() not to fail")

wd, err := os.Getwd()
assert.NilError(t, err, "expected os.Getwd() not to fail")

// Change directory to the temporary directory, and restore the original
// working directory when we're done.
os.Chdir(tmpDir)
defer func() { os.Chdir(wd) }()
env.ChangeWorkingDir(t, tmpDir)

r, err := gogit.PlainInit(tmpDir, false)
assert.NilError(t, err, "expected gogit.PlainInit() not to fail")
Expand Down
Loading
Loading